🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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(", ")}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user