🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* PR-2 MemoryCore Facade Test Suite
|
||||
*
|
||||
* Run: node --test test/memory-core/facade.test.mjs
|
||||
*
|
||||
* Tests the MemoryCore facade contract:
|
||||
* 1. Module importable
|
||||
* 2. health() returns ok:true
|
||||
* 3. stats() returns stable structure
|
||||
* 4. search() returns unified contract
|
||||
* 5. get("MEMORY.md") returns unified contract
|
||||
* 6. get("missing-file") returns ok:false
|
||||
* 7. write() only writes to test namespace
|
||||
* 8. delete() only deletes from test namespace
|
||||
* 9. Does not modify real MEMORY.md
|
||||
* 10. Does not change memory_search old tool behavior (verified by baseline)
|
||||
* 11. Does not change memory_get old tool behavior (verified by baseline)
|
||||
* 12. production-check still passes (verified in facade-health.test.mjs)
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { readFileSync, existsSync, unlinkSync, writeFileSync } 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);
|
||||
|
||||
// Dynamic import because facade uses ESM
|
||||
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);
|
||||
});
|
||||
|
||||
describe("PR-2: MemoryCore Facade", () => {
|
||||
|
||||
// ─── Test 1: Module importable ────────────────────
|
||||
it("facade module is importable", () => {
|
||||
assert.ok(typeof createMemoryCore === "function", "createMemoryCore should be a function");
|
||||
assert.ok(mc !== undefined, "facade instance should exist");
|
||||
assert.ok(typeof mc.search === "function", "mc.search should be a function");
|
||||
assert.ok(typeof mc.get === "function", "mc.get should be a function");
|
||||
assert.ok(typeof mc.write === "function", "mc.write should be a function");
|
||||
assert.ok(typeof mc.delete === "function", "mc.delete should be a function");
|
||||
assert.ok(typeof mc.stats === "function", "mc.stats should be a function");
|
||||
assert.ok(typeof mc.health === "function", "mc.health should be a function");
|
||||
});
|
||||
|
||||
// ─── Test 2: health() returns ok:true ──────────────
|
||||
it("health() returns ok:true", async () => {
|
||||
const h = await mc.health();
|
||||
assert.ok(h.ok === true, `Expected health.ok=true, got ${JSON.stringify(h)}`);
|
||||
assert.ok(h.memoryCoreAvailable === true, "memoryCoreAvailable should be true");
|
||||
assert.ok(h.canSearch === true, "canSearch should be true");
|
||||
assert.ok(h.canGet === true, "canGet should be true");
|
||||
assert.ok(h.canWriteTestNamespace === true, "canWriteTestNamespace should be true");
|
||||
});
|
||||
|
||||
// ─── Test 3: stats() returns stable structure ──────
|
||||
it("stats() returns stable structure", async () => {
|
||||
const s = await mc.stats();
|
||||
|
||||
assert.ok(typeof s.memoryFileCount === "number", "memoryFileCount should be number");
|
||||
assert.ok(s.memoryFileCount > 0, `Expected >0 files, got ${s.memoryFileCount}`);
|
||||
assert.ok(typeof s.memoryTotalSizeBytes === "number", "memoryTotalSizeBytes should be number");
|
||||
assert.ok(s.memoryTotalSizeBytes > 0, "memoryTotalSizeBytes should be > 0");
|
||||
assert.ok(typeof s.knownPluginCount === "number", "knownPluginCount should be number");
|
||||
assert.ok(s.knownPluginCount >= 2, `Expected >=2 plugins, got ${s.knownPluginCount}`);
|
||||
assert.ok(typeof s.legacyItemCount === "number", "legacyItemCount should be number");
|
||||
assert.ok(s.legacyItemCount >= 1, `Expected >=1 legacy items, got ${s.legacyItemCount}`);
|
||||
|
||||
// Breakdown
|
||||
assert.ok(s.breakdown !== undefined, "breakdown should exist");
|
||||
assert.ok(typeof s.breakdown.daily === "number", "breakdown.daily should be number");
|
||||
assert.ok(typeof s.breakdown.registers === "number", "breakdown.registers should be number");
|
||||
assert.ok(typeof s.breakdown.vault === "number", "breakdown.vault should be number");
|
||||
assert.ok(typeof s.breakdown.core === "number", "breakdown.core should be number");
|
||||
});
|
||||
|
||||
// ─── Test 4: search() returns unified contract ─────
|
||||
it("search() returns unified contract", async () => {
|
||||
const result = await mc.search("memory", { maxResults: 5 });
|
||||
|
||||
assert.equal(result.ok, true, `search ok should be true: ${JSON.stringify(result.error)}`);
|
||||
assert.ok(Array.isArray(result.results), "results should be array");
|
||||
assert.ok(["fts5-search", "memory-core-facade"].includes(result.source), `source should be fts5-search or memory-core-facade, got ${result.source}`);
|
||||
assert.ok(typeof result.latencyMs === "number", "latencyMs should be number");
|
||||
assert.ok(result.latencyMs >= 0, "latencyMs should be >= 0");
|
||||
|
||||
// Each result should have the contract
|
||||
for (const r of result.results) {
|
||||
assert.ok(typeof r.id === "string", `result.id should be string, got ${typeof r.id}`);
|
||||
assert.ok(typeof r.path === "string", `result.path should be string`);
|
||||
assert.ok(typeof r.score === "number", `result.score should be number`);
|
||||
assert.ok(r.score >= 0 && r.score <= 1, `score should be 0-1, got ${r.score}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 5: search empty query returns ok:false ────
|
||||
it("search empty query returns error contract", async () => {
|
||||
const result = await mc.search("");
|
||||
assert.equal(result.ok, false, "empty query should fail");
|
||||
assert.ok(result.error, "should have error object");
|
||||
assert.ok(Array.isArray(result.results), "results should still be array");
|
||||
assert.equal(result.results.length, 0, "results should be empty");
|
||||
});
|
||||
|
||||
// ─── Test 6: get("MEMORY.md") returns unified contract ──
|
||||
it("get(MEMORY.md) returns unified contract", async () => {
|
||||
const result = await mc.get("MEMORY.md");
|
||||
|
||||
assert.equal(result.ok, true, `get MEMORY.md should succeed: ${JSON.stringify(result.error)}`);
|
||||
assert.ok(typeof result.content === "string", "content should be string");
|
||||
assert.ok(result.content.length > 100, `content too short: ${result.content.length} chars`);
|
||||
assert.ok(result.metadata, "should have metadata");
|
||||
assert.equal(result.metadata.path, "MEMORY.md");
|
||||
assert.ok(result.metadata.sizeBytes > 0, "sizeBytes should be > 0");
|
||||
assert.ok(typeof result.metadata.modifiedAt === "string", "modifiedAt should be string");
|
||||
});
|
||||
|
||||
// ─── Test 7: get with maxLines truncates ────────────
|
||||
it("get with maxLines truncates content", async () => {
|
||||
const result = await mc.get("MEMORY.md", { maxLines: 5 });
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
const lines = result.content.split("\n");
|
||||
assert.ok(lines.length <= 6, `Expected <=6 lines (5 + truncation notice), got ${lines.length}`);
|
||||
});
|
||||
|
||||
// ─── Test 8: get("missing-file") returns ok:false ───
|
||||
it("get(missing-file) returns ok:false", async () => {
|
||||
const result = await mc.get("definitely-does-not-exist-99999.md");
|
||||
|
||||
assert.equal(result.ok, false, "missing file should fail");
|
||||
assert.ok(result.error, "should have error object");
|
||||
assert.ok(result.error.code, "should have error code");
|
||||
// Should not have content on failure
|
||||
assert.ok(result.content === undefined, "should not have content on failure");
|
||||
});
|
||||
|
||||
// ─── Test 9: get path traversal is blocked ──────────
|
||||
it("get blocks path traversal", async () => {
|
||||
const result = await mc.get("../etc/passwd");
|
||||
assert.equal(result.ok, false, "path traversal should fail");
|
||||
assert.ok(result.error.code.includes("TRAVERSAL") || result.error.code.includes("NOT_ALLOWED"),
|
||||
`Expected traversal/allowed error, got ${result.error.code}`);
|
||||
});
|
||||
|
||||
// ─── Test 10: write() only writes to test namespace ─
|
||||
it("write only allows test namespace", async () => {
|
||||
// Attempt to write to non-test namespace
|
||||
const badResult = await mc.write({ namespace: "memory", key: "something", content: "bad" });
|
||||
assert.equal(badResult.ok, false, "non-test namespace should fail");
|
||||
assert.equal(badResult.error.code, "NAMESPACE_LOCKED");
|
||||
|
||||
// Write to test namespace should work
|
||||
const testKey = `facade-test-${Date.now()}`;
|
||||
const goodResult = await mc.write({ namespace: "test", key: testKey, content: "hello from facade test" });
|
||||
assert.equal(goodResult.ok, true, `test namespace write should succeed: ${JSON.stringify(goodResult.error)}`);
|
||||
assert.ok(goodResult.path, "should return path");
|
||||
assert.ok(goodResult.path.includes("test-memory-core-"), "path should contain test prefix");
|
||||
|
||||
// Clean up
|
||||
const delResult = await mc.delete(testKey);
|
||||
assert.equal(delResult.ok, true, "cleanup delete should succeed");
|
||||
});
|
||||
|
||||
// ─── Test 11: delete() only deletes test namespace ──
|
||||
it("delete only works on test namespace", async () => {
|
||||
// Delete non-existent key
|
||||
const result = await mc.delete("does-not-exist-key-xyz");
|
||||
assert.equal(result.ok, false, "non-existent key should fail");
|
||||
assert.equal(result.error.code, "NOT_FOUND");
|
||||
|
||||
// Write then delete
|
||||
const key = `delete-test-${Date.now()}`;
|
||||
await mc.write({ namespace: "test", key, content: "to be deleted" });
|
||||
const delResult = await mc.delete(key);
|
||||
assert.equal(delResult.ok, true, "delete should succeed");
|
||||
|
||||
// Double delete should fail
|
||||
const doubleDel = await mc.delete(key);
|
||||
assert.equal(doubleDel.ok, false, "double delete should fail");
|
||||
});
|
||||
|
||||
// ─── Test 12: Does not modify real MEMORY.md ───────
|
||||
it("does not modify real MEMORY.md", async () => {
|
||||
const originalContent = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
||||
const originalSize = Buffer.byteLength(originalContent, "utf8");
|
||||
|
||||
// Run all facade operations
|
||||
await mc.search("test query");
|
||||
await mc.get("MEMORY.md");
|
||||
await mc.stats();
|
||||
await mc.health();
|
||||
await mc.write({ namespace: "test", key: "integrity-check", content: "check" });
|
||||
await mc.delete("integrity-check");
|
||||
|
||||
// Verify MEMORY.md unchanged
|
||||
const currentContent = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
||||
const currentSize = Buffer.byteLength(currentContent, "utf8");
|
||||
|
||||
assert.equal(currentContent, originalContent, "MEMORY.md content must not change");
|
||||
assert.equal(currentSize, originalSize, "MEMORY.md size must not change");
|
||||
});
|
||||
|
||||
// ─── Test 13: search handles special characters ─────
|
||||
it("search handles special characters safely", async () => {
|
||||
const result = await mc.search("test * ? [regex] (special)");
|
||||
assert.equal(result.ok, true, "special chars should not crash search");
|
||||
assert.ok(Array.isArray(result.results), "results should be array");
|
||||
// May or may not find results, but must not crash
|
||||
});
|
||||
|
||||
// ─── Test 14: Multiple concurrent searches ──────────
|
||||
it("handles multiple concurrent searches", async () => {
|
||||
const queries = ["memory", "小龙", "architecture", "MEMORY", "test"];
|
||||
const results = await Promise.all(queries.map(q => mc.search(q, { maxResults: 3 })));
|
||||
|
||||
assert.equal(results.length, 5, "should have 5 results");
|
||||
for (const r of results) {
|
||||
assert.equal(r.ok, true, `each search should be ok: ${JSON.stringify(r.error)}`);
|
||||
assert.ok(Array.isArray(r.results), "each should have results array");
|
||||
assert.ok(["fts5-search", "memory-core-facade"].includes(r.source), `source should be fts5-search, got ${r.source}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 15: stats consistency ─────────────────────
|
||||
it("stats are consistent between calls", async () => {
|
||||
const s1 = await mc.stats();
|
||||
const s2 = await mc.stats();
|
||||
|
||||
assert.equal(s1.memoryFileCount, s2.memoryFileCount, "file count should be consistent");
|
||||
assert.equal(s1.memoryTotalSizeBytes, s2.memoryTotalSizeBytes, "total size should be consistent");
|
||||
assert.equal(s1.knownPluginCount, s2.knownPluginCount);
|
||||
assert.equal(s1.legacyItemCount, s2.legacyItemCount);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user