/** * PR-3: Get Shadow Migration Test * * Run: node --test test/memory-core/get-shadow.test.mjs * * Compares legacy memory_get (file system read) vs MemoryCore.get() * across test cases: valid files, missing files, path traversal, etc. */ import { describe, it, before } from "node:test"; import assert from "node:assert"; import { readFileSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = join(fileURLToPath(import.meta.url), "..", "..", ".."); const ROOT = resolve(__dirname); let createMemoryCore; let mc; before(async () => { const mod = await import(join(ROOT, "src", "memory-core", "memory-core-facade.mjs")); createMemoryCore = mod.createMemoryCore; mc = createMemoryCore(ROOT); }); /** * Simulate legacy memory_get behavior: read a file from workspace. */ function legacyGet(relativePath) { try { const normalized = relativePath.replace(/^\/+/, ""); if (normalized.includes("..")) { return { ok: false, error: { code: "PATH_TRAVERSAL", message: "Path traversal denied" } }; } const fullPath = join(ROOT, normalized); if (!existsSync(fullPath)) { return { ok: false, error: { code: "NOT_FOUND", message: "File not found" } }; } const content = readFileSync(fullPath, "utf8"); return { ok: true, content }; } catch (e) { return { ok: false, error: { code: "READ_ERROR", message: e.message } }; } } /** * Compare legacy and facade get results. */ function compareGetResults(testCase, legacy, facade) { const checks = { okConsistent: legacy.ok === facade.ok, hasContentWhenOk: !legacy.ok || (typeof legacy.content === "string" && typeof facade.content === "string"), errorWhenNotOk: legacy.ok || (legacy.error !== undefined && facade.error !== undefined), noContentWhenNotOk: legacy.ok || (legacy.content === undefined && facade.content === undefined), }; // For successful reads, content should match if (legacy.ok && facade.ok) { // Allow small differences (line ending normalization) const legacyTrimmed = (legacy.content || "").replace(/\r\n/g, "\n").trim(); const facadeTrimmed = (facade.content || "").replace(/\r\n/g, "\n").trim(); checks.contentMatches = legacyTrimmed === facadeTrimmed; } else { checks.contentMatches = true; // N/A when not ok } const allCompatible = Object.values(checks).every(Boolean); return { testCase, legacyOk: legacy.ok, facadeOk: facade.ok, allCompatible, checks, legacyError: legacy.error || null, facadeError: facade.error || null, }; } describe("PR-3: Get Shadow Migration Test", () => { // ─── Test 1 ────────────────────────────────────────── it("MEMORY.md — both return ok:true with matching content", async () => { const legacy = legacyGet("MEMORY.md"); const facade = await mc.get("MEMORY.md"); const cmp = compareGetResults("MEMORY.md", legacy, facade); assert.equal(legacy.ok, true, "Legacy get should succeed"); assert.equal(facade.ok, true, "Facade get should succeed"); assert.ok(cmp.checks.contentMatches, "Content should match between legacy and facade"); }); // ─── Test 2 ────────────────────────────────────────── it("memory/vault.md — both return ok:true", async () => { const legacy = legacyGet("memory/vault.md"); const facade = await mc.get("memory/vault.md"); const cmp = compareGetResults("memory/vault.md", legacy, facade); assert.equal(legacy.ok, facade.ok, "ok consistency"); if (legacy.ok) { assert.ok(cmp.checks.contentMatches, "Content should match"); } }); // ─── Test 3 ────────────────────────────────────────── it("memory/daily/ — reads daily files", async () => { const files = ["memory/daily/2026-06-04.md", "memory/daily/2026-06-03.md"]; for (const file of files) { const legacy = legacyGet(file); const facade = await mc.get(file); const cmp = compareGetResults(file, legacy, facade); // Both should agree on existence assert.equal(legacy.ok, facade.ok, `${file}: ok mismatch (legacy=${legacy.ok}, facade=${facade.ok})`); if (legacy.ok && facade.ok) { assert.ok(cmp.checks.contentMatches, `${file}: content mismatch`); } } }); // ─── Test 4 ────────────────────────────────────────── it("nonexistent file — both return ok:false", async () => { const testFile = "definitely-does-not-exist-99999-xyz.md"; const legacy = legacyGet(testFile); const facade = await mc.get(testFile); const cmp = compareGetResults(testFile, legacy, facade); assert.equal(legacy.ok, false, "Legacy should fail for missing file"); assert.equal(facade.ok, false, "Facade should fail for missing file"); assert.ok(cmp.allCompatible, `Compatibility: ${JSON.stringify(cmp.checks)}`); }); // ─── Test 5 ────────────────────────────────────────── it("path traversal blocked by both", async () => { const traversalPaths = [ "../etc/passwd", "../../../root/.ssh/id_rsa", "..%2F..%2Fetc/passwd", ]; for (const path of traversalPaths) { const legacy = legacyGet(path); const facade = await mc.get(path); assert.equal(legacy.ok, false, `Legacy should block "${path}"`); assert.equal(facade.ok, false, `Facade should block "${path}"`); } }); // ─── Test 6 ────────────────────────────────────────── it("empty path — both reject", async () => { const legacy = legacyGet(""); const facade = await mc.get(""); // Both should fail for empty path assert.equal(legacy.ok, false, "Legacy should fail for empty path"); assert.equal(facade.ok, false, "Facade should fail for empty path"); }); // ─── Test 7 ────────────────────────────────────────── it("core files all accessible via both", async () => { const coreFiles = ["SOUL.md", "USER.md", "AGENTS.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md"]; for (const file of coreFiles) { if (!existsSync(join(ROOT, file))) continue; const legacy = legacyGet(file); const facade = await mc.get(file); const cmp = compareGetResults(file, legacy, facade); assert.equal(legacy.ok, facade.ok, `${file}: ok mismatch`); if (legacy.ok && facade.ok) { assert.ok(cmp.checks.contentMatches, `${file}: content mismatch`); } } }); // ─── Test 8 ────────────────────────────────────────── it("does not modify any real files", async () => { // Take snapshots of key files const keyFiles = ["MEMORY.md", "SOUL.md", "memory/vault.md"].filter(f => existsSync(join(ROOT, f))); const snapshots = {}; for (const f of keyFiles) { snapshots[f] = readFileSync(join(ROOT, f), "utf8"); } // Run lots of get operations await mc.get("MEMORY.md"); await mc.get("memory/vault.md"); await mc.get("memory/daily/2026-06-04.md"); await mc.get("nonexistent"); await mc.get("../etc/passwd"); // should be blocked // Verify no changes for (const f of keyFiles) { const current = readFileSync(join(ROOT, f), "utf8"); assert.equal(current, snapshots[f], `${f} must not be modified`); } }); // ─── Test 9 ────────────────────────────────────────── it("all compatibility checks pass for standard files", async () => { const testCases = ["MEMORY.md", "memory/vault.md", "nonexistent-file-xxx"]; for (const tc of testCases) { const legacy = legacyGet(tc); const facade = await mc.get(tc); const cmp = compareGetResults(tc, legacy, facade); assert.ok(cmp.allCompatible, `${tc}: compatibility failure — ${Object.entries(cmp.checks).filter(([,v]) => !v).map(([k]) => k).join(", ")}`); } }); });