mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-19 09:07:06 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8bb05e884 | ||
|
|
c60323e11f |
@@ -7,6 +7,8 @@
|
||||
# 3. DB-deleted file → NOT restored to storage (UPDATE STORAGE skip)
|
||||
# 4. Both, storage newer → DB updated (SYNC: STORAGE → DB)
|
||||
# 5. Both, DB newer → storage updated (SYNC: DB → STORAGE)
|
||||
# 6. Compatibility mode → omitted vault-path works
|
||||
# 7. Unknown local origin → conflict preserved, deduplicated, and resolved
|
||||
#
|
||||
# Not covered (require precise mtime control or artificial conflict injection):
|
||||
# - Both, equal mtime → no-op (EVEN)
|
||||
@@ -43,7 +45,8 @@ cli_test_init_settings_file "$SETTINGS_FILE"
|
||||
# isConfigured=true is required for mirror (canProceedScan checks this)
|
||||
cli_test_mark_settings_configured "$SETTINGS_FILE"
|
||||
|
||||
# Enable writeDocumentsIfConflicted to resolve unsynced conflicts during mirror
|
||||
# Allow incoming DB content to be reflected when conflicts exist (Case 5).
|
||||
# This does not resolve conflicts or authorise overwriting DB content.
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const file = process.argv[1];
|
||||
@@ -181,6 +184,11 @@ echo "=== Case 4: storage newer → DB updated (Separated Paths) ==="
|
||||
# Seed DB with old content (mtime ≈ now)
|
||||
printf 'old content\n' | run_cli "$DB_DIR" --settings "$DB_SETTINGS" put test/sync-storage-newer.md
|
||||
|
||||
# Establish the file's recorded base before making an ordinary local edit.
|
||||
# A direct put followed by unrelated local content has unknown provenance.
|
||||
run_mirror_test
|
||||
cli_test_assert_equal "old content" "$(cat "$VAULT_DIR/test/sync-storage-newer.md")" "Case 4 base was not reflected"
|
||||
|
||||
# Write new content to storage with a timestamp 1 hour in the future
|
||||
printf 'new content\n' > "$VAULT_DIR/test/sync-storage-newer.md"
|
||||
touch -t "$(portable_touch_timestamp '+1 hour')" "$VAULT_DIR/test/sync-storage-newer.md"
|
||||
@@ -188,6 +196,8 @@ touch -t "$(portable_touch_timestamp '+1 hour')" "$VAULT_DIR/test/sync-storage-n
|
||||
run_mirror_test
|
||||
|
||||
DB_RESULT_FILE="$WORK_DIR/case4-pull.txt"
|
||||
CASE4_INFO="$(run_cli "$DB_DIR" --settings "$DB_SETTINGS" info test/sync-storage-newer.md)"
|
||||
cli_test_assert_equal "N/A" "$(printf '%s' "$CASE4_INFO" | cli_test_json_string_field_from_stdin conflicts)" "Ordinary local edit unexpectedly created a conflict"
|
||||
run_cli "$DB_DIR" --settings "$DB_SETTINGS" pull test/sync-storage-newer.md "$DB_RESULT_FILE"
|
||||
if cmp -s "$VAULT_DIR/test/sync-storage-newer.md" "$DB_RESULT_FILE"; then
|
||||
assert_pass "DB updated to match newer storage file"
|
||||
@@ -238,6 +248,55 @@ else
|
||||
assert_fail "Compatibility mode failed to sync file into DB"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Case 7: Unknown local origin must preserve both contents, regardless of mtime
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== Case 7: unknown local origin → preserve and resolve conflict ==="
|
||||
|
||||
UNKNOWN_PATH="test/unknown-origin.md"
|
||||
printf 'original DB content\n' | run_cli "$DB_DIR" --settings "$DB_SETTINGS" put "$UNKNOWN_PATH"
|
||||
printf 'unrelated local content\n' > "$VAULT_DIR/$UNKNOWN_PATH"
|
||||
touch -t "$(portable_touch_timestamp '+1 hour')" "$VAULT_DIR/$UNKNOWN_PATH"
|
||||
run_mirror_test
|
||||
|
||||
UNKNOWN_INFO="$(run_cli "$DB_DIR" --settings "$DB_SETTINGS" info "$UNKNOWN_PATH")"
|
||||
WINNER="$(printf '%s' "$UNKNOWN_INFO" | cli_test_json_string_field_from_stdin revision)"
|
||||
CONFLICT="$(printf '%s' "$UNKNOWN_INFO" | cli_test_json_string_field_from_stdin conflicts)"
|
||||
if [[ ! "$WINNER" =~ ^1-[[:xdigit:]]+$ || ! "$CONFLICT" =~ ^1-[[:xdigit:]]+$ || "$WINNER" == "$CONFLICT" ]]; then
|
||||
echo "[FAIL] Expected two independent non-deleted root revisions: $UNKNOWN_INFO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Do not assume which randomly identified root PouchDB selects as the winner.
|
||||
LOCAL_REV=""
|
||||
DB_REV=""
|
||||
for revision in "$WINNER" "$CONFLICT"; do
|
||||
CONTENT="$(run_cli "$DB_DIR" --settings "$DB_SETTINGS" cat-rev "$UNKNOWN_PATH" "$revision" | cli_test_sanitise_cat_stdout)"
|
||||
case "$CONTENT" in
|
||||
'unrelated local content') LOCAL_REV="$revision" ;;
|
||||
'original DB content') DB_REV="$revision" ;;
|
||||
*) echo "[FAIL] Unexpected content for $revision: $CONTENT" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$LOCAL_REV" && -n "$DB_REV" ]] || { echo "[FAIL] Both contents must remain readable" >&2; exit 1; }
|
||||
|
||||
# Force another ordinary save of the same unknown bytes, even if incoming
|
||||
# reflection replaced the file under writeDocumentsIfConflicted.
|
||||
printf 'unrelated local content\n' > "$VAULT_DIR/$UNKNOWN_PATH"
|
||||
touch -t "$(portable_touch_timestamp '+1 hour')" "$VAULT_DIR/$UNKNOWN_PATH"
|
||||
run_mirror_test
|
||||
REPEATED_INFO="$(run_cli "$DB_DIR" --settings "$DB_SETTINGS" info "$UNKNOWN_PATH")"
|
||||
cli_test_assert_equal "$WINNER" "$(printf '%s' "$REPEATED_INFO" | cli_test_json_string_field_from_stdin revision)" "Repeated mirror changed the winning revision"
|
||||
cli_test_assert_equal "$CONFLICT" "$(printf '%s' "$REPEATED_INFO" | cli_test_json_string_field_from_stdin conflicts)" "Repeated mirror created another conflict"
|
||||
|
||||
run_cli "$DB_DIR" --vault "$VAULT_DIR" --settings "$DB_SETTINGS" resolve "$UNKNOWN_PATH" "$LOCAL_REV"
|
||||
RESOLVED_INFO="$(run_cli "$DB_DIR" --settings "$DB_SETTINGS" info "$UNKNOWN_PATH")"
|
||||
cli_test_assert_equal "N/A" "$(printf '%s' "$RESOLVED_INFO" | cli_test_json_string_field_from_stdin conflicts)" "CLI resolve left a conflict"
|
||||
cli_test_assert_equal "$LOCAL_REV" "$(printf '%s' "$RESOLVED_INFO" | cli_test_json_string_field_from_stdin revision)" "CLI resolve selected the wrong revision"
|
||||
cli_test_assert_equal "unrelated local content" "$(cat "$VAULT_DIR/$UNKNOWN_PATH")" "CLI resolve did not reflect the selected content"
|
||||
assert_pass "Unknown local content was preserved, deduplicated, and resolved through the CLI"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Summary
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* 4. Both, storage newer -> DB updated (SYNC: STORAGE -> DB)
|
||||
* 5. Both, DB newer -> storage updated (SYNC: DB -> STORAGE)
|
||||
* 6. Compatibility mode -> omitted vault-path works (same DB + vault path)
|
||||
* 7. Unknown local origin -> conflict preserved, deduplicated, and resolved
|
||||
*
|
||||
* No external services are required.
|
||||
*
|
||||
@@ -18,9 +19,9 @@
|
||||
* deno test -A test-mirror.ts
|
||||
*/
|
||||
|
||||
import { assert } from "@std/assert";
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { TempDir } from "./helpers/temp.ts";
|
||||
import { runCliOrFail } from "./helpers/cli.ts";
|
||||
import { runCliOrFail, runCliWithInputOrFail } from "./helpers/cli.ts";
|
||||
import { initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts";
|
||||
|
||||
Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
||||
@@ -130,6 +131,10 @@ Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
||||
await Deno.writeTextFile(seedFile, "old content\n");
|
||||
await dbRun("push", seedFile, "test/sync-storage-newer.md");
|
||||
|
||||
// Reflect the shared base into the actual Vault before editing it.
|
||||
await runMirror();
|
||||
assertEquals(await Deno.readTextFile(workDir.join("vault", "test", "sync-storage-newer.md")), "old content\n");
|
||||
|
||||
// Write new content to storage with a timestamp 1 hour in the future
|
||||
const storageFile = workDir.join("vault", "test", "sync-storage-newer.md");
|
||||
await Deno.writeTextFile(storageFile, "new content\n");
|
||||
@@ -138,6 +143,8 @@ Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
||||
await runMirror();
|
||||
|
||||
const resultFile = workDir.join("case4-pull.txt");
|
||||
const info = JSON.parse(await dbRun("info", "test/sync-storage-newer.md"));
|
||||
assertEquals(info.conflicts, "N/A", "An ordinary local edit must not create a conflict");
|
||||
await dbRun("pull", "test/sync-storage-newer.md", resultFile);
|
||||
const storageContent = await Deno.readTextFile(storageFile);
|
||||
const pulledContent = await Deno.readTextFile(resultFile);
|
||||
@@ -184,6 +191,47 @@ Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
||||
assert(pulled === "compat-content\n", `Compatibility mode failed to sync file into DB (got: '${pulled}')`);
|
||||
console.log("[PASS] case 6: compatibility mode works");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 7: unknown local origin must preserve both contents regardless of mtime.
|
||||
// This deliberately uses put: push would record a file provenance entry.
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 7: unknown local content is preserved, deduplicated, and resolved", async () => {
|
||||
const path = "test/unknown-origin.md";
|
||||
const storageFile = workDir.join("vault", "test", "unknown-origin.md");
|
||||
await runCliWithInputOrFail("original DB content\n", dbDir, "--settings", dbSettings, "put", path);
|
||||
const writeUnknownFile = async () => {
|
||||
await Deno.writeTextFile(storageFile, "unrelated local content\n");
|
||||
await Deno.utime(storageFile, new Date(), new Date(Date.now() + 3600_000));
|
||||
};
|
||||
await writeUnknownFile();
|
||||
await runMirror();
|
||||
|
||||
const info = JSON.parse(await dbRun("info", path));
|
||||
assert(/^1-[\da-f]+$/.test(info.revision), "Expected an independent winning root");
|
||||
assert(/^1-[\da-f]+$/.test(info.conflicts), "Expected exactly one independent conflicting root");
|
||||
assert(info.revision !== info.conflicts, "Expected two distinct revisions");
|
||||
const contents = new Map<string, string>();
|
||||
for (const revision of [info.revision, info.conflicts]) {
|
||||
contents.set(await dbRun("cat-rev", path, revision), revision);
|
||||
}
|
||||
assertEquals([...contents.keys()].sort(), ["original DB content\n", "unrelated local content\n"]);
|
||||
|
||||
// Re-submit identical local bytes even if incoming reflection replaced
|
||||
// the file under writeDocumentsIfConflicted; no third branch is needed.
|
||||
await writeUnknownFile();
|
||||
await runMirror();
|
||||
const repeated = JSON.parse(await dbRun("info", path));
|
||||
assertEquals(repeated.revision, info.revision);
|
||||
assertEquals(repeated.conflicts, info.conflicts);
|
||||
|
||||
const localRevision = contents.get("unrelated local content\n")!;
|
||||
await runCliOrFail(dbDir, "--vault", vaultDir, "--settings", dbSettings, "resolve", path, localRevision);
|
||||
const resolved = JSON.parse(await dbRun("info", path));
|
||||
assertEquals(resolved.conflicts, "N/A");
|
||||
assertEquals(resolved.revision, localRevision);
|
||||
assertEquals(await Deno.readTextFile(storageFile), "unrelated local content\n");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -52,6 +52,31 @@ After pushing, confirm that the `Unit Tests` job passed for the exact commit bei
|
||||
|
||||
The local checks and CI use the project's installed rule versions and configured file scope. Record the authenticated external Community Review result separately when that review is required; the project-side checks do not replace it. When the external review reports additional findings, retain its relevant output and investigate differences in installation, type resolution, scope, or rules.
|
||||
|
||||
## CLI mirror regression tests
|
||||
|
||||
Run the native CLI subprocess suite after building the CLI:
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=3072 npm run build --workspace self-hosted-livesync-cli
|
||||
cd src/apps/cli/testdeno
|
||||
deno test -A --no-check test-mirror.ts
|
||||
```
|
||||
|
||||
The expected result is seven passing steps. These cover storage-only and database-only files, database deletion, an ordinary local edit, incoming database content, an omitted Vault path, and local content with unknown provenance.
|
||||
|
||||
For an ordinary local edit, first reflect the database content into the Vault, confirm its bytes, and then edit that file. The next `mirror` must store the edit without a conflict. For unknown provenance, use `put` to seed only the database and independently create different local content with a newer modification time. The next `mirror` must preserve two independent non-deleted revisions. Read both with `cat-rev`, submit the same local bytes again to check that no additional revision appears, and use `resolve` to select the local content. Confirm that the conflict is gone and the selected content is reflected into the Vault.
|
||||
|
||||
`put` deliberately bypasses file provenance, whereas `push` records it. Substituting one for the other changes the scenario. The tests enable `writeDocumentsIfConflicted` for incoming reflection; this setting does not authorise an ordinary save to replace unrelated database content or resolve the conflict. Do not assume which independent root PouchDB selects as the winner.
|
||||
|
||||
The [CLI Docker workflow](../.github/workflows/cli-docker.yml) runs the corresponding Bash suite. From the repository root, build and check that path with:
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max-old-space-size=3072 npm run build:docker --workspace self-hosted-livesync-cli
|
||||
npm run test:e2e:docker:mirror --workspace self-hosted-livesync-cli
|
||||
```
|
||||
|
||||
The expected result is `PASS=7 FAIL=0`. These mirror suites use temporary local databases and need no CouchDB service. The complete `test:e2e:docker:all` command also runs the other Docker CLI suites and manages a disposable CouchDB fixture; use it to check the full Docker CI gate. Keep the ordinary-edit and unknown-provenance scenarios aligned between the Deno and Bash suites.
|
||||
|
||||
## Multiple-device conflict regression tests
|
||||
|
||||
### Preparation and execution
|
||||
|
||||
Reference in New Issue
Block a user