Files
16gagent/test/memory-core/get-shadow.test.mjs
T
2026-06-06 10:40:48 +08:00

223 lines
8.5 KiB
JavaScript

/**
* 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(", ")}`);
}
});
});