mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-10 09:41:55 +00:00
feat(tests): add Deno-based tests for checking CLI functionality in the same-codebase between platforms.
This commit is contained in:
196
src/apps/cli/testdeno/test-mirror.ts
Normal file
196
src/apps/cli/testdeno/test-mirror.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Deno port of test-mirror-linux.sh
|
||||
*
|
||||
* Tests the `mirror` command — bidirectional synchronisation between a local
|
||||
* storage directory (vault) and an in-process database.
|
||||
*
|
||||
* Covered cases (identical to the bash test):
|
||||
* 1. Storage-only file -> synced into DB (UPDATE DATABASE)
|
||||
* 2. DB-only file -> restored to storage (UPDATE STORAGE)
|
||||
* 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 (same DB + vault path)
|
||||
*
|
||||
* No external services are required.
|
||||
*
|
||||
* Run:
|
||||
* deno test -A test-mirror.ts
|
||||
*/
|
||||
|
||||
import { assert } from "@std/assert";
|
||||
import { TempDir } from "./helpers/temp.ts";
|
||||
import { runCliOrFail } from "./helpers/cli.ts";
|
||||
import { initSettingsFile, markSettingsConfigured } from "./helpers/settings.ts";
|
||||
|
||||
Deno.test("mirror: storage <-> DB synchronisation", async (t) => {
|
||||
await using workDir = await TempDir.create("livesync-cli-mirror");
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Shared setup
|
||||
// -------------------------------------------------------------------
|
||||
const settingsFile = workDir.join("data.json");
|
||||
const vaultDir = workDir.join("vault");
|
||||
const dbDir = workDir.join("db");
|
||||
await Deno.mkdir(workDir.join("vault", "test"), { recursive: true });
|
||||
await Deno.mkdir(dbDir, { recursive: true });
|
||||
|
||||
await initSettingsFile(settingsFile);
|
||||
// isConfigured=true is required for canProceedScan in the mirror command.
|
||||
await markSettingsConfigured(settingsFile);
|
||||
|
||||
// Copy settings to the DB directory (separated-path mode)
|
||||
const dbSettings = workDir.join("db", "settings.json");
|
||||
await Deno.copyFile(settingsFile, dbSettings);
|
||||
|
||||
/** Run mirror in separated-path mode: DB dir ≠ vault dir. */
|
||||
const runMirror = () => runCliOrFail(dbDir, "--settings", dbSettings, "mirror", vaultDir);
|
||||
|
||||
/** Run mirror in compatibility mode: DB path = vault path. */
|
||||
const runMirrorCompat = () => runCliOrFail(vaultDir, "--settings", settingsFile, "mirror");
|
||||
|
||||
// Helper wrappers
|
||||
const dbRun = (...args: string[]) => runCliOrFail(dbDir, "--settings", dbSettings, ...args);
|
||||
const compatRun = (...args: string[]) => runCliOrFail(vaultDir, "--settings", settingsFile, ...args);
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 1: storage-only -> DB (UPDATE DATABASE)
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 1: storage-only file is synced into DB", async () => {
|
||||
const storageFile = workDir.join("vault", "test", "storage-only.md");
|
||||
await Deno.writeTextFile(storageFile, "storage-only content\n");
|
||||
|
||||
await runMirror();
|
||||
|
||||
const resultFile = workDir.join("case1-pull.txt");
|
||||
await dbRun("pull", "test/storage-only.md", resultFile);
|
||||
|
||||
const storageContent = await Deno.readTextFile(storageFile);
|
||||
const pulledContent = await Deno.readTextFile(resultFile);
|
||||
assert(
|
||||
storageContent === pulledContent,
|
||||
`storage-only file NOT synced into DB\nexpected: ${storageContent}\ngot: ${pulledContent}`
|
||||
);
|
||||
console.log("[PASS] case 1: storage-only file was synced into DB");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 2: DB-only -> storage (UPDATE STORAGE)
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 2: DB-only file is restored to storage", async () => {
|
||||
await dbRun(
|
||||
"push",
|
||||
// write inline via push (pipe not needed — push takes a file path)
|
||||
// create a temp file with content and push it
|
||||
await (async () => {
|
||||
const tmp = workDir.join("db-only-src.txt");
|
||||
await Deno.writeTextFile(tmp, "db-only content\n");
|
||||
return tmp;
|
||||
})(),
|
||||
"test/db-only.md"
|
||||
);
|
||||
|
||||
const storagePath = workDir.join("vault", "test", "db-only.md");
|
||||
assert(!(await exists(storagePath)), "db-only.md unexpectedly exists in storage before mirror");
|
||||
|
||||
await runMirror();
|
||||
|
||||
assert(await exists(storagePath), "DB-only file NOT restored to storage after mirror");
|
||||
const content = await Deno.readTextFile(storagePath);
|
||||
assert(content === "db-only content\n", `DB-only file restored but content mismatch: '${content}'`);
|
||||
console.log("[PASS] case 2: DB-only file was restored to storage");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 3: DB-deleted -> storage untouched
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 3: DB-deleted entry is NOT restored to storage", async () => {
|
||||
const deletedSrc = workDir.join("deleted-src.txt");
|
||||
await Deno.writeTextFile(deletedSrc, "to-be-deleted\n");
|
||||
await dbRun("push", deletedSrc, "test/deleted.md");
|
||||
await dbRun("rm", "test/deleted.md");
|
||||
|
||||
await runMirror();
|
||||
|
||||
const storagePath = workDir.join("vault", "test", "deleted.md");
|
||||
assert(!(await exists(storagePath)), "deleted DB entry was incorrectly restored to storage");
|
||||
console.log("[PASS] case 3: deleted DB entry was NOT restored to storage");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 4: storage newer -> DB updated (SYNC: STORAGE -> DB)
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 4: storage newer than DB -> DB is updated", async () => {
|
||||
// Seed DB with old content (mtime ~ now)
|
||||
const seedFile = workDir.join("case4-seed.txt");
|
||||
await Deno.writeTextFile(seedFile, "old content\n");
|
||||
await dbRun("push", seedFile, "test/sync-storage-newer.md");
|
||||
|
||||
// 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");
|
||||
await Deno.utime(storageFile, new Date(), new Date(Date.now() + 3600_000));
|
||||
|
||||
await runMirror();
|
||||
|
||||
const resultFile = workDir.join("case4-pull.txt");
|
||||
await dbRun("pull", "test/sync-storage-newer.md", resultFile);
|
||||
const storageContent = await Deno.readTextFile(storageFile);
|
||||
const pulledContent = await Deno.readTextFile(resultFile);
|
||||
assert(
|
||||
storageContent === pulledContent,
|
||||
`DB NOT updated to match newer storage file\nexpected: ${storageContent}\ngot: ${pulledContent}`
|
||||
);
|
||||
console.log("[PASS] case 4: DB updated to match newer storage file");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 5: DB newer -> storage updated (SYNC: DB -> STORAGE)
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 5: DB newer than storage -> storage is updated", async () => {
|
||||
// Write old content to storage with a timestamp 1 hour in the past
|
||||
const storageFile = workDir.join("vault", "test", "sync-db-newer.md");
|
||||
await Deno.writeTextFile(storageFile, "old storage content\n");
|
||||
await Deno.utime(storageFile, new Date(), new Date(Date.now() - 3600_000));
|
||||
|
||||
// Write new content to DB only (mtime ~ now, newer than the storage file)
|
||||
const dbNewFile = workDir.join("case5-db-new.txt");
|
||||
await Deno.writeTextFile(dbNewFile, "new db content\n");
|
||||
await dbRun("push", dbNewFile, "test/sync-db-newer.md");
|
||||
|
||||
await runMirror();
|
||||
|
||||
const content = await Deno.readTextFile(storageFile);
|
||||
assert(content === "new db content\n", `storage NOT updated to match newer DB entry (got: '${content}')`);
|
||||
console.log("[PASS] case 5: storage updated to match newer DB entry");
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Case 6: compatibility mode (vault path = DB path)
|
||||
// -------------------------------------------------------------------
|
||||
await t.step("case 6: compatibility mode (omitted vault-path)", async () => {
|
||||
const compatFile = workDir.join("vault", "compat.md");
|
||||
await Deno.writeTextFile(compatFile, "compat-content\n");
|
||||
|
||||
await runMirrorCompat();
|
||||
|
||||
const resultFile = workDir.join("case6-pull.txt");
|
||||
await compatRun("pull", "compat.md", resultFile);
|
||||
const pulled = await Deno.readTextFile(resultFile);
|
||||
assert(pulled === "compat-content\n", `Compatibility mode failed to sync file into DB (got: '${pulled}')`);
|
||||
console.log("[PASS] case 6: compatibility mode works");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await Deno.stat(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user