diff --git a/src/apps/cli/testdeno/deno.json b/src/apps/cli/testdeno/deno.json index 8a273f8..2fd5183 100644 --- a/src/apps/cli/testdeno/deno.json +++ b/src/apps/cli/testdeno/deno.json @@ -1,7 +1,10 @@ { "tasks": { "test": "deno test --env-file=.test.env -A --no-check test-*.ts", - "test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts", + "test:local": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts test-mirror.ts test-daemon.ts", + "test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts", + "test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts", + "test:remote-commands": "deno test --env-file=.test.env -A --no-check test-remote-commands.ts", "test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts", "test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts", "test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts", diff --git a/src/apps/cli/testdeno/test-daemon.ts b/src/apps/cli/testdeno/test-daemon.ts new file mode 100644 index 0000000..e960dfc --- /dev/null +++ b/src/apps/cli/testdeno/test-daemon.ts @@ -0,0 +1,116 @@ +/** + * Deno port of test-daemon-linux.sh + * + * Tests daemon-related ignore rules behaviour. + * + * Tests that are runnable without a long-running daemon process are exercised + * here using the 'mirror' command, which calls the same 'isTargetFile' handler + * stack that the daemon uses. + * + * Covered cases: + * 1. .livesync/ignore with *.tmp pattern → ignored file is not synced to database + * 2. .livesync/ignore missing → no error, and normal synchronisation continues + * 3. import: .gitignore directive → patterns from .gitignore are merged + * + * Run: + * deno test -A test-daemon.ts + */ + +import { join } from "@std/path"; +import { assertEquals } from "@std/assert"; +import { TempDir } from "./helpers/temp.ts"; +import { runCliOrFail, runCli, assertContains, assertNotContains } from "./helpers/cli.ts"; +import { initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts"; + +Deno.test("daemon: ignore rules behaviour", async (t) => { + // ------------------------------------------------------------------------- + // Case 1: .livesync/ignore with *.tmp → ignored file not synced to database + // ------------------------------------------------------------------------- + await t.step("case 1: .livesync/ignore *.tmp prevents sync", async () => { + await using workDir = await TempDir.create("livesync-cli-daemon-c1"); + const settingsFile = workDir.join("data.json"); + const vaultDir = workDir.join("vault"); + + await Deno.mkdir(join(vaultDir, ".livesync"), { recursive: true }); + await Deno.mkdir(join(vaultDir, "notes"), { recursive: true }); + + await initSettingsFile(settingsFile); + await markSettingsConfigured(settingsFile); + + await Deno.writeTextFile(join(vaultDir, ".livesync", "ignore"), "*.tmp\n"); + await Deno.writeTextFile(join(vaultDir, "notes", "normal.md"), "normal content\n"); + await Deno.writeTextFile(join(vaultDir, "notes", "scratch.tmp"), "tmp content\n"); + + console.log("[INFO] Running mirror for Case 1..."); + await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror"); + + // The normal file should be in the database. + const resultNormal = workDir.join("case1-normal.txt"); + await runCliOrFail(vaultDir, "--settings", settingsFile, "pull", "notes/normal.md", resultNormal); + const normalContent = await Deno.readTextFile(resultNormal); + assertEquals(normalContent, "normal content\n", "normal.md content mismatch after mirror"); + + // The .tmp file should NOT be in the database. + const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls"); + assertNotContains(dbList, "scratch.tmp", "scratch.tmp (ignored) was unexpectedly synced to database"); + assertContains(dbList, "normal.md", "normal.md was not found in database after mirror"); + console.log("[PASS] Case 1 verified successfully"); + }); + + // ------------------------------------------------------------------------- + // Case 2: .livesync/ignore absent → no error, and normal synchronisation continues + // ------------------------------------------------------------------------- + await t.step("case 2: .livesync/ignore absent does not cause failure", async () => { + await using workDir = await TempDir.create("livesync-cli-daemon-c2"); + const settingsFile = workDir.join("data2.json"); + const vaultDir = workDir.join("vault2"); + + await Deno.mkdir(join(vaultDir, "notes"), { recursive: true }); + + await initSettingsFile(settingsFile); + await markSettingsConfigured(settingsFile); + + // No .livesync directory at all. + await Deno.writeTextFile(join(vaultDir, "notes", "hello.md"), "hello\n"); + + console.log("[INFO] Running mirror for Case 2..."); + const result = await runCli(vaultDir, "--settings", settingsFile, "mirror"); + assertEquals(result.code, 0, "mirror exited non-zero when .livesync/ignore is absent"); + + // The normal file should have been synced. + const resultHello = workDir.join("case2-hello.txt"); + await runCliOrFail(vaultDir, "--settings", settingsFile, "pull", "notes/hello.md", resultHello); + const helloContent = await Deno.readTextFile(resultHello); + assertEquals(helloContent, "hello\n", "file content mismatch when .livesync/ignore is absent"); + console.log("[PASS] Case 2 verified successfully"); + }); + + // ------------------------------------------------------------------------- + // Case 3: import: .gitignore merges patterns + // ------------------------------------------------------------------------- + await t.step("case 3: import: .gitignore directive merges patterns", async () => { + await using workDir = await TempDir.create("livesync-cli-daemon-c3"); + const settingsFile = workDir.join("data3.json"); + const vaultDir = workDir.join("vault3"); + + await Deno.mkdir(join(vaultDir, ".livesync"), { recursive: true }); + await Deno.mkdir(join(vaultDir, "notes"), { recursive: true }); + + await initSettingsFile(settingsFile); + await markSettingsConfigured(settingsFile); + + await Deno.writeTextFile(join(vaultDir, ".livesync", "ignore"), "import: .gitignore\n"); + await Deno.writeTextFile(join(vaultDir, ".gitignore"), "# gitignore comment\n*.log\nbuild/\n"); + + await Deno.writeTextFile(join(vaultDir, "notes", "regular.md"), "regular note\n"); + await Deno.writeTextFile(join(vaultDir, "notes", "debug.log"), "log content\n"); + + console.log("[INFO] Running mirror for Case 3..."); + await runCliOrFail(vaultDir, "--settings", settingsFile, "mirror"); + + const dbList = await runCliOrFail(vaultDir, "--settings", settingsFile, "ls"); + assertNotContains(dbList, "debug.log", "debug.log (ignored via .gitignore import) was unexpectedly synced to database"); + assertContains(dbList, "regular.md", "regular.md was not synced normally alongside .gitignore import rules"); + console.log("[PASS] Case 3 verified successfully"); + }); +}); diff --git a/src/apps/cli/testdeno/test-decoupled-vault.ts b/src/apps/cli/testdeno/test-decoupled-vault.ts new file mode 100644 index 0000000..7e0f69a --- /dev/null +++ b/src/apps/cli/testdeno/test-decoupled-vault.ts @@ -0,0 +1,114 @@ +/** + * Deno port of test-decoupled-vault-linux.sh + * + * Tests push, pull, and mirror command behaviour when the vault directory is + * decoupled (separated) from the database directory. + * + * Run: + * deno test -A test-decoupled-vault.ts + */ + +import { join } from "@std/path"; +import { assertEquals } from "@std/assert"; +import { TempDir } from "./helpers/temp.ts"; +import { runCliOrFail } from "./helpers/cli.ts"; +import { applyCouchdbSettings, initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts"; +import { startCouchdb, stopCouchdb } from "./helpers/docker.ts"; + +const REMOTE_PATH = Deno.env.get("REMOTE_PATH") ?? "test/push-pull-decoupled.txt"; + +Deno.test("decoupled database and vault", async () => { + await using workDir = await TempDir.create("livesync-cli-decoupled"); + + const settingsFile = workDir.join("data.json"); + const vaultDir = workDir.join("vault"); + const dbDir = workDir.join("db"); + + await Deno.mkdir(join(vaultDir, "test"), { recursive: true }); + await Deno.mkdir(dbDir, { recursive: true }); + + const uri = Deno.env.get("COUCHDB_URI") ?? "http://127.0.0.1:5989/"; + const user = Deno.env.get("COUCHDB_USER") ?? "admin"; + const password = Deno.env.get("COUCHDB_PASSWORD") ?? "testpassword"; + const dbname = Deno.env.get("COUCHDB_DBNAME") ?? `decoupled-${Date.now()}`; + + const shouldStartDocker = Deno.env.get("LIVESYNC_START_DOCKER") !== "0"; + const keepDocker = Deno.env.get("LIVESYNC_DEBUG_KEEP_DOCKER") === "1"; + + if (shouldStartDocker) { + await startCouchdb(uri, user, password, dbname); + } + + try { + await initSettingsFile(settingsFile); + + if (uri && user && password && dbname) { + console.log("[INFO] applying CouchDB environment variables to settings"); + await applyCouchdbSettings(settingsFile, uri, user, password, dbname); + } else { + console.warn( + "[WARN] CouchDB environment variables are not fully set. Push and pull operations may fail." + ); + await markSettingsConfigured(settingsFile); + } + + const srcFile = workDir.join("push-source.txt"); + const pulledFile = workDir.join("pull-result.txt"); + const content = `push-pull-decoupled-test ${new Date().toISOString()}\n`; + await Deno.writeTextFile(srcFile, content); + + // 1. Test push command with decoupled vault directory + console.log(`[INFO] push with decoupled vault -> ${REMOTE_PATH}`); + await runCliOrFail( + dbDir, + "--vault", + vaultDir, + "--settings", + settingsFile, + "push", + srcFile, + REMOTE_PATH + ); + + // 2. Test pull command with decoupled vault directory + console.log(`[INFO] pull with decoupled vault <- ${REMOTE_PATH}`); + await runCliOrFail( + dbDir, + "--vault", + vaultDir, + "--settings", + settingsFile, + "pull", + REMOTE_PATH, + pulledFile + ); + + const pulled = await Deno.readTextFile(pulledFile); + assertEquals(pulled, content, "push/pull roundtrip with decoupled vault content mismatch"); + console.log("[PASS] push/pull roundtrip with decoupled vault matched"); + + // 3. Clean up pulled file and vault test directory to verify mirror + await Deno.remove(pulledFile).catch(() => {}); + await Deno.remove(join(vaultDir, "test"), { recursive: true }).catch(() => {}); + + // 4. Test mirror command with decoupled vault directory + console.log("[INFO] mirror with decoupled vault"); + await runCliOrFail( + dbDir, + "--vault", + vaultDir, + "--settings", + settingsFile, + "mirror" + ); + + const restoredFile = join(vaultDir, REMOTE_PATH); + const restored = await Deno.readTextFile(restoredFile); + assertEquals(restored, content, "mirror with decoupled vault content mismatch"); + console.log("[PASS] mirror with decoupled vault matched"); + } finally { + if (shouldStartDocker && !keepDocker) { + await stopCouchdb().catch(() => {}); + } + } +}); diff --git a/src/apps/cli/testdeno/test-remote-commands.ts b/src/apps/cli/testdeno/test-remote-commands.ts new file mode 100644 index 0000000..9ed0afb --- /dev/null +++ b/src/apps/cli/testdeno/test-remote-commands.ts @@ -0,0 +1,130 @@ +/** + * Deno port of test-remote-commands-linux.sh + * + * Tests remote management commands: remote-status, lock-remote, unlock-remote, + * and mark-resolved. + * + * Scenario: + * 1. Start CouchDB, create a test database, and perform an initial sync. + * 2. Run remote-status and assert that the output contains the database name in JSON format. + * 3. Run lock-remote and verify that the remote database is locked. + * 4. Lock the remote database milestone manually, verify status, and run unlock-remote. + * Assert that the output of unlock-remote contains the unlocked verification status. + * 5. Lock the remote database milestone manually, run mark-resolved, and verify that the + * current device is accepted. + * + * Run: + * deno test -A test-remote-commands.ts + */ + +import { join } from "@std/path"; +import { TempDir } from "./helpers/temp.ts"; +import { runCli, assertContains } from "./helpers/cli.ts"; +import { applyCouchdbSettings, initSettingsFile } from "./helpers/settings.ts"; +import { startCouchdb, stopCouchdb, updateCouchdbDoc } from "./helpers/docker.ts"; + +async function runCliCombinedOrFail(...args: string[]): Promise { + const res = await runCli(...args); + if (res.code !== 0) { + throw new Error(`CLI exited with code ${res.code}\nstdout: ${res.stdout}\nstderr: ${res.stderr}`); + } + return res.combined; +} + +Deno.test("remote management commands", async () => { + await using workDir = await TempDir.create("livesync-cli-remote-cmds"); + + const settingsFile = workDir.join("settings.json"); + const vaultDir = workDir.join("vault"); + await Deno.mkdir(vaultDir, { recursive: true }); + + const uri = Deno.env.get("COUCHDB_URI") ?? "http://127.0.0.1:5989/"; + const user = Deno.env.get("COUCHDB_USER") ?? "admin"; + const password = Deno.env.get("COUCHDB_PASSWORD") ?? "testpassword"; + const dbSuffix = `${Date.now()}-${Math.floor(Math.random() * 10000)}`; + const dbname = Deno.env.get("COUCHDB_DBNAME") ?? `remotes-${dbSuffix}`; + + const shouldStartDocker = Deno.env.get("LIVESYNC_START_DOCKER") !== "0"; + const keepDocker = Deno.env.get("LIVESYNC_DEBUG_KEEP_DOCKER") === "1"; + + if (shouldStartDocker) { + await startCouchdb(uri, user, password, dbname); + } + + try { + await initSettingsFile(settingsFile); + await applyCouchdbSettings(settingsFile, uri, user, password, dbname, true); + + console.log("[INFO] Performing initial sync to create milestone document..."); + await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "sync"); + + // 1. remote-status outputs valid JSON with CouchDB details + console.log("[CASE] remote-status outputs valid JSON with CouchDB details"); + const statusOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "remote-status"); + assertContains( + statusOutput, + `"db_name": "${dbname}"`, + "remote-status should return JSON containing db_name" + ); + console.log("[PASS] remote-status verified"); + + // 2. lock-remote locks and verifies state + console.log("[CASE] lock-remote locks and verifies state"); + const lockOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "lock-remote"); + assertContains( + lockOutput, + "[Verification] Remote Database: LOCKED", + "lock-remote output should show that the remote database is locked" + ); + console.log("[PASS] lock-remote verified"); + + // 3. unlock-remote unlocks and verifies state + console.log("[CASE] unlock-remote unlocks and verifies state"); + // Manually lock milestone + console.log("[INFO] Manually locking milestone..."); + await updateCouchdbDoc(uri, user, password, `${dbname}/_local/obsydian_livesync_milestone`, (doc) => { + doc.locked = true; + doc.accepted_nodes = []; + return doc; + }); + + // Run unlock-remote and verify output contains verification message + const unlockOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "unlock-remote"); + assertContains( + unlockOutput, + "[Verification] Remote Database: UNLOCKED", + "unlock-remote output should contain verification status" + ); + console.log("[PASS] unlock-remote verified"); + + // 4. mark-resolved resolves and verifies state + console.log("[CASE] mark-resolved resolves and verifies state"); + // Manually lock milestone + console.log("[INFO] Manually locking milestone..."); + await updateCouchdbDoc(uri, user, password, `${dbname}/_local/obsydian_livesync_milestone`, (doc) => { + doc.locked = true; + doc.accepted_nodes = []; + return doc; + }); + + // Run mark-resolved and verify output contains verification messages + const resolvedOutput = await runCliCombinedOrFail(vaultDir, "--settings", settingsFile, "mark-resolved"); + assertContains( + resolvedOutput, + "[Verification] Remote Database: LOCKED", + "mark-resolved output should show that the remote database remains locked" + ); + assertContains( + resolvedOutput, + "ACCEPTED", + "mark-resolved output should show that the current device node is accepted" + ); + console.log("[PASS] mark-resolved verified"); + + console.log("[ALL PASS] All remote CLI commands verified successfully"); + } finally { + if (shouldStartDocker && !keepDocker) { + await stopCouchdb().catch(() => {}); + } + } +}); diff --git a/src/apps/cli/testdeno/test_dev_deno.md b/src/apps/cli/testdeno/test_dev_deno.md index 9ad15ae..8926b9c 100644 --- a/src/apps/cli/testdeno/test_dev_deno.md +++ b/src/apps/cli/testdeno/test_dev_deno.md @@ -39,6 +39,9 @@ src/apps/cli/testdeno/ test-mirror.ts test-sync-two-local-databases.ts test-sync-locked-remote.ts + test-daemon.ts + test-decoupled-vault.ts + test-remote-commands.ts ``` --- @@ -54,6 +57,9 @@ Main tasks: - `deno task test` - `deno task test:local` +- `deno task test:daemon` +- `deno task test:decoupled-vault` +- `deno task test:remote-commands` - `deno task test:push-pull` - `deno task test:setup-put-cat` - `deno task test:mirror` @@ -183,6 +189,19 @@ Both CouchDB and P2P relay flows are bash-independent. - `MINIO-enc0` - `MINIO-enc1` +### `test-daemon.ts` + +- Verifies daemon-related ignore rules behaviour. +- Exercises scenarios with `.livesync/ignore` wildcard rules, missing ignore rules, and imported `.gitignore` rules. + +### `test-decoupled-vault.ts` + +- Verifies push, pull, and mirror command behaviour when the vault directory is decoupled from the database directory. + +### `test-remote-commands.ts` + +- Verifies remote database management commands: `remote-status`, `lock-remote`, `unlock-remote`, and `mark-resolved`. + --- ## Running tests (PowerShell) @@ -198,11 +217,14 @@ deno task test:local # Individual tests deno task test:setup-put-cat deno task test:mirror +deno task test:daemon deno task test:push-pull deno task test:sync-locked-remote # CouchDB-based tests deno task test:sync-two-local +deno task test:decoupled-vault +deno task test:remote-commands deno task test:e2e-couchdb # P2P-based tests