/** * Node.js port of test-sync-locked-remote-linux.sh * * Verifies CLI sync behaviour when the remote milestone document is unlocked * versus locked. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdir } from "node:fs/promises"; import { TempDir } from "./helpers/temp.ts"; import { runCli } from "./helpers/cli.ts"; import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts"; import { createCouchdbDatabase, startCouchdb, stopCouchdb, updateCouchdbDoc } from "./helpers/docker.ts"; function assertStringIncludes(actual: string, expected: string, message?: string): void { assert.ok(actual.includes(expected), message ?? `Expected ${JSON.stringify(actual)} to include ${JSON.stringify(expected)}`); } const MILESTONE_DOC = "_local/obsydian_livesync_milestone"; function requireEnv(...keys: string[]): string { for (const key of keys) { const value = process.env[key]?.trim(); if (value) return value; } throw new Error(`Required env var is missing: ${keys.join(" or ")}`); } test("sync: actionable error against locked remote DB", async () => { const couchdbUri = requireEnv("COUCHDB_URI", "hostname").replace(/\/$/, ""); const couchdbUser = requireEnv("COUCHDB_USER", "username"); const couchdbPassword = requireEnv("COUCHDB_PASSWORD", "password"); const dbPrefix = requireEnv("COUCHDB_DBNAME", "dbname"); const dbname = `${dbPrefix}-locked-${Date.now()}-${Math.floor(Math.random() * 100000)}`; await using workDir = await TempDir.create("livesync-cli-locked-test"); const vaultDir = workDir.join("vault"); const settingsFile = workDir.join("settings.json"); await mkdir(vaultDir, { recursive: true }); const shouldStartDocker = process.env["LIVESYNC_START_DOCKER"] !== "0"; const keepDocker = process.env["LIVESYNC_DEBUG_KEEP_DOCKER"] === "1"; if (shouldStartDocker) { console.log(`[INFO] starting CouchDB and creating test database: ${dbname}`); await startCouchdb(couchdbUri, couchdbUser, couchdbPassword, dbname); } else { console.log(`[INFO] using existing CouchDB and creating test database: ${dbname}`); await createCouchdbDatabase(couchdbUri, couchdbUser, couchdbPassword, dbname); } try { await initSettingsFile(settingsFile); await applyCouchdbSettings(settingsFile, couchdbUri, couchdbUser, couchdbPassword, dbname, true); console.log("[CASE] initial sync to create milestone document"); const initialSync = await runCli(vaultDir, "--settings", settingsFile, "sync"); assert( initialSync.code === 0, `initial sync failed\nstdout: ${initialSync.stdout}\nstderr: ${initialSync.stderr}` ); const updateMilestone = async (locked: boolean) => { await updateCouchdbDoc(couchdbUri, couchdbUser, couchdbPassword, `${dbname}/${MILESTONE_DOC}`, (doc) => ({ ...doc, locked, accepted_nodes: [], })); }; console.log("[CASE] sync should succeed when remote is not locked"); await updateMilestone(false); const unlockedSync = await runCli(vaultDir, "--settings", settingsFile, "sync"); assert( unlockedSync.code === 0, `sync should succeed when remote is not locked\nstdout: ${unlockedSync.stdout}\nstderr: ${unlockedSync.stderr}` ); assert( !unlockedSync.combined.includes("The remote database is locked"), `locked error should not appear when remote is not locked\n${unlockedSync.combined}` ); console.log("[PASS] unlocked remote DB syncs successfully"); console.log("[CASE] sync should fail with actionable error when remote is locked"); await updateMilestone(true); const lockedSync = await runCli(vaultDir, "--settings", settingsFile, "sync"); assert( lockedSync.code !== 0, `sync should fail when remote is locked\nstdout: ${lockedSync.stdout}\nstderr: ${lockedSync.stderr}` ); assertStringIncludes(lockedSync.combined, "The remote database is locked and this device is not yet accepted"); console.log("[PASS] locked remote DB produces actionable CLI error"); } finally { if (shouldStartDocker && !keepDocker) { await stopCouchdb().catch(() => {}); } } });