144 lines
5.7 KiB
JavaScript
144 lines
5.7 KiB
JavaScript
/**
|
|
* PR-3: Search Shadow Migration Test
|
|
*
|
|
* Run: node --test test/memory-core/search-shadow.test.mjs
|
|
*
|
|
* Tests the shadow comparison tool:
|
|
* 1. Shadow tool is runnable
|
|
* 2. All 22 queries complete
|
|
* 3. Shape compatibility reported for each
|
|
* 4. Empty query handled gracefully
|
|
* 5. Production check still passes
|
|
*/
|
|
|
|
import { describe, it, before } from "node:test";
|
|
import assert from "node:assert";
|
|
import { join, resolve } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
|
const ROOT = resolve(__dirname);
|
|
const SHADOW_TOOL = join(ROOT, "scripts", "memory-search-shadow.mjs");
|
|
|
|
describe("PR-3: Search Shadow Migration Test", () => {
|
|
|
|
let shadowResult;
|
|
|
|
before(async () => {
|
|
// Run shadow tool with --json for structured output
|
|
const result = spawnSync("node", [SHADOW_TOOL, "--json"], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
timeout: 60000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
|
|
const output = result.stdout + result.stderr;
|
|
|
|
// Extract JSON from output (may have other text)
|
|
const jsonMatch = output.match(/\{[\s\S]*"results"[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
try {
|
|
shadowResult = JSON.parse(jsonMatch[0]);
|
|
} catch {
|
|
// Fall back to running without --json and parsing text
|
|
shadowResult = null;
|
|
}
|
|
}
|
|
|
|
console.log(output.slice(-500));
|
|
});
|
|
|
|
// ─── Test 1 ──────────────────────────────────────────
|
|
it("shadow tool is runnable and produces results", () => {
|
|
assert.ok(shadowResult, "Shadow tool should produce JSON output");
|
|
assert.ok(typeof shadowResult.totalQueries === "number", "Should have totalQueries");
|
|
assert.ok(shadowResult.totalQueries >= 20, `Expected >=20 queries, got ${shadowResult.totalQueries}`);
|
|
});
|
|
|
|
// ─── Test 2 ──────────────────────────────────────────
|
|
it("all queries complete without crash", () => {
|
|
assert.ok(Array.isArray(shadowResult.results), "results should be array");
|
|
assert.equal(shadowResult.results.length, shadowResult.totalQueries,
|
|
"Results count should match total queries");
|
|
});
|
|
|
|
// ─── Test 3 ──────────────────────────────────────────
|
|
it("each query reports shapeCompatible", () => {
|
|
for (const r of shadowResult.results) {
|
|
assert.ok(typeof r.shapeCompatible === "boolean",
|
|
`shapeCompatible should be boolean for query "${r.query}"`);
|
|
}
|
|
});
|
|
|
|
// ─── Test 4 ──────────────────────────────────────────
|
|
it("empty query handled gracefully (documented as known difference)", () => {
|
|
const emptyResult = shadowResult.results.find(r => r.query === "");
|
|
assert.ok(emptyResult, "Should have empty query test case");
|
|
|
|
// Empty query: legacy FTS5 returns all, facade rejects
|
|
// This is a KNOWN difference — facade is more correct
|
|
assert.equal(emptyResult.legacyOk, true,
|
|
"Legacy treats empty as match-all (known quirk)");
|
|
assert.equal(emptyResult.facadeOk, false,
|
|
"Facade rejects empty query (correct behavior)");
|
|
// shapeCompatible may be false here — that's OK, documented
|
|
});
|
|
|
|
// ─── Test 5 ──────────────────────────────────────────
|
|
it("high shape compatibility rate (>90%)", () => {
|
|
const rate = shadowResult.passed / shadowResult.totalQueries;
|
|
assert.ok(rate >= 0.9,
|
|
`Shape compatibility rate ${Math.round(rate*100)}% below 90% threshold`);
|
|
});
|
|
|
|
// ─── Test 6 ──────────────────────────────────────────
|
|
it("results have required fields", () => {
|
|
const requiredFields = [
|
|
"query", "legacyOk", "facadeOk", "legacyCount", "facadeCount",
|
|
"topOverlap", "shapeCompatible", "latencyLegacyMs", "latencyFacadeMs",
|
|
];
|
|
|
|
for (const r of shadowResult.results) {
|
|
for (const field of requiredFields) {
|
|
assert.ok(field in r, `Missing field "${field}" in result for "${r.query}"`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// ─── Test 7 ──────────────────────────────────────────
|
|
it("production check still passes after shadow", { timeout: 180000 }, () => {
|
|
const result = spawnSync("bash", [join(ROOT, "scripts", "production-check.sh")], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
timeout: 170000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
|
|
const output = result.stdout + result.stderr;
|
|
|
|
assert.equal(result.status, 0,
|
|
`Production check failed (exit ${result.status})`);
|
|
assert.ok(output.includes("PRODUCTION CHECK PASSED"),
|
|
"Production check should report PASSED");
|
|
});
|
|
|
|
// ─── Test 8 ──────────────────────────────────────────
|
|
it("shadow compare does not modify any memory", async () => {
|
|
const { readFileSync } = await import("node:fs");
|
|
const memContent = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
|
|
|
// Re-run shadow tool
|
|
spawnSync("node", [SHADOW_TOOL], {
|
|
cwd: ROOT,
|
|
encoding: "utf8",
|
|
timeout: 60000,
|
|
});
|
|
|
|
// Verify MEMORY.md unchanged
|
|
const memAfter = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
|
assert.equal(memAfter, memContent, "MEMORY.md must not be modified by shadow");
|
|
});
|
|
});
|