From d4aedf59f334f3e945f3df9ce1bad1bc7c86e707 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 12 Mar 2026 18:20:55 +0900 Subject: [PATCH] A- Add more tests. - Object Storage support has also been confirmed (and fixed) in CLI. --- package-lock.json | 86 +++- src/apps/cli/commands/runCommand.ts | 9 +- src/apps/cli/commands/runCommand.unit.spec.ts | 204 ++++++++ src/apps/cli/commands/utils.ts | 8 +- src/apps/cli/commands/utils.unit.spec.ts | 29 ++ src/apps/cli/main.ts | 32 +- src/apps/cli/main.unit.spec.ts | 61 +++ src/apps/cli/package.json | 9 +- .../cli/test/test-e2e-two-vaults-common.sh | 445 ++++++++++++++++++ .../cli/test/test-e2e-two-vaults-matrix.sh | 31 ++ .../test-e2e-two-vaults-with-docker-linux.sh | 246 +--------- src/apps/cli/vite.config.ts | 11 +- src/lib | 2 +- 13 files changed, 892 insertions(+), 281 deletions(-) create mode 100644 src/apps/cli/commands/runCommand.unit.spec.ts create mode 100644 src/apps/cli/commands/utils.unit.spec.ts create mode 100644 src/apps/cli/main.unit.spec.ts create mode 100755 src/apps/cli/test/test-e2e-two-vaults-common.sh create mode 100755 src/apps/cli/test/test-e2e-two-vaults-matrix.sh mode change 100644 => 100755 src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh diff --git a/package-lock.json b/package-lock.json index dfb7f22..d89d159 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1939,6 +1939,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz", "integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -2005,6 +2006,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz", "integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.4", "@firebase/component": "0.7.0", @@ -2020,7 +2022,8 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/app/node_modules/idb": { "version": "7.1.1", @@ -2489,6 +2492,7 @@ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -3081,8 +3085,7 @@ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@multiformats/dns": { "version": "1.0.10", @@ -4944,6 +4947,7 @@ "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", @@ -5423,6 +5427,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5627,6 +5632,7 @@ "integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/mocker": "4.0.16", "@vitest/utils": "4.0.16", @@ -5650,6 +5656,7 @@ "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/browser": "4.0.16", "@vitest/mocker": "4.0.16", @@ -6394,6 +6401,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7402,8 +7410,7 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -8286,6 +8293,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8396,6 +8404,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10768,6 +10777,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -11319,6 +11329,7 @@ "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-2.8.11.tgz", "integrity": "sha512-EjkyN0CI6uP+e4OOkEcZvhbZtlwFl4Y0rkkMvDbXmcfILX4E4n/jKE4Ppoc1qhNufxToxVWCMDS2ipniQgiYaw==", "license": "Apache-2.0 OR MIT", + "peer": true, "dependencies": { "@chainsafe/is-ip": "^2.1.0", "@chainsafe/netmask": "^2.0.0", @@ -12619,6 +12630,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12643,6 +12655,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "lilconfig": "^3.1.1" }, @@ -14213,8 +14226,7 @@ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sublevel-pouchdb": { "version": "9.0.0", @@ -14281,6 +14293,7 @@ "integrity": "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -14598,6 +14611,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14725,6 +14739,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -15330,6 +15345,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15466,6 +15482,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16026,6 +16043,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16059,6 +16077,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -16154,8 +16173,7 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/wait-port": { "version": "1.1.0", @@ -16671,6 +16689,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -18018,6 +18037,7 @@ "version": "0.14.4", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz", "integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==", + "peer": true, "requires": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -18071,6 +18091,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz", "integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==", + "peer": true, "requires": { "@firebase/app": "0.14.4", "@firebase/component": "0.7.0", @@ -18082,7 +18103,8 @@ "@firebase/app-types": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", - "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "peer": true }, "@firebase/auth": { "version": "1.11.0", @@ -18410,6 +18432,7 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "peer": true, "requires": { "tslib": "^2.1.0" } @@ -18882,8 +18905,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "dev": true, - "peer": true + "dev": true }, "@multiformats/dns": { "version": "1.0.10", @@ -20067,6 +20089,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, + "peer": true, "requires": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", @@ -20494,6 +20517,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -20601,6 +20625,7 @@ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.16.tgz", "integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==", "dev": true, + "peer": true, "requires": { "@vitest/mocker": "4.0.16", "@vitest/utils": "4.0.16", @@ -20617,6 +20642,7 @@ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.16.tgz", "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", "dev": true, + "peer": true, "requires": { "@vitest/browser": "4.0.16", "@vitest/mocker": "4.0.16", @@ -21139,7 +21165,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -21813,8 +21840,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true, - "peer": true + "dev": true }, "cross-spawn": { "version": "7.0.6", @@ -22407,6 +22433,7 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, + "peer": true, "requires": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", @@ -22482,6 +22509,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -24077,7 +24105,8 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true + "dev": true, + "peer": true }, "js-sdsl": { "version": "4.3.0", @@ -24471,6 +24500,7 @@ "version": "2.8.11", "resolved": "https://registry.npmjs.org/libp2p/-/libp2p-2.8.11.tgz", "integrity": "sha512-EjkyN0CI6uP+e4OOkEcZvhbZtlwFl4Y0rkkMvDbXmcfILX4E4n/jKE4Ppoc1qhNufxToxVWCMDS2ipniQgiYaw==", + "peer": true, "requires": { "@chainsafe/is-ip": "^2.1.0", "@chainsafe/netmask": "^2.0.0", @@ -25355,6 +25385,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "peer": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25366,6 +25397,7 @@ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, + "peer": true, "requires": { "lilconfig": "^3.1.1" } @@ -26468,8 +26500,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "dev": true, - "peer": true + "dev": true }, "sublevel-pouchdb": { "version": "9.0.0", @@ -26524,6 +26555,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.41.1.tgz", "integrity": "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==", "dev": true, + "peer": true, "requires": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -26708,7 +26740,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true } } }, @@ -26806,6 +26839,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, + "peer": true, "requires": { "esbuild": "~0.27.0", "fsevents": "~2.3.3", @@ -27105,7 +27139,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "peer": true }, "uint8-varint": { "version": "2.0.4", @@ -27206,6 +27241,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -27436,7 +27472,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true } } }, @@ -27452,6 +27489,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, + "peer": true, "requires": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -27492,8 +27530,7 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "dev": true, - "peer": true + "dev": true }, "wait-port": { "version": "1.1.0", @@ -27862,7 +27899,8 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true + "dev": true, + "peer": true }, "yargs": { "version": "17.7.2", diff --git a/src/apps/cli/commands/runCommand.ts b/src/apps/cli/commands/runCommand.ts index 9137920..18ccb35 100644 --- a/src/apps/cli/commands/runCommand.ts +++ b/src/apps/cli/commands/runCommand.ts @@ -151,7 +151,8 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext if (body.type === "text/plain") { process.stdout.write(await body.text()); } else { - process.stdout.write(Buffer.from(await body.arrayBuffer())); + const buffer = Buffer.from(await body.arrayBuffer()); + process.stdout.write(new Uint8Array(buffer)); } return true; } @@ -178,7 +179,8 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext if (body.type === "text/plain") { process.stdout.write(await body.text()); } else { - process.stdout.write(Buffer.from(await body.arrayBuffer())); + const buffer = Buffer.from(await body.arrayBuffer()); + process.stdout.write(new Uint8Array(buffer)); } return true; } @@ -236,8 +238,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext return entry.status === "available"; }) .map((entry: { rev: string }) => entry.rev); - const pastRevisionsText = - pastRevisions.length > 0 ? pastRevisions.map((rev: string) => `${rev}`) : ["N/A"]; + const pastRevisionsText = pastRevisions.length > 0 ? pastRevisions.map((rev: string) => `${rev}`) : ["N/A"]; const out = { id: doc._id, revision: doc._rev ?? "", diff --git a/src/apps/cli/commands/runCommand.unit.spec.ts b/src/apps/cli/commands/runCommand.unit.spec.ts new file mode 100644 index 0000000..a616656 --- /dev/null +++ b/src/apps/cli/commands/runCommand.unit.spec.ts @@ -0,0 +1,204 @@ +import * as processSetting from "@lib/API/processSetting"; +import { configURIBase } from "@lib/common/models/shared.const"; +import { DEFAULT_SETTINGS } from "@lib/common/types"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { runCommand } from "./runCommand"; +import type { CLIOptions } from "./types"; +import * as commandUtils from "./utils"; + +function createCoreMock() { + return { + services: { + control: { + activated: Promise.resolve(), + applySettings: vi.fn(async () => {}), + }, + setting: { + applyPartial: vi.fn(async () => {}), + }, + }, + serviceModules: { + fileHandler: { + dbToStorage: vi.fn(async () => true), + storeFileToDB: vi.fn(async () => true), + }, + storageAccess: { + readFileAuto: vi.fn(async () => ""), + writeFileAuto: vi.fn(async () => {}), + }, + databaseFileAccess: { + fetch: vi.fn(async () => undefined), + }, + }, + } as any; +} + +function makeOptions(command: CLIOptions["command"], commandArgs: string[]): CLIOptions { + return { + command, + commandArgs, + databasePath: "/tmp/vault", + verbose: false, + force: false, + }; +} + +async function createSetupURI(passphrase: string): Promise { + const settings = { + ...DEFAULT_SETTINGS, + couchDB_URI: "http://127.0.0.1:5984", + couchDB_DBNAME: "livesync-test-db", + couchDB_USER: "user", + couchDB_PASSWORD: "pass", + isConfigured: true, + } as any; + return await processSetting.encodeSettingsToSetupURI(settings, passphrase); +} + +describe("runCommand abnormal cases", () => { + const context = { + vaultPath: "/tmp/vault", + settingsPath: "/tmp/vault/.livesync/settings.json", + } as any; + + beforeEach(() => { + vi.restoreAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("pull returns false for non-existing path", async () => { + const core = createCoreMock(); + core.serviceModules.fileHandler.dbToStorage.mockResolvedValue(false); + + const result = await runCommand(makeOptions("pull", ["missing.md", "/tmp/out.md"]), { + ...context, + core, + }); + + expect(result).toBe(false); + expect(core.serviceModules.fileHandler.dbToStorage).toHaveBeenCalled(); + }); + + it("pull-rev throws on empty revision", async () => { + const core = createCoreMock(); + + await expect( + runCommand(makeOptions("pull-rev", ["file.md", "/tmp/out.md", " "]), { + ...context, + core, + }) + ).rejects.toThrow("pull-rev requires a non-empty revision"); + }); + + it("pull-rev returns false for invalid revision", async () => { + const core = createCoreMock(); + core.serviceModules.databaseFileAccess.fetch.mockResolvedValue(undefined); + + const result = await runCommand(makeOptions("pull-rev", ["file.md", "/tmp/out.md", "9-invalid"]), { + ...context, + core, + }); + + expect(result).toBe(false); + expect(core.serviceModules.databaseFileAccess.fetch).toHaveBeenCalledWith("file.md", "9-invalid", true); + }); + + it("cat-rev throws on empty revision", async () => { + const core = createCoreMock(); + + await expect( + runCommand(makeOptions("cat-rev", ["file.md", " "]), { + ...context, + core, + }) + ).rejects.toThrow("cat-rev requires a non-empty revision"); + }); + + it("cat-rev returns false for invalid revision", async () => { + const core = createCoreMock(); + core.serviceModules.databaseFileAccess.fetch.mockResolvedValue(undefined); + + const result = await runCommand(makeOptions("cat-rev", ["file.md", "9-invalid"]), { + ...context, + core, + }); + + expect(result).toBe(false); + expect(core.serviceModules.databaseFileAccess.fetch).toHaveBeenCalledWith("file.md", "9-invalid", true); + }); + + it("push rejects when source file does not exist", async () => { + const core = createCoreMock(); + + await expect( + runCommand(makeOptions("push", ["/tmp/livesync-missing-src-file.md", "dst.md"]), { + ...context, + core, + }) + ).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("setup rejects invalid URI", async () => { + const core = createCoreMock(); + + await expect( + runCommand(makeOptions("setup", ["https://invalid.example/setup"]), { + ...context, + core, + }) + ).rejects.toThrow(`setup URI must start with ${configURIBase}`); + }); + + it("setup rejects empty passphrase", async () => { + const core = createCoreMock(); + vi.spyOn(commandUtils, "promptForPassphrase").mockRejectedValue(new Error("Passphrase is required")); + + await expect( + runCommand(makeOptions("setup", [`${configURIBase}dummy`]), { + ...context, + core, + }) + ).rejects.toThrow("Passphrase is required"); + }); + + it("setup accepts URI generated by encodeSettingsToSetupURI", async () => { + const core = createCoreMock(); + const passphrase = "correct-passphrase"; + const setupURI = await createSetupURI(passphrase); + vi.spyOn(commandUtils, "promptForPassphrase").mockResolvedValue(passphrase); + + const result = await runCommand(makeOptions("setup", [setupURI]), { + ...context, + core, + }); + + expect(result).toBe(true); + expect(core.services.setting.applyPartial).toHaveBeenCalledTimes(1); + expect(core.services.control.applySettings).toHaveBeenCalledTimes(1); + const [appliedSettings, saveImmediately] = core.services.setting.applyPartial.mock.calls[0]; + expect(saveImmediately).toBe(true); + expect(appliedSettings.couchDB_URI).toBe("http://127.0.0.1:5984"); + expect(appliedSettings.couchDB_DBNAME).toBe("livesync-test-db"); + expect(appliedSettings.isConfigured).toBe(true); + expect(appliedSettings.useIndexedDBAdapter).toBe(false); + }); + + it("setup rejects encoded URI when passphrase is wrong", async () => { + const core = createCoreMock(); + const setupURI = await createSetupURI("correct-passphrase"); + vi.spyOn(commandUtils, "promptForPassphrase").mockResolvedValue("wrong-passphrase"); + + await expect( + runCommand(makeOptions("setup", [setupURI]), { + ...context, + core, + }) + ).rejects.toThrow(); + + expect(core.services.setting.applyPartial).not.toHaveBeenCalled(); + expect(core.services.control.applySettings).not.toHaveBeenCalled(); + }); +}); diff --git a/src/apps/cli/commands/utils.ts b/src/apps/cli/commands/utils.ts index d085822..f56940f 100644 --- a/src/apps/cli/commands/utils.ts +++ b/src/apps/cli/commands/utils.ts @@ -8,7 +8,13 @@ export function toArrayBuffer(data: Buffer): ArrayBuffer { export function toVaultRelativePath(inputPath: string, vaultPath: string): string { const stripped = inputPath.replace(/^[/\\]+/, ""); if (!path.isAbsolute(inputPath)) { - return stripped.replace(/\\/g, "/"); + const normalized = stripped.replace(/\\/g, "/"); + const resolved = path.resolve(vaultPath, normalized); + const rel = path.relative(vaultPath, resolved); + if (rel.startsWith("..") || path.isAbsolute(rel)) { + throw new Error(`Path ${inputPath} is outside of the local database directory`); + } + return rel.replace(/\\/g, "/"); } const resolved = path.resolve(inputPath); const rel = path.relative(vaultPath, resolved); diff --git a/src/apps/cli/commands/utils.unit.spec.ts b/src/apps/cli/commands/utils.unit.spec.ts new file mode 100644 index 0000000..e209bf7 --- /dev/null +++ b/src/apps/cli/commands/utils.unit.spec.ts @@ -0,0 +1,29 @@ +import * as path from "path"; +import { describe, expect, it } from "vitest"; +import { toVaultRelativePath } from "./utils"; + +describe("toVaultRelativePath", () => { + const vaultPath = path.resolve("/tmp/livesync-vault"); + + it("rejects absolute paths outside vault", () => { + expect(() => toVaultRelativePath("/etc/passwd", vaultPath)).toThrow("outside of the local database directory"); + }); + + it("normalizes leading slash for absolute path inside vault", () => { + const absoluteInsideVault = path.join(vaultPath, "notes", "foo.md"); + expect(toVaultRelativePath(absoluteInsideVault, vaultPath)).toBe("notes/foo.md"); + }); + + it("normalizes Windows-style separators", () => { + expect(toVaultRelativePath("notes\\daily\\2026-03-12.md", vaultPath)).toBe("notes/daily/2026-03-12.md"); + }); + + it("returns vault-relative path for another absolute path inside vault", () => { + const absoluteInsideVault = path.join(vaultPath, "docs", "inside.md"); + expect(toVaultRelativePath(absoluteInsideVault, vaultPath)).toBe("docs/inside.md"); + }); + + it("rejects relative path traversal that escapes vault", () => { + expect(() => toVaultRelativePath("../escape.md", vaultPath)).toThrow("outside of the local database directory"); + }); +}); diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 64b3578..58817b3 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -22,6 +22,7 @@ if (!("localStorage" in globalThis)) { import * as fs from "fs/promises"; import * as path from "path"; +import { pathToFileURL } from "node:url"; import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub"; import { LiveSyncBaseCore } from "../../LiveSyncBaseCore"; import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules"; @@ -93,7 +94,7 @@ Examples: `); } -function parseArgs(): CLIOptions { +export function parseArgs(): CLIOptions { const args = process.argv.slice(2); if (args.length === 0 || args.includes("--help") || args.includes("-h")) { @@ -157,6 +158,11 @@ function parseArgs(): CLIOptions { process.exit(1); } + if (command === "daemon" && commandArgs.length > 0) { + console.error(`Error: Unknown command '${commandArgs[0]}'`); + process.exit(1); + } + return { databasePath, settingsPath, @@ -323,7 +329,7 @@ async function main() { console.error(`[Error] Failed to initialize LiveSync`); process.exit(1); } - + await core.services.setting.suspendAllSync(); await core.services.control.onReady(); infoLog(`[Ready] LiveSync is running`); @@ -368,8 +374,20 @@ async function main() { } } -// Run main -main().catch((error) => { - console.error(`[Fatal Error]`, error); - process.exit(1); -}); +// Run main only when invoked as the entrypoint, not when imported by tests. +const isEntryPoint = (() => { + const argv1 = process.argv[1]; + if (!argv1) return false; + try { + return import.meta.url === pathToFileURL(argv1).href; + } catch { + return false; + } +})(); + +if (isEntryPoint) { + main().catch((error) => { + console.error(`[Fatal Error]`, error); + process.exit(1); + }); +} diff --git a/src/apps/cli/main.unit.spec.ts b/src/apps/cli/main.unit.spec.ts new file mode 100644 index 0000000..032a2b4 --- /dev/null +++ b/src/apps/cli/main.unit.spec.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { parseArgs } from "./main"; + +function mockProcessExit() { + const exitMock = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`__EXIT__:${code ?? 0}`); + }) as any); + return exitMock; +} + +describe("CLI parseArgs", () => { + const originalArgv = process.argv.slice(); + + afterEach(() => { + process.argv = originalArgv.slice(); + vi.restoreAllMocks(); + }); + + it("exits 1 when --settings has no value", () => { + process.argv = ["node", "livesync-cli", "./vault", "--settings"]; + const exitMock = mockProcessExit(); + const stderr = vi.spyOn(console, "error").mockImplementation(() => {}); + + expect(() => parseArgs()).toThrowError("__EXIT__:1"); + expect(exitMock).toHaveBeenCalledWith(1); + expect(stderr).toHaveBeenCalledWith("Error: Missing value for --settings"); + }); + + it("exits 1 when database-path is missing", () => { + process.argv = ["node", "livesync-cli", "sync"]; + const exitMock = mockProcessExit(); + const stderr = vi.spyOn(console, "error").mockImplementation(() => {}); + + expect(() => parseArgs()).toThrowError("__EXIT__:1"); + expect(exitMock).toHaveBeenCalledWith(1); + expect(stderr).toHaveBeenCalledWith("Error: database-path is required"); + }); + + it("exits 1 for unknown command after database-path", () => { + process.argv = ["node", "livesync-cli", "./vault", "unknown-cmd"]; + const exitMock = mockProcessExit(); + const stderr = vi.spyOn(console, "error").mockImplementation(() => {}); + + expect(() => parseArgs()).toThrowError("__EXIT__:1"); + expect(exitMock).toHaveBeenCalledWith(1); + expect(stderr).toHaveBeenCalledWith("Error: Unknown command 'unknown-cmd'"); + }); + + it("exits 0 and prints help for --help", () => { + process.argv = ["node", "livesync-cli", "--help"]; + const exitMock = mockProcessExit(); + const stdout = vi.spyOn(console, "log").mockImplementation(() => {}); + + expect(() => parseArgs()).toThrowError("__EXIT__:0"); + expect(exitMock).toHaveBeenCalledWith(0); + expect(stdout).toHaveBeenCalled(); + const combined = stdout.mock.calls.flat().join("\n"); + expect(combined).toContain("Usage:"); + expect(combined).toContain("livesync-cli [database-path]"); + }); +}); diff --git a/src/apps/cli/package.json b/src/apps/cli/package.json index 327e4fe..611f54d 100644 --- a/src/apps/cli/package.json +++ b/src/apps/cli/package.json @@ -11,7 +11,14 @@ "cli": "node dist/index.cjs", "buildRun": "npm run build && npm run cli --", "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json", - "test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh" + "test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts", + "test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh", + "test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh", + "test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh", + "test:e2e:push-pull": "bash test/test-push-pull-linux.sh", + "test:e2e:setup-put-cat": "bash test/test-setup-put-cat-linux.sh", + "test:e2e:sync-two-local": "bash test/test-sync-two-local-databases-linux.sh", + "test:e2e:all": "npm run test:e2e:two-vaults && npm run test:e2e:push-pull && npm run test:e2e:setup-put-cat && npm run test:e2e:sync-two-local" }, "dependencies": {}, "devDependencies": {} diff --git a/src/apps/cli/test/test-e2e-two-vaults-common.sh b/src/apps/cli/test/test-e2e-two-vaults-common.sh new file mode 100755 index 0000000..03a5400 --- /dev/null +++ b/src/apps/cli/test/test-e2e-two-vaults-common.sh @@ -0,0 +1,445 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$CLI_DIR" + +CLI_CMD=(npm --silent run cli -- -v) +RUN_BUILD="${RUN_BUILD:-1}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" +TEST_ENV_FILE="${TEST_ENV_FILE:-$CLI_DIR/.test.env}" +REMOTE_TYPE="${REMOTE_TYPE:-COUCHDB}" +ENCRYPT="${ENCRYPT:-0}" +TEST_LABEL="${TEST_LABEL:-${REMOTE_TYPE}-enc${ENCRYPT}}" +E2E_PASSPHRASE="${E2E_PASSPHRASE:-e2e-passphrase}" + +if [[ ! -f "$TEST_ENV_FILE" ]]; then + echo "[ERROR] test env file not found: $TEST_ENV_FILE" >&2 + exit 1 +fi + +set -a +source "$TEST_ENV_FILE" +set +a + +DB_SUFFIX="$(date +%s)-$RANDOM" + +VAULT_ROOT="$CLI_DIR/.livesync" +VAULT_A="$VAULT_ROOT/testvault_a" +VAULT_B="$VAULT_ROOT/testvault_b" +SETTINGS_A="$VAULT_ROOT/test-settings-a.json" +SETTINGS_B="$VAULT_ROOT/test-settings-b.json" +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-e2e.${TEST_LABEL}.XXXXXX")" + +COUCHDB_URI="" +COUCHDB_DBNAME="" +MINIO_BUCKET="" + +require_env() { + local var_name="$1" + if [[ -z "${!var_name:-}" ]]; then + echo "[ERROR] required variable '$var_name' is missing in $TEST_ENV_FILE" >&2 + exit 1 + fi +} + +if [[ "$REMOTE_TYPE" == "COUCHDB" ]]; then + require_env hostname + require_env dbname + require_env username + require_env password + COUCHDB_URI="${hostname%/}" + COUCHDB_DBNAME="${dbname}-${DB_SUFFIX}" +elif [[ "$REMOTE_TYPE" == "MINIO" ]]; then + require_env accessKey + require_env secretKey + require_env minioEndpoint + require_env bucketName + MINIO_BUCKET="${bucketName}-${DB_SUFFIX}" +else + echo "[ERROR] unsupported REMOTE_TYPE: $REMOTE_TYPE (use COUCHDB or MINIO)" >&2 + exit 1 +fi + +cleanup() { + local exit_code=$? + if [[ "$REMOTE_TYPE" == "COUCHDB" ]]; then + bash "$CLI_DIR/util/couchdb-stop.sh" >/dev/null 2>&1 || true + else + bash "$CLI_DIR/util/minio-stop.sh" >/dev/null 2>&1 || true + fi + + if [[ "$KEEP_TEST_DATA" != "1" ]]; then + rm -rf "$VAULT_A" "$VAULT_B" "$SETTINGS_A" "$SETTINGS_B" "$WORK_DIR" + else + echo "[INFO] KEEP_TEST_DATA=1, preserving test artefacts" + echo " vault a: $VAULT_A" + echo " vault b: $VAULT_B" + echo " settings: $SETTINGS_A, $SETTINGS_B" + echo " work dir: $WORK_DIR" + fi + exit "$exit_code" +} +trap cleanup EXIT + +run_cli() { + "${CLI_CMD[@]}" "$@" +} + +run_cli_a() { + run_cli "$VAULT_A" --settings "$SETTINGS_A" "$@" +} + +run_cli_b() { + run_cli "$VAULT_B" --settings "$SETTINGS_B" "$@" +} + +assert_contains() { + local haystack="$1" + local needle="$2" + local message="$3" + if ! grep -Fq "$needle" <<< "$haystack"; then + echo "[FAIL] $message" >&2 + echo "[FAIL] expected to find: $needle" >&2 + echo "[FAIL] actual output:" >&2 + echo "$haystack" >&2 + exit 1 + fi +} + +assert_equal() { + local expected="$1" + local actual="$2" + local message="$3" + if [[ "$expected" != "$actual" ]]; then + echo "[FAIL] $message" >&2 + echo "[FAIL] expected: $expected" >&2 + echo "[FAIL] actual: $actual" >&2 + exit 1 + fi +} + +assert_command_fails() { + local message="$1" + shift + set +e + "$@" >"$WORK_DIR/failed-command.log" 2>&1 + local exit_code=$? + set -e + if [[ "$exit_code" -eq 0 ]]; then + echo "[FAIL] $message" >&2 + cat "$WORK_DIR/failed-command.log" >&2 + exit 1 + fi +} + +sanitise_cat_stdout() { + sed '/^\[CLIWatchAdapter\] File watching is not enabled in CLI version$/d' +} + +extract_json_string_field() { + local field_name="$1" + node -e ' +const fs = require("node:fs"); +const fieldName = process.argv[1]; +const data = JSON.parse(fs.readFileSync(0, "utf-8")); +const value = data[fieldName]; +if (typeof value === "string") { + process.stdout.write(value); +} +' "$field_name" +} + +sync_both() { + run_cli_a sync >/dev/null + run_cli_b sync >/dev/null +} + +curl_json() { + curl -4 -sS --fail --connect-timeout 3 --max-time 15 "$@" +} + +configure_remote_settings() { + local settings_file="$1" + SETTINGS_FILE="$settings_file" \ + REMOTE_TYPE="$REMOTE_TYPE" \ + COUCHDB_URI="$COUCHDB_URI" \ + COUCHDB_USER="${username:-}" \ + COUCHDB_PASSWORD="${password:-}" \ + COUCHDB_DBNAME="$COUCHDB_DBNAME" \ + MINIO_ENDPOINT="${minioEndpoint:-}" \ + MINIO_BUCKET="$MINIO_BUCKET" \ + MINIO_ACCESS_KEY="${accessKey:-}" \ + MINIO_SECRET_KEY="${secretKey:-}" \ + ENCRYPT="$ENCRYPT" \ + E2E_PASSPHRASE="$E2E_PASSPHRASE" \ + node <<'NODE' +const fs = require("node:fs"); +const settingsPath = process.env.SETTINGS_FILE; +const data = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); + +const remoteType = process.env.REMOTE_TYPE; +if (remoteType === "COUCHDB") { + data.remoteType = ""; + data.couchDB_URI = process.env.COUCHDB_URI; + data.couchDB_USER = process.env.COUCHDB_USER; + data.couchDB_PASSWORD = process.env.COUCHDB_PASSWORD; + data.couchDB_DBNAME = process.env.COUCHDB_DBNAME; +} else if (remoteType === "MINIO") { + data.remoteType = "MINIO"; + data.bucket = process.env.MINIO_BUCKET; + data.endpoint = process.env.MINIO_ENDPOINT; + data.accessKey = process.env.MINIO_ACCESS_KEY; + data.secretKey = process.env.MINIO_SECRET_KEY; + data.region = "auto"; + data.forcePathStyle = true; +} + +data.liveSync = true; +data.syncOnStart = false; +data.syncOnSave = false; +data.usePluginSync = false; + +data.encrypt = process.env.ENCRYPT === "1"; +data.passphrase = data.encrypt ? process.env.E2E_PASSPHRASE : ""; + +data.isConfigured = true; + +fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8"); +NODE +} + +init_settings() { + local settings_file="$1" + run_cli init-settings --force "$settings_file" >/dev/null + configure_remote_settings "$settings_file" + cat "$settings_file" +} + +wait_for_minio_bucket() { + local retries=30 + local delay_sec=2 + local i + for ((i = 1; i <= retries; i++)); do + if docker run --rm --network host --entrypoint=/bin/sh minio/mc -c "mc alias set myminio $minioEndpoint $accessKey $secretKey >/dev/null 2>&1 && mc ls myminio/$MINIO_BUCKET >/dev/null 2>&1"; then + return 0 + fi + bucketName="$MINIO_BUCKET" bash "$CLI_DIR/util/minio-init.sh" >/dev/null 2>&1 || true + sleep "$delay_sec" + done + return 1 +} + +start_remote() { + if [[ "$REMOTE_TYPE" == "COUCHDB" ]]; then + echo "[INFO] stopping leftover CouchDB container if present" + bash "$CLI_DIR/util/couchdb-stop.sh" >/dev/null 2>&1 || true + + echo "[INFO] starting CouchDB test container" + bash "$CLI_DIR/util/couchdb-start.sh" + + echo "[INFO] initialising CouchDB test container" + bash "$CLI_DIR/util/couchdb-init.sh" + + echo "[INFO] CouchDB create test database: $COUCHDB_DBNAME" + until (curl_json -X PUT --user "${username}:${password}" "${hostname}/${COUCHDB_DBNAME}"); do sleep 5; done + else + echo "[INFO] stopping leftover MinIO container if present" + bash "$CLI_DIR/util/minio-stop.sh" >/dev/null 2>&1 || true + + echo "[INFO] starting MinIO test container" + bucketName="$MINIO_BUCKET" bash "$CLI_DIR/util/minio-start.sh" + + echo "[INFO] initialising MinIO test bucket: $MINIO_BUCKET" + local minio_init_ok=0 + for _ in 1 2 3 4 5; do + if bucketName="$MINIO_BUCKET" bash "$CLI_DIR/util/minio-init.sh"; then + minio_init_ok=1 + break + fi + sleep 2 + done + if [[ "$minio_init_ok" != "1" ]]; then + echo "[FAIL] could not initialise MinIO bucket after retries: $MINIO_BUCKET" >&2 + exit 1 + fi + if ! wait_for_minio_bucket; then + echo "[FAIL] MinIO bucket not ready: $MINIO_BUCKET" >&2 + exit 1 + fi + fi +} + +if [[ "$RUN_BUILD" == "1" ]]; then + echo "[INFO] building CLI" + npm run build +fi + +echo "[INFO] e2e case: remote=$REMOTE_TYPE encrypt=$ENCRYPT label=$TEST_LABEL" +start_remote + +echo "[INFO] preparing vaults and settings" +rm -rf "$VAULT_A" "$VAULT_B" "$SETTINGS_A" "$SETTINGS_B" +mkdir -p "$VAULT_A" "$VAULT_B" +init_settings "$SETTINGS_A" +init_settings "$SETTINGS_B" + +if [[ "$REMOTE_TYPE" == "COUCHDB" ]]; then + echo "[INFO] test remote DB: $COUCHDB_DBNAME" +else + echo "[INFO] test remote bucket: $MINIO_BUCKET" +fi + +TARGET_A_ONLY="e2e/a-only-info.md" +TARGET_SYNC="e2e/sync-info.md" +TARGET_PUSH="e2e/pushed-from-a.md" +TARGET_PUT="e2e/put-from-a.md" +TARGET_CONFLICT="e2e/conflict.md" + +echo "[CASE] A puts and A can get info" +printf 'alpha-from-a\n' | run_cli_a put "$TARGET_A_ONLY" >/dev/null +INFO_A_ONLY="$(run_cli_a info "$TARGET_A_ONLY")" +assert_contains "$INFO_A_ONLY" "\"path\": \"$TARGET_A_ONLY\"" "A info should include path after put" +echo "[PASS] A put/info" + +echo "[CASE] A puts, both sync, and B can get info" +printf 'visible-after-sync\n' | run_cli_a put "$TARGET_SYNC" >/dev/null +sync_both +INFO_B_SYNC="$(run_cli_b info "$TARGET_SYNC")" +assert_contains "$INFO_B_SYNC" "\"path\": \"$TARGET_SYNC\"" "B info should include path after sync" +echo "[PASS] sync A->B and B info" + +echo "[CASE] A pushes and puts, both sync, and B can pull and cat" +PUSH_SRC="$WORK_DIR/push-source.txt" +PULL_DST="$WORK_DIR/pull-destination.txt" +printf 'pushed-content-%s\n' "$DB_SUFFIX" > "$PUSH_SRC" +run_cli_a push "$PUSH_SRC" "$TARGET_PUSH" >/dev/null +printf 'put-content-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_PUT" >/dev/null +sync_both +run_cli_b pull "$TARGET_PUSH" "$PULL_DST" >/dev/null +if ! cmp -s "$PUSH_SRC" "$PULL_DST"; then + echo "[FAIL] B pull result does not match pushed source" >&2 + echo "--- source ---" >&2 + cat "$PUSH_SRC" >&2 + echo "--- pulled ---" >&2 + cat "$PULL_DST" >&2 + exit 1 +fi +CAT_B_PUT="$(run_cli_b cat "$TARGET_PUT" | sanitise_cat_stdout)" +assert_equal "put-content-$DB_SUFFIX" "$CAT_B_PUT" "B cat should return A put content" +echo "[PASS] push/pull and put/cat across vaults" + +echo "[CASE] A removes, both sync, and B can no longer cat" +run_cli_a rm "$TARGET_PUT" >/dev/null +sync_both +assert_command_fails "B cat should fail after A removed the file and synced" run_cli_b cat "$TARGET_PUT" +echo "[PASS] rm is replicated" + +echo "[CASE] verify conflict detection" +printf 'conflict-base\n' | run_cli_a put "$TARGET_CONFLICT" >/dev/null +sync_both +INFO_B_BASE="$(run_cli_b info "$TARGET_CONFLICT")" +assert_contains "$INFO_B_BASE" "\"path\": \"$TARGET_CONFLICT\"" "B should be able to info before creating conflict" + +printf 'conflict-from-a-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_CONFLICT" >/dev/null +printf 'conflict-from-b-%s\n' "$DB_SUFFIX" | run_cli_b put "$TARGET_CONFLICT" >/dev/null + +run_cli_a sync >/dev/null +run_cli_b sync >/dev/null +run_cli_a sync >/dev/null + +INFO_A_CONFLICT="$(run_cli_a info "$TARGET_CONFLICT")" +INFO_B_CONFLICT="$(run_cli_b info "$TARGET_CONFLICT")" +if grep -qF '"conflicts": "N/A"' <<< "$INFO_A_CONFLICT" && grep -qF '"conflicts": "N/A"' <<< "$INFO_B_CONFLICT"; then + echo "[FAIL] conflict was expected but both A and B show Conflicts: N/A" >&2 + echo "--- A info ---" >&2 + echo "$INFO_A_CONFLICT" >&2 + echo "--- B info ---" >&2 + echo "$INFO_B_CONFLICT" >&2 + exit 1 +fi +echo "[PASS] conflict detected by info" + +echo "[CASE] verify ls marks conflicted revisions" +LS_A_CONFLICT_LINE="$(run_cli_a ls "$TARGET_CONFLICT" | awk -F $'\t' -v p="$TARGET_CONFLICT" '$1==p {print; exit}')" +LS_B_CONFLICT_LINE="$(run_cli_b ls "$TARGET_CONFLICT" | awk -F $'\t' -v p="$TARGET_CONFLICT" '$1==p {print; exit}')" +if [[ -z "$LS_A_CONFLICT_LINE" || -z "$LS_B_CONFLICT_LINE" ]]; then + echo "[FAIL] ls output did not include conflict target on one of the vaults" >&2 + echo "--- A ls ---" >&2 + run_cli_a ls "$TARGET_CONFLICT" >&2 || true + echo "--- B ls ---" >&2 + run_cli_b ls "$TARGET_CONFLICT" >&2 || true + exit 1 +fi +LS_A_CONFLICT_REV="$(awk -F $'\t' '{print $4}' <<< "$LS_A_CONFLICT_LINE")" +LS_B_CONFLICT_REV="$(awk -F $'\t' '{print $4}' <<< "$LS_B_CONFLICT_LINE")" +if [[ "$LS_A_CONFLICT_REV" != *"*" && "$LS_B_CONFLICT_REV" != *"*" ]]; then + echo "[FAIL] conflicted entry should be marked with '*' in ls revision column on at least one vault" >&2 + echo "A: $LS_A_CONFLICT_LINE" >&2 + echo "B: $LS_B_CONFLICT_LINE" >&2 + exit 1 +fi +echo "[PASS] ls marks conflicts" + +echo "[CASE] resolve conflict on A and verify both vaults are clean" +KEEP_REVISION="$(printf '%s' "$INFO_A_CONFLICT" | extract_json_string_field revision)" +if [[ -z "$KEEP_REVISION" ]]; then + echo "[FAIL] could not extract current revision from A info output" >&2 + echo "$INFO_A_CONFLICT" >&2 + exit 1 +fi + +run_cli_a resolve "$TARGET_CONFLICT" "$KEEP_REVISION" >/dev/null + +INFO_A_RESOLVED="" +INFO_B_RESOLVED="" +RESOLVE_PROPAGATED=0 +for _ in 1 2 3 4 5; do + sync_both + INFO_A_RESOLVED="$(run_cli_a info "$TARGET_CONFLICT")" + INFO_B_RESOLVED="$(run_cli_b info "$TARGET_CONFLICT")" + if grep -qF '"conflicts": "N/A"' <<< "$INFO_A_RESOLVED" && grep -qF '"conflicts": "N/A"' <<< "$INFO_B_RESOLVED"; then + RESOLVE_PROPAGATED=1 + break + fi +done +if [[ "$RESOLVE_PROPAGATED" != "1" ]]; then + KEEP_REVISION_B="$(printf '%s' "$INFO_B_RESOLVED" | extract_json_string_field revision)" + if [[ -n "$KEEP_REVISION_B" ]]; then + run_cli_b resolve "$TARGET_CONFLICT" "$KEEP_REVISION_B" >/dev/null + sync_both + INFO_A_RESOLVED="$(run_cli_a info "$TARGET_CONFLICT")" + INFO_B_RESOLVED="$(run_cli_b info "$TARGET_CONFLICT")" + if grep -qF '"conflicts": "N/A"' <<< "$INFO_A_RESOLVED" && grep -qF '"conflicts": "N/A"' <<< "$INFO_B_RESOLVED"; then + RESOLVE_PROPAGATED=1 + fi + fi +fi + +if [[ "$RESOLVE_PROPAGATED" != "1" ]]; then + echo "[FAIL] conflicts should be resolved on both vaults" >&2 + echo "--- A info after resolve ---" >&2 + echo "$INFO_A_RESOLVED" >&2 + echo "--- B info after resolve ---" >&2 + echo "$INFO_B_RESOLVED" >&2 + exit 1 +fi + +LS_A_RESOLVED_LINE="$(run_cli_a ls "$TARGET_CONFLICT" | awk -F $'\t' -v p="$TARGET_CONFLICT" '$1==p {print; exit}')" +LS_B_RESOLVED_LINE="$(run_cli_b ls "$TARGET_CONFLICT" | awk -F $'\t' -v p="$TARGET_CONFLICT" '$1==p {print; exit}')" +LS_A_RESOLVED_REV="$(awk -F $'\t' '{print $4}' <<< "$LS_A_RESOLVED_LINE")" +LS_B_RESOLVED_REV="$(awk -F $'\t' '{print $4}' <<< "$LS_B_RESOLVED_LINE")" +if [[ "$LS_A_RESOLVED_REV" == *"*" || "$LS_B_RESOLVED_REV" == *"*" ]]; then + echo "[FAIL] resolved entry should not be marked as conflicted in ls" >&2 + echo "A: $LS_A_RESOLVED_LINE" >&2 + echo "B: $LS_B_RESOLVED_LINE" >&2 + exit 1 +fi + +CAT_A_RESOLVED="$(run_cli_a cat "$TARGET_CONFLICT" | sanitise_cat_stdout)" +CAT_B_RESOLVED="$(run_cli_b cat "$TARGET_CONFLICT" | sanitise_cat_stdout)" +assert_equal "$CAT_A_RESOLVED" "$CAT_B_RESOLVED" "resolved content should match across both vaults" +echo "[PASS] resolve is replicated and ls reflects resolved state" + +echo "[PASS] all requested E2E scenarios completed (${TEST_LABEL})" diff --git a/src/apps/cli/test/test-e2e-two-vaults-matrix.sh b/src/apps/cli/test/test-e2e-two-vaults-matrix.sh new file mode 100755 index 0000000..843e60f --- /dev/null +++ b/src/apps/cli/test/test-e2e-two-vaults-matrix.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +RUN_BUILD="${RUN_BUILD:-1}" +KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" +TEST_ENV_FILE="${TEST_ENV_FILE:-$(cd -- "$SCRIPT_DIR/.." && pwd)/.test.env}" + +run_case() { + local remote_type="$1" + local encrypt="$2" + local label="${remote_type}-enc${encrypt}" + + echo "[INFO] ===== CASE START: $label =====" + REMOTE_TYPE="$remote_type" \ + ENCRYPT="$encrypt" \ + RUN_BUILD="$RUN_BUILD" \ + KEEP_TEST_DATA="$KEEP_TEST_DATA" \ + TEST_ENV_FILE="$TEST_ENV_FILE" \ + TEST_LABEL="$label" \ + bash "$SCRIPT_DIR/test-e2e-two-vaults-common.sh" + echo "[INFO] ===== CASE PASS: $label =====" +} + +run_case COUCHDB 0 +run_case COUCHDB 1 +run_case MINIO 0 +run_case MINIO 1 + +echo "[PASS] all matrix cases completed" diff --git a/src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh b/src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh old mode 100644 new mode 100755 index b23b00c..9edf76f --- a/src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh +++ b/src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh @@ -2,246 +2,8 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -CLI_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" -cd "$CLI_DIR" -# verbose -CLI_CMD=(npm run cli -- -v ) -RUN_BUILD="${RUN_BUILD:-1}" -KEEP_TEST_DATA="${KEEP_TEST_DATA:-0}" -TEST_ENV_FILE="${TEST_ENV_FILE:-$CLI_DIR/.test.env}" - -if [[ ! -f "$TEST_ENV_FILE" ]]; then - echo "[ERROR] test env file not found: $TEST_ENV_FILE" >&2 - exit 1 -fi - -set -a -source "$TEST_ENV_FILE" -set +a - -for var in hostname dbname username password; do - if [[ -z "${!var:-}" ]]; then - echo "[ERROR] required variable '$var' is missing in $TEST_ENV_FILE" >&2 - exit 1 - fi -done - -COUCHDB_URI="${hostname%/}" -DB_SUFFIX="$(date +%s)-$RANDOM" -COUCHDB_DBNAME="${dbname}-${DB_SUFFIX}" - -VAULT_ROOT="$CLI_DIR/.livesync" -VAULT_A="$VAULT_ROOT/testvault_a" -VAULT_B="$VAULT_ROOT/testvault_b" -SETTINGS_A="$VAULT_ROOT/test-settings-a.json" -SETTINGS_B="$VAULT_ROOT/test-settings-b.json" -WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-cli-e2e.XXXXXX")" - -cleanup() { - local exit_code=$? - bash "$CLI_DIR/util/couchdb-stop.sh" >/dev/null 2>&1 || true - if [[ "$KEEP_TEST_DATA" != "1" ]]; then - rm -rf "$VAULT_A" "$VAULT_B" "$SETTINGS_A" "$SETTINGS_B" "$WORK_DIR" - else - echo "[INFO] KEEP_TEST_DATA=1, preserving test artefacts" - echo " vault a: $VAULT_A" - echo " vault b: $VAULT_B" - echo " settings: $SETTINGS_A, $SETTINGS_B" - echo " work dir: $WORK_DIR" - fi - exit "$exit_code" -} -trap cleanup EXIT - -run_cli() { - "${CLI_CMD[@]}" "$@" -} - -run_cli_a() { - run_cli "$VAULT_A" --settings "$SETTINGS_A" "$@" -} - -run_cli_b() { - run_cli "$VAULT_B" --settings "$SETTINGS_B" "$@" -} - -assert_contains() { - local haystack="$1" - local needle="$2" - local message="$3" - if ! grep -Fq "$needle" <<< "$haystack"; then - echo "[FAIL] $message" >&2 - echo "[FAIL] expected to find: $needle" >&2 - echo "[FAIL] actual output:" >&2 - echo "$haystack" >&2 - exit 1 - fi -} - -assert_equal() { - local expected="$1" - local actual="$2" - local message="$3" - if [[ "$expected" != "$actual" ]]; then - echo "[FAIL] $message" >&2 - echo "[FAIL] expected: $expected" >&2 - echo "[FAIL] actual: $actual" >&2 - exit 1 - fi -} - -assert_command_fails() { - local message="$1" - shift - set +e - "$@" >"$WORK_DIR/failed-command.log" 2>&1 - local exit_code=$? - set -e - if [[ "$exit_code" -eq 0 ]]; then - echo "[FAIL] $message" >&2 - cat "$WORK_DIR/failed-command.log" >&2 - exit 1 - fi -} - -sanitise_cat_stdout() { - sed '/^\[CLIWatchAdapter\] File watching is not enabled in CLI version$/d' -} - -sync_both() { - run_cli_a sync >/dev/null - run_cli_b sync >/dev/null -} - -curl_json() { - curl -4 -sS --fail --connect-timeout 3 --max-time 15 "$@" -} - -init_settings() { - local settings_file="$1" - run_cli init-settings --force "$settings_file" >/dev/null - SETTINGS_FILE="$settings_file" \ - COUCHDB_URI="$COUCHDB_URI" \ - COUCHDB_USER="$username" \ - COUCHDB_PASSWORD="$password" \ - COUCHDB_DBNAME="$COUCHDB_DBNAME" \ - node <<'NODE' -const fs = require("node:fs"); -const settingsPath = process.env.SETTINGS_FILE; -const data = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); - -data.couchDB_URI = process.env.COUCHDB_URI; -data.couchDB_USER = process.env.COUCHDB_USER; -data.couchDB_PASSWORD = process.env.COUCHDB_PASSWORD; -data.couchDB_DBNAME = process.env.COUCHDB_DBNAME; -data.liveSync = true; -data.syncOnStart = false; -data.syncOnSave = false; -data.usePluginSync = false; -data.isConfigured = true; - -fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8"); -NODE -cat "$settings_file" -} - -echo "[INFO] stopping leftover CouchDB container if present" -bash "$CLI_DIR/util/couchdb-stop.sh" >/dev/null 2>&1 || true - -echo "[INFO] starting CouchDB test container" -bash "$CLI_DIR/util/couchdb-start.sh" - -echo "status" -docker ps --filter "name=couchdb-test" - -echo "[INFO] initialising CouchDB test container" -bash "$CLI_DIR/util/couchdb-init.sh" - -echo "[INFO] CouchDB create test database: $COUCHDB_DBNAME" -until (curl_json -X PUT --user "${username}:${password}" "${hostname}/${COUCHDB_DBNAME}" ); do sleep 5; done - -if [[ "$RUN_BUILD" == "1" ]]; then - echo "[INFO] building CLI" - npm run build -fi - -echo "[INFO] preparing vaults and settings" -rm -rf "$VAULT_A" "$VAULT_B" "$SETTINGS_A" "$SETTINGS_B" -mkdir -p "$VAULT_A" "$VAULT_B" -init_settings "$SETTINGS_A" -init_settings "$SETTINGS_B" - -echo "[INFO] test DB: $COUCHDB_DBNAME" - -TARGET_A_ONLY="e2e/a-only-info.md" -TARGET_SYNC="e2e/sync-info.md" -TARGET_PUSH="e2e/pushed-from-a.md" -TARGET_PUT="e2e/put-from-a.md" -TARGET_CONFLICT="e2e/conflict.md" - -echo "[CASE] A puts and A can get info" -printf 'alpha-from-a\n' | run_cli_a put "$TARGET_A_ONLY" >/dev/null -INFO_A_ONLY="$(run_cli_a info "$TARGET_A_ONLY")" -assert_contains "$INFO_A_ONLY" "\"path\": \"$TARGET_A_ONLY\"" "A info should include path after put" -echo "[PASS] A put/info" - -echo "[CASE] A puts, both sync, and B can get info" -printf 'visible-after-sync\n' | run_cli_a put "$TARGET_SYNC" >/dev/null -sync_both -INFO_B_SYNC="$(run_cli_b info "$TARGET_SYNC")" -assert_contains "$INFO_B_SYNC" "\"path\": \"$TARGET_SYNC\"" "B info should include path after sync" -echo "[PASS] sync A->B and B info" - -echo "[CASE] A pushes and puts, both sync, and B can pull and cat" -PUSH_SRC="$WORK_DIR/push-source.txt" -PULL_DST="$WORK_DIR/pull-destination.txt" -printf 'pushed-content-%s\n' "$DB_SUFFIX" > "$PUSH_SRC" -run_cli_a push "$PUSH_SRC" "$TARGET_PUSH" >/dev/null -printf 'put-content-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_PUT" >/dev/null -sync_both -run_cli_b pull "$TARGET_PUSH" "$PULL_DST" >/dev/null -if ! cmp -s "$PUSH_SRC" "$PULL_DST"; then - echo "[FAIL] B pull result does not match pushed source" >&2 - echo "--- source ---" >&2 - cat "$PUSH_SRC" >&2 - echo "--- pulled ---" >&2 - cat "$PULL_DST" >&2 - exit 1 -fi -CAT_B_PUT="$(run_cli_b cat "$TARGET_PUT" | sanitise_cat_stdout)" -assert_equal "put-content-$DB_SUFFIX" "$CAT_B_PUT" "B cat should return A put content" -echo "[PASS] push/pull and put/cat across vaults" - -echo "[CASE] A removes, both sync, and B can no longer cat" -run_cli_a rm "$TARGET_PUT" >/dev/null -sync_both -assert_command_fails "B cat should fail after A removed the file and synced" run_cli_b cat "$TARGET_PUT" -echo "[PASS] rm is replicated" - -echo "[CASE] verify conflict detection" -printf 'conflict-base\n' | run_cli_a put "$TARGET_CONFLICT" >/dev/null -sync_both -INFO_B_BASE="$(run_cli_b info "$TARGET_CONFLICT")" -assert_contains "$INFO_B_BASE" "\"path\": \"$TARGET_CONFLICT\"" "B should be able to info before creating conflict" - -printf 'conflict-from-a-%s\n' "$DB_SUFFIX" | run_cli_a put "$TARGET_CONFLICT" >/dev/null -printf 'conflict-from-b-%s\n' "$DB_SUFFIX" | run_cli_b put "$TARGET_CONFLICT" >/dev/null - -run_cli_a sync >/dev/null -run_cli_b sync >/dev/null -run_cli_a sync >/dev/null - -INFO_A_CONFLICT="$(run_cli_a info "$TARGET_CONFLICT")" -INFO_B_CONFLICT="$(run_cli_b info "$TARGET_CONFLICT")" -if grep -qF '"conflicts": "N/A"' <<< "$INFO_A_CONFLICT" && grep -qF '"conflicts": "N/A"' <<< "$INFO_B_CONFLICT"; then - echo "[FAIL] conflict was expected but both A and B show Conflicts: N/A" >&2 - echo "--- A info ---" >&2 - echo "$INFO_A_CONFLICT" >&2 - echo "--- B info ---" >&2 - echo "$INFO_B_CONFLICT" >&2 - exit 1 -fi -echo "[PASS] conflict detected by info" - -echo "[PASS] all requested E2E scenarios completed" \ No newline at end of file +REMOTE_TYPE="${REMOTE_TYPE:-COUCHDB}" \ +ENCRYPT="${ENCRYPT:-0}" \ +TEST_LABEL="${TEST_LABEL:-${REMOTE_TYPE}-enc${ENCRYPT}}" \ +bash "$SCRIPT_DIR/test-e2e-two-vaults-common.sh" \ No newline at end of file diff --git a/src/apps/cli/vite.config.ts b/src/apps/cli/vite.config.ts index d54fb44..42e6202 100644 --- a/src/apps/cli/vite.config.ts +++ b/src/apps/cli/vite.config.ts @@ -12,6 +12,14 @@ export default defineConfig({ alias: { "@lib/worker/bgWorker.ts": "../../lib/src/worker/bgWorker.mock.ts", "@lib/pouchdb/pouchdb-browser.ts": path.resolve(__dirname, "lib/pouchdb-node.ts"), + // The CLI runs on Node.js; force AWS XML builder to its CJS Node entry + // so Vite does not resolve the browser DOMParser-based XML parser. + "@aws-sdk/xml-builder": path.resolve( + __dirname, + "../../../node_modules/@aws-sdk/xml-builder/dist-cjs/index.js" + ), + // Force fflate to the Node CJS entry; browser entry expects Web Worker globals. + fflate: path.resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"), "@": path.resolve(__dirname, "../../"), "@lib": path.resolve(__dirname, "../../lib/src"), "../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts", @@ -32,7 +40,8 @@ export default defineConfig({ if (id.startsWith(".") || id.startsWith("/")) return false; if (id.startsWith("@/") || id.startsWith("@lib/")) return false; if (id.endsWith(".ts") || id.endsWith(".js")) return false; - if (id === "fs" || id === "fs/promises" || id === "path" || id === "crypto") return true; + if (id === "fs" || id === "fs/promises" || id === "path" || id === "crypto" || id === "worker_threads") + return true; if (id.startsWith("pouchdb-")) return true; if (id.startsWith("node:")) return true; return false; diff --git a/src/lib b/src/lib index 41e2340..4346ead 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 41e234023530cea101473c5d2f781941b6bbc9e8 +Subproject commit 4346ead9c86574c3f370d458a59ea48b502e6d33