test/smart-agent-lsp-memory.test.js
Rama main · réplica de lectura del repositorio original.
"use strict";
const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const WorkspaceEvents = require("../lib/workspace-events");
const WorkspaceIndex = require("../lib/workspace-index");
const ContextEngine = require("../lib/context-engine");
const LspManager = require("../lib/lsp-manager");
const MemoryStore = require("../lib/memory-store");
const createMemoryTools = require("../lib/memory-tools");
const AiChat = require("../lib/ai-chat");
async function waitFor(check, timeoutMs) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const value = check();
if (value) return value;
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error("Timed out waiting for condition");
}
async function main() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "vrcloud-lsp-memory-"));
const workspace = path.join(root, "workspace");
const dataDir = path.join(root, "data", "context");
const source = path.join(workspace, "src", "app.ts");
fs.mkdirSync(path.dirname(source), { recursive: true });
fs.writeFileSync(source, "export function AppSymbol() { return 1; }\nAppSymbol();\nBROKEN\n");
const events = new WorkspaceEvents({ workspace, dedupeMs: 1 });
const index = new WorkspaceIndex({ workspace, dataDir, events });
await index.whenIdle();
const mockServer = path.join(__dirname, "fixtures", "mock-lsp-server.js");
const lsp = new LspManager({
workspace,
index,
events,
profiles: {
typescript: {
command: process.execPath,
args: [mockServer],
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
},
},
commandResolver: async (profile) => profile.command,
requestTimeoutMs: 2000,
backoffBaseMs: 20,
});
assert.strictEqual(await lsp.didOpen("src/app.ts", fs.readFileSync(source, "utf8"), 1), true);
await waitFor(() => lsp.diagnostics("src/app.ts").length, 2000);
assert.match(lsp.diagnostics("src/app.ts")[0].message, /version 1/);
const symbols = await lsp.workspaceSymbols("App", "typescript");
assert.strictEqual(symbols[0].name, "AppSymbol");
const definitions = await lsp.definitions("src/app.ts", { line: 1, character: 3 }, "AppSymbol");
const references = await lsp.references("src/app.ts", { line: 1, character: 3 }, { symbol: "AppSymbol" });
assert.strictEqual(definitions.length, 1);
assert.strictEqual(references.length, 1);
await lsp.didChange("src/app.ts", "export function AppSymbol() { return 2; }\nBROKEN\n", 2);
await waitFor(() => {
const list = lsp.diagnostics("src/app.ts");
return list.length && /version 2/.test(list[0].message);
}, 2000);
assert.strictEqual(lsp.status().servers.typescript.status, "running");
await lsp.restart("typescript");
assert.strictEqual(lsp.status().servers.typescript.status, "running");
await waitFor(() => lsp.diagnostics("src/app.ts").length, 2000);
const notifications = [];
const monotonic = new LspManager({
workspace,
profiles: { typescript: { command: "mock", args: [], extensions: [".ts"] } },
});
monotonic.start = async function (key) {
const state = this.state(key);
if (!state.connection) {
state.connection = {
closed: false,
notify: async (method, params) => { notifications.push({ method, params }); },
};
const doc = this.documents.get("src/app.ts");
if (doc) {
await state.connection.notify("textDocument/didOpen", {
textDocument: { uri: doc.uri, languageId: doc.languageId, version: doc.version, text: doc.text },
});
state.opened.add(doc.rel);
}
}
return state;
};
await monotonic.didOpen("src/app.ts", "first", 0);
await monotonic.didOpen("src/app.ts", "second", 0);
await monotonic.didChange("src/app.ts", "third", 1);
assert.deepStrictEqual(notifications.map((item) => item.method), [
"textDocument/didOpen", "textDocument/didChange", "textDocument/didChange",
]);
assert.deepStrictEqual(notifications.map((item) => item.params.textDocument.version), [1, 2, 3]);
monotonic.state("typescript").connection.closed = true;
await monotonic.close();
const unavailable = new LspManager({
workspace,
index,
profiles: { typescript: { command: "not-installed", args: [], extensions: [".ts"] } },
commandResolver: async () => "",
});
const lexical = await unavailable.workspaceSymbols("AppSymbol", "typescript");
assert.ok(lexical.some((item) => item.name === "AppSymbol"));
assert.strictEqual(unavailable.status().servers.typescript.status, "unavailable");
const memory = new MemoryStore({ workspace, dataDir, events });
await memory.whenIdle();
const stored = await memory.upsert({
kind: "architecture",
statement: "AppSymbol is the public TypeScript entry point",
scope: { component: "app" },
evidence: [{ path: "src/app.ts", range: { start: { line: 0, character: 0 }, end: { line: 0, character: 42 } } }],
provenance: { source: "test" },
confidence: 0.95,
ttlSeconds: 60,
});
assert.match(stored.id, /^mem-/);
assert.strictEqual(stored.status, "active");
assert.match(stored.evidence[0].hash, /^[a-f0-9]{64}$/);
assert.strictEqual((await memory.search("public TypeScript")).length, 1);
fs.writeFileSync(source, "export function AppSymbol() { return 3; }\nBROKEN\n");
await new Promise((resolve) => setTimeout(resolve, 5));
events.publish("test", ["src/app.ts"], { wildcard: false });
await memory.whenIdle();
await waitFor(() => lsp.diagnostics("src/app.ts").length, 2000);
assert.strictEqual((await memory.search("", { status: ["stale"] }))[0].id, stored.id);
const refreshed = await memory.revalidate(stored.id, { confidence: 0.9 });
assert.strictEqual(refreshed.status, "active");
assert.notStrictEqual(refreshed.evidence[0].hash, stored.evidence[0].hash);
const engine = new ContextEngine({
workspace, index, events, lspManager: lsp, memoryStore: memory, characterBudget: 2400,
});
await index.whenIdle();
const context = await engine.retrieve("AppSymbol TypeScript entry", {
active: ["src/app.ts"], characterBudget: 2400,
});
assert.ok(context.text.includes("[Active structured project memory]"));
assert.ok(context.text.includes("AppSymbol is the public TypeScript entry point"));
assert.ok(context.text.includes("[Cached language-server context]"));
assert.ok(context.text.includes("mock-lsp") || context.text.includes("AppSymbol"));
const tools = createMemoryTools(memory);
assert.strictEqual(tools.memory_search.annotations.readOnlyHint, true);
const ai = new AiChat({ workspace, customTools: tools });
ai.mode = "ask";
assert.deepStrictEqual(Object.keys(ai.customToolSet()), ["memory_search"]);
ai.mode = "agent";
assert.deepStrictEqual(Object.keys(ai.customToolSet()).sort(), [
"memory_forget", "memory_revalidate", "memory_search", "memory_upsert",
]);
const toolSearch = await tools.memory_search.execute({ query: "AppSymbol" });
assert.strictEqual(toolSearch.structuredContent.count, 1);
const forgotten = await memory.forget(stored.id);
assert.strictEqual(forgotten.status, "invalid");
assert.strictEqual((await memory.search("AppSymbol")).length, 0);
assert.ok(fs.existsSync(path.join(dataDir, WorkspaceIndex.workspaceHash(workspace), "memory.json")));
const reloaded = new MemoryStore({ workspace, dataDir });
await reloaded.whenIdle();
assert.strictEqual((await reloaded.search("", { status: ["invalid"] }))[0].id, stored.id);
if (ai.unsubscribeProjectContext) ai.unsubscribeProjectContext();
engine.close();
await memory.close();
await reloaded.close();
await lsp.didClose("src/app.ts");
await lsp.close();
await unavailable.close();
await index.close();
events.close();
fs.rmSync(root, { recursive: true, force: true });
console.log("smart-agent-lsp-memory: ok");
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});