mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-04-05 16:45:20 +00:00
A- Add more tests.
- Object Storage support has also been confirmed (and fixed) in CLI.
This commit is contained in:
86
package-lock.json
generated
86
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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 ?? "",
|
||||
|
||||
204
src/apps/cli/commands/runCommand.unit.spec.ts
Normal file
204
src/apps/cli/commands/runCommand.unit.spec.ts
Normal file
@@ -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<string> {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
29
src/apps/cli/commands/utils.unit.spec.ts
Normal file
29
src/apps/cli/commands/utils.unit.spec.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
61
src/apps/cli/main.unit.spec.ts
Normal file
61
src/apps/cli/main.unit.spec.ts
Normal file
@@ -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]");
|
||||
});
|
||||
});
|
||||
@@ -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": {}
|
||||
|
||||
445
src/apps/cli/test/test-e2e-two-vaults-common.sh
Executable file
445
src/apps/cli/test/test-e2e-two-vaults-common.sh
Executable file
@@ -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})"
|
||||
31
src/apps/cli/test/test-e2e-two-vaults-matrix.sh
Executable file
31
src/apps/cli/test/test-e2e-two-vaults-matrix.sh
Executable file
@@ -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"
|
||||
246
src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh
Normal file → Executable file
246
src/apps/cli/test/test-e2e-two-vaults-with-docker-linux.sh
Normal file → Executable file
@@ -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"
|
||||
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"
|
||||
@@ -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;
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 41e2340235...4346ead9c8
Reference in New Issue
Block a user