Compare commits

..
Author SHA1 Message Date
vorotamoroz ae264c3383 Lock patched devalue for 1.0.30 release 2026-09-18 09:11:20 +00:00
vorotamoroz df023f016e Clarify 1.0.30 synchronisation notes 2026-09-18 08:41:54 +00:00
github-actions[bot] 8dcd53090a Releasing 1.0.30 2026-09-18 08:40:49 +00:00
vorotamoroz 9998bd6dde Merge pull request #1204 from vrtmrz/fix/fast-fetch-security-seed-reads
Avoid repeated remote parameter reads during Fast Fetch and Journal transfers
2026-09-18 17:21:59 +09:00
vorotamoroz fa97f96960 Group pending synchronisation fixes in Unreleased notes 2026-09-18 08:14:38 +00:00
vorotamoroz 916ec4f276 Document restart conflict protection in 1.0.29 notes 2026-09-18 07:03:34 +00:00
vorotamoroz 8ea656a7da Use Commonlib 0.1.27 for Journal transfer readiness 2026-09-18 07:01:07 +00:00
vorotamoroz a3231b0906 Reuse Security Seed reads within each resource 2026-09-18 03:34:31 +00:00
vorotamoroz a8fe11e0e0 Merge pull request #1203 from vrtmrz/test/cli-mirror-provenance
Align CLI mirror tests with file provenance
2026-09-18 11:02:11 +09:00
vorotamoroz e8bb05e884 Test ordinary mirror edits and preserve unknown local content 2026-09-18 01:14:35 +00:00
vorotamoroz c60323e11f Merge pull request #1201 from vrtmrz/fix/stale-file-revision-provenance
Prevent stale local files from replacing newer database content
2026-09-18 01:31:08 +09:00
15 changed files with 228 additions and 24 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "1.0.29",
"version": "1.0.30",
"minAppVersion": "1.7.2",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
+12 -12
View File
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "1.0.29",
"version": "1.0.30",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "1.0.29",
"version": "1.0.30",
"license": "MIT",
"workspaces": [
"src/apps/cli",
@@ -23,7 +23,7 @@
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@vrtmrz/browser-ui-kit": "0.1.0",
"@vrtmrz/livesync-commonlib": "0.1.26",
"@vrtmrz/livesync-commonlib": "0.1.27",
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
"@vrtmrz/ui-interactions": "0.1.2",
"diff-match-patch": "^1.0.5",
@@ -4567,9 +4567,9 @@
}
},
"node_modules/@vrtmrz/livesync-commonlib": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.26.tgz",
"integrity": "sha512-AVJky976PP1M+g18im6tJ1eKSq82uN73vkS98WWZWvMJ1UDz6O8YRVWa9dkFakAVXecdxxxLkiD45g5HTRnn6Q==",
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.27.tgz",
"integrity": "sha512-n/AIqXPGN9Z7zmq0QSKnlE5uY5hTZgXW6VM6NJERR0/ggx3TTv1b3WsSbiLpCeU7Mu6KbPqC2BSjr4jCGvSdCA==",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
@@ -5629,9 +5629,9 @@
}
},
"node_modules/devalue": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.2.tgz",
"integrity": "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==",
"devOptional": true,
"license": "MIT"
},
@@ -12665,7 +12665,7 @@
},
"src/apps/cli": {
"name": "self-hosted-livesync-cli",
"version": "1.0.29-cli",
"version": "1.0.30-cli",
"dependencies": {
"chokidar": "^4.0.0",
"minimatch": "^10.2.5",
@@ -12690,7 +12690,7 @@
},
"src/apps/webapp": {
"name": "livesync-webapp",
"version": "1.0.29-webapp",
"version": "1.0.30-webapp",
"dependencies": {
"octagonal-wheels": "^0.1.54"
},
@@ -12702,7 +12702,7 @@
}
},
"src/apps/webpeer": {
"version": "1.0.29-webpeer",
"version": "1.0.30-webpeer",
"dependencies": {
"octagonal-wheels": "^0.1.54"
},
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "1.0.29",
"version": "1.0.30",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
@@ -183,7 +183,7 @@
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@vrtmrz/browser-ui-kit": "0.1.0",
"@vrtmrz/livesync-commonlib": "0.1.26",
"@vrtmrz/livesync-commonlib": "0.1.27",
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
"@vrtmrz/ui-interactions": "0.1.2",
"diff-match-patch": "^1.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "self-hosted-livesync-cli",
"private": true,
"version": "1.0.29-cli",
"version": "1.0.30-cli",
"main": "dist/index.cjs",
"type": "module",
"scripts": {
+60 -1
View File
@@ -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
# ─────────────────────────────────────────────────────────────────────────────
+50 -2
View File
@@ -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");
});
});
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "1.0.29-webapp",
"version": "1.0.30-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "1.0.29-webpeer",
"version": "1.0.30-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
+2 -1
View File
@@ -3,6 +3,7 @@ import {
CAPABILITY_NOT_APPLICABLE,
CENTRAL_REMOTE_REPLICATION_READINESS,
NO_INTERACTION,
PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
REPLICATION_PROGRESS_PRESENTATIONS,
REMOTE_RESOURCE_KINDS,
defineReplicatorProviderDefinitions,
@@ -134,7 +135,7 @@ export function createCentralReplicatorProviderDefinitions(
[REMOTE_MINIO]: {
kind: REMOTE_MINIO,
diagnosticName: "Object Storage",
readiness: CENTRAL_REMOTE_REPLICATION_READINESS,
readiness: PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
isConfigured: (settings) =>
settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(),
configurationIdentity: getObjectStorageReplicatorConfigurationIdentity,
@@ -47,6 +47,12 @@ describe("central Replicator provider definitions", () => {
.toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort());
});
it("lets Journal prepare its own fresh Security Seed while CouchDB uses central preparation", () => {
const definitions = createCentralReplicatorProviderDefinitions({} as never);
expect(definitions.get(REMOTE_COUCHDB)?.readiness.centralRemotePreparation).toBe("required");
expect(definitions.get(REMOTE_MINIO)?.readiness.centralRemotePreparation).toBe("provider-owned");
});
it("composes CouchDB and Object Storage policies outside LiveSyncBaseCore", async () => {
const host = {} as Parameters<typeof createCentralReplicatorProviderDefinitions>[0];
const definitions = createCentralReplicatorProviderDefinitions(host);
@@ -285,6 +285,46 @@ describe("replicator probe factories", () => {
expect(objectReplicator.closeReplication).toHaveBeenCalledOnce();
});
it("reads the Security Seed once per resource, including concurrent reads, and refreshes for a new resource", async () => {
const settings = createSettings();
const factory = createCouchDBSecuritySeedResourceFactory({} as never);
const firstResource = await factory(settings);
const firstReplicator = mocks.couchDB[0];
const firstSeed = new Uint8Array([1]);
firstReplicator.getReplicationPBKDF2Salt.mockResolvedValue(firstSeed);
const [first, concurrent] = await Promise.all([firstResource.read(), firstResource.read()]);
expect(first).toBe(firstSeed);
expect(concurrent).toBe(firstSeed);
await expect(firstResource.read()).resolves.toBe(firstSeed);
expect(firstReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledOnce();
expect(firstReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith({ ...settings }, true);
await firstResource.dispose();
const nextResource = await factory(settings);
const nextReplicator = mocks.couchDB[1];
const nextSeed = new Uint8Array([2]);
nextReplicator.getReplicationPBKDF2Salt.mockResolvedValue(nextSeed);
await expect(nextResource.read()).resolves.toBe(nextSeed);
expect(nextReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledOnce();
expect(nextReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith({ ...settings }, true);
await nextResource.dispose();
});
it("retries a failed Security Seed read within the same resource", async () => {
const resource = await createCouchDBSecuritySeedResourceFactory({} as never)(createSettings());
const replicator = mocks.couchDB[0];
const failure = new Error("connection interrupted");
const seed = new Uint8Array([1]);
replicator.getReplicationPBKDF2Salt.mockRejectedValueOnce(failure).mockResolvedValueOnce(seed);
await expect(resource.read()).rejects.toBe(failure);
await expect(resource.read()).resolves.toBe(seed);
await expect(resource.read()).resolves.toBe(seed);
expect(replicator.getReplicationPBKDF2Salt).toHaveBeenCalledTimes(2);
await resource.dispose();
});
it("checks synchronisation information through an owned connection and disposes the private Replicator", async () => {
const settings = createSettings();
const snapshot = { ...settings };
+12 -1
View File
@@ -21,8 +21,19 @@ function createSecuritySeedResourceFactory(
return (setting) => {
const snapshot = snapshotRemoteSettings(setting);
const replicator = createReplicator();
let readPromise: Promise<Uint8Array<ArrayBuffer>> | undefined;
const read = () => {
if (readPromise) return readPromise;
const pending = Promise.resolve().then(() => replicator.getReplicationPBKDF2Salt(snapshot, true));
readPromise = pending;
// A failed read must not poison a later retry within the same resource.
void pending.catch(() => {
if (readPromise === pending) readPromise = undefined;
});
return pending;
};
return Promise.resolve({
read: () => replicator.getReplicationPBKDF2Salt(snapshot, true),
read,
dispose: createReplicatorDisposer(replicator),
});
};
+25
View File
@@ -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
+13
View File
@@ -12,6 +12,19 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
## Unreleased
## 1.0.30
18th September, 2026
### Synchronisation
#### Fixed
- After a restart, unchanged local files no longer overwrite newer synchronised content. (#994)
- When LiveSync cannot establish a local file's origin, it keeps the file as a conflict for you to review. This also applies to ordinary file synchronisation in the command-line tool.
- Fast Fetch completes initial setup with fewer remote requests.
- Object Storage synchronisation makes fewer remote requests while still checking its parameters before writing.
## 1.0.29
16th September, 2026
+2 -1
View File
@@ -40,5 +40,6 @@
"1.0.26": "1.7.2",
"1.0.27": "1.7.2",
"1.0.28": "1.7.2",
"1.0.29": "1.7.2"
"1.0.29": "1.7.2",
"1.0.30": "1.7.2"
}