Ctrl K
ComunidadGuías…

test/smart-agent-lsp-memory.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"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;
});