🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* PR-3.5: FTS5 Search Adapter Test
|
||||
*
|
||||
* Run: node --test test/memory-core/fts5-adapter.test.mjs
|
||||
*
|
||||
* Tests:
|
||||
* 1. FTS5 adapter is importable
|
||||
* 2. FTS5 adapter opens DB read-only
|
||||
* 3. FTS5 query returns unified contract
|
||||
* 4. FTS5 results have stable fields
|
||||
* 5. Empty query returns INVALID_QUERY
|
||||
* 6. Fallback to file-search when FTS5 unavailable
|
||||
* 7. Does not modify SQLite
|
||||
* 8. Does not modify MEMORY.md
|
||||
* 9. Production check still passes
|
||||
*/
|
||||
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const ROOT = resolve(__dirname);
|
||||
const DB_PATH = join(homedir(), ".openclaw", "memory", "main.sqlite");
|
||||
|
||||
let createFTS5Adapter;
|
||||
let adapter;
|
||||
let fts5Available;
|
||||
|
||||
before(async () => {
|
||||
const mod = await import(
|
||||
join(ROOT, "src", "memory-core", "memory-fts5-search-adapter.mjs")
|
||||
);
|
||||
createFTS5Adapter = mod.createFTS5SearchAdapter;
|
||||
adapter = createFTS5Adapter(DB_PATH);
|
||||
fts5Available = await adapter.isAvailable();
|
||||
});
|
||||
|
||||
describe("PR-3.5: FTS5 Search Adapter", () => {
|
||||
|
||||
// ─── Test 1 ──────────────────────────────────────────
|
||||
it("adapter is importable", () => {
|
||||
assert.ok(typeof createFTS5Adapter === "function",
|
||||
"createFTS5SearchAdapter should be a function");
|
||||
assert.ok(typeof adapter.search === "function",
|
||||
"adapter.search should be a function");
|
||||
assert.ok(typeof adapter.isAvailable === "function",
|
||||
"adapter.isAvailable should be a function");
|
||||
});
|
||||
|
||||
// ─── Test 2 ──────────────────────────────────────────
|
||||
it("adapter opens DB read-only", async () => {
|
||||
// FTS5 availability check
|
||||
const available = await adapter.isAvailable();
|
||||
assert.equal(typeof available, "boolean");
|
||||
|
||||
if (available) {
|
||||
// Verify we can search (proves read-only connection works)
|
||||
const result = await adapter.search("test", { maxResults: 1 });
|
||||
// Either ok:true (found results) or ok:true (no results)
|
||||
// or ok:false (FTS5 error on that term) — all acceptable
|
||||
assert.ok(typeof result.ok === "boolean", "should have ok field");
|
||||
assert.equal(result.source, "fts5-search",
|
||||
`source should be fts5-search, got ${result.source}`);
|
||||
} else {
|
||||
console.log(" FTS5 not available on this system — skipping read test");
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 3 ──────────────────────────────────────────
|
||||
it("FTS5 query returns unified contract", async () => {
|
||||
const result = await adapter.search("memory", { maxResults: 5 });
|
||||
|
||||
assert.equal(result.ok, true, `search should succeed: ${JSON.stringify(result.error)}`);
|
||||
assert.ok(Array.isArray(result.results), "results should be array");
|
||||
assert.ok(result.results.length > 0, "should find results for 'memory'");
|
||||
assert.equal(result.source, "fts5-search");
|
||||
assert.ok(typeof result.latencyMs === "number", "latencyMs should be number");
|
||||
assert.ok(result.latencyMs >= 0, "latencyMs should be >= 0");
|
||||
});
|
||||
|
||||
// ─── Test 4 ──────────────────────────────────────────
|
||||
it("FTS5 results have stable fields", async () => {
|
||||
const result = await adapter.search("OpenClaw", { maxResults: 5 });
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.ok(result.results.length > 0, "should find OpenClaw references");
|
||||
|
||||
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.snippet === "string", `result.snippet 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}`);
|
||||
assert.ok(typeof r.source === "string", `result.source should be string`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 5 ──────────────────────────────────────────
|
||||
it("empty query returns INVALID_QUERY", async () => {
|
||||
const result = await adapter.search("");
|
||||
|
||||
assert.equal(result.ok, false, "empty query should fail");
|
||||
assert.equal(result.error.code, "INVALID_QUERY",
|
||||
`Expected INVALID_QUERY, got ${result.error?.code}`);
|
||||
assert.ok(Array.isArray(result.results), "results should be array");
|
||||
assert.equal(result.results.length, 0, "results should be empty");
|
||||
assert.equal(result.source, "fts5-search");
|
||||
});
|
||||
|
||||
// ─── Test 6 ──────────────────────────────────────────
|
||||
it("fallback to file-search when FTS5 path is invalid", async () => {
|
||||
// Create adapter pointing to non-existent DB path
|
||||
const badAdapter = createFTS5Adapter("/nonexistent/path/main.sqlite");
|
||||
|
||||
// Should return error without fallback
|
||||
const result = await badAdapter.search("test", { maxResults: 3 });
|
||||
|
||||
assert.equal(result.ok, false, "should fail without DB");
|
||||
assert.equal(result.source, "file-search-fallback",
|
||||
"source should indicate fallback attempt");
|
||||
assert.ok(result.error, "should have error");
|
||||
});
|
||||
|
||||
// ─── Test 7 ──────────────────────────────────────────
|
||||
it("does not modify SQLite file", async () => {
|
||||
const statBefore = existsSync(DB_PATH) ? readFileSync(DB_PATH, null).length : -1;
|
||||
|
||||
// Run multiple searches
|
||||
await adapter.search("memory", { maxResults: 10 });
|
||||
await adapter.search("agent", { maxResults: 10 });
|
||||
await adapter.search("config", { maxResults: 10 });
|
||||
await adapter.search("");
|
||||
|
||||
const statAfter = existsSync(DB_PATH) ? readFileSync(DB_PATH, null).length : -1;
|
||||
|
||||
assert.equal(statAfter, statBefore,
|
||||
`SQLite file size changed: ${statBefore} → ${statAfter}`);
|
||||
});
|
||||
|
||||
// ─── Test 8 ──────────────────────────────────────────
|
||||
it("does not modify MEMORY.md", async () => {
|
||||
const before = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
||||
|
||||
// Run queries
|
||||
await adapter.search("MEMORY.md", { maxResults: 5 });
|
||||
await adapter.search("memory system", { maxResults: 5 });
|
||||
|
||||
const after = readFileSync(join(ROOT, "MEMORY.md"), "utf8");
|
||||
|
||||
assert.equal(after, before, "MEMORY.md must not be modified");
|
||||
});
|
||||
|
||||
// ─── Test 9 ──────────────────────────────────────────
|
||||
it("multiple queries are consistent", async () => {
|
||||
const r1 = await adapter.search("architecture", { maxResults: 5 });
|
||||
const r2 = await adapter.search("architecture", { maxResults: 5 });
|
||||
|
||||
assert.equal(r1.ok, r2.ok);
|
||||
assert.equal(r1.source, r2.source);
|
||||
assert.equal(r1.results.length, r2.results.length,
|
||||
"same query should return same count");
|
||||
|
||||
if (r1.results.length > 0 && r2.results.length > 0) {
|
||||
assert.equal(r1.results[0].id, r2.results[0].id,
|
||||
"same query should return same top result");
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 10 ─────────────────────────────────────────
|
||||
it("production check still passes", { timeout: 180000 }, () => {
|
||||
const result = spawnSync("bash", [join(ROOT, "scripts", "production-check.sh")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 170000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Production check failed (exit ${result.status})`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user