"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 AiChat = require("../lib/ai-chat"); async function main() { const root = fs.mkdtempSync(path.join(os.tmpdir(), "vrcloud-context-")); const workspace = path.join(root, "workspace"); const dataDir = path.join(root, "data", "context"); fs.mkdirSync(path.join(workspace, "src"), { recursive: true }); fs.mkdirSync(path.join(workspace, "test"), { recursive: true }); fs.mkdirSync(path.join(workspace, "docs"), { recursive: true }); fs.writeFileSync(path.join(workspace, "src", "active.js"), "const sharedNeedle = require('./open');\nfunction activeSymbol() { return sharedNeedle; }\nmodule.exports = activeSymbol;\n"); fs.writeFileSync(path.join(workspace, "src", "open.js"), "function openSymbol() { return 'sharedNeedle'; }\nmodule.exports = openSymbol;\n"); fs.writeFileSync(path.join(workspace, "src", "explicit.js"), "export function explicitSymbol() { return 'sharedNeedle'; }\n"); fs.writeFileSync(path.join(workspace, "test", "active.test.js"), "const test = require('node:test');\ntest('active', () => {});\n"); fs.writeFileSync(path.join(workspace, "docs", "notes.weird"), "unsupportedLanguageToken lives here\nsecond line\n"); const events = new WorkspaceEvents({ workspace, dedupeMs: 50 }); const seen = []; events.onChange((event) => seen.push(event)); const first = events.publish("editor", ["src\\active.js", "src/active.js"], { wildcard: false }); const duplicate = events.publish("disk", ["src/active.js"], { wildcard: false }); const wildcard = events.publish("shell", [], {}); assert.ok(first); assert.strictEqual(duplicate, null); assert.deepStrictEqual(first.changes, ["src/active.js"]); assert.strictEqual(first.seq, 1); assert.strictEqual(typeof first.at, "number"); assert.strictEqual(first.source, "editor"); assert.strictEqual(first.wildcard, false); assert.strictEqual(wildcard.wildcard, true); assert.strictEqual(seen.length, 2); await new Promise((resolve) => setTimeout(resolve, 70)); assert.strictEqual(seen.length, 3, "a deduped edit must receive a trailing flush"); assert.strictEqual(seen[2].source, "disk"); assert.deepStrictEqual(seen[2].changes, ["src/active.js"]); const index = new WorkspaceIndex({ workspace, dataDir, events }); await index.whenIdle(); const manifest = index.manifest(); const active = manifest.files["src/active.js"]; assert.ok(active); assert.strictEqual(active.language, "javascript"); assert.match(active.hash, /^[a-f0-9]{64}$/); assert.strictEqual(typeof active.mtime, "number"); assert.strictEqual(typeof active.size, "number"); assert.ok(active.symbols.includes("activeSymbol")); assert.ok(active.imports.includes("./open")); assert.ok(manifest.files["test/active.test.js"].testHints.length > 0); assert.strictEqual(manifest.files["docs/notes.weird"].language, "plaintext"); assert.ok(fs.existsSync(path.join(dataDir, WorkspaceIndex.workspaceHash(workspace), "index.json"))); await new Promise((resolve) => setTimeout(resolve, 60)); fs.writeFileSync(path.join(workspace, "src", "active.js"), "const rapidFirst = true;\nfunction activeSymbol() { return rapidFirst; }\n"); events.publish("editor", ["src/active.js"], { wildcard: false }); fs.writeFileSync(path.join(workspace, "src", "active.js"), "const rapidFinal = true;\nfunction activeSymbol() { return rapidFinal; }\n"); events.publish("editor", ["src/active.js"], { wildcard: false }); await new Promise((resolve) => setTimeout(resolve, 70)); await index.whenIdle(); assert.ok(index.manifest().files["src/active.js"].terms.rapidfinal, "the trailing event must index the final rapid edit"); const engine = new ContextEngine({ workspace, index, events, characterBudget: 1400 }); const ranked = await engine.retrieve("sharedNeedle in src/explicit.js", { active: ["src/active.js"], open: ["src/open.js"], characterBudget: 1400, }); assert.deepStrictEqual(ranked.files.slice(0, 3).map((file) => file.path), [ "src/active.js", "src/open.js", "src/explicit.js", ]); assert.ok(ranked.characterCount <= 1400); const fallback = await engine.retrieve("unsupportedLanguageToken", { characterBudget: 500 }); assert.strictEqual(fallback.files[0].path, "docs/notes.weird"); assert.ok(fallback.text.includes("unsupportedLanguageToken")); const readOnly = await engine.retrieve("activeSymbol", { active: ["src/active.js"], readOnly: true, characterBudget: 600, }); assert.ok(readOnly.text.startsWith("[Read-only workspace context")); assert.ok(readOnly.characterCount <= 600); const cached = await engine.retrieve("activeSymbol", { active: ["src/active.js"], readOnly: true, characterBudget: 600, }); assert.strictEqual(cached, readOnly); let releaseMemory; let memorySettled = false; const delayedMemory = { revision: 0, ready: new Promise((resolve) => { releaseMemory = resolve; }), cachedSearch: () => [{ id: "ready-memory", kind: "fact", statement: "memory loaded before context caching", evidence: [], }], }; const memoryEngine = new ContextEngine({ workspace, index, memoryStore: delayedMemory, characterBudget: 800, }); const memoryPending = memoryEngine.retrieve("memory loaded", { characterBudget: 800 }) .then((result) => { memorySettled = true; return result; }); await new Promise((resolve) => setImmediate(resolve)); assert.strictEqual(memorySettled, false, "context retrieval must await memory readiness"); delayedMemory.revision = 1; releaseMemory(); const memoryContext = await memoryPending; assert.ok(memoryContext.text.includes("memory loaded before context caching")); const ai = new AiChat({ workspace, contextEngine: engine, workspaceEvents: events, openFiles: () => ({ active: "src/active.js", open: ["src/active.js"] }), }); ai.mode = "ask"; const askPrompt = await ai.agentPrompt("Where is activeSymbol defined?", ""); assert.ok(askPrompt.includes("ASK MODE (read-only)")); assert.ok(askPrompt.includes("[Read-only workspace context")); assert.ok(askPrompt.includes("src/active.js")); await new Promise((resolve) => setTimeout(resolve, 60)); fs.writeFileSync(path.join(workspace, "src", "active.js"), "const changedToken = true;\nfunction activeSymbol() { return changedToken; }\n"); events.publish("editor", ["src/active.js"], { wildcard: false }); await index.whenIdle(); const updated = index.manifest().files["src/active.js"]; assert.notStrictEqual(updated.hash, active.hash); assert.ok(updated.terms.changedtoken); const refreshed = await engine.retrieve("changedToken", { active: ["src/active.js"], readOnly: true, characterBudget: 600, }); assert.notStrictEqual(refreshed, cached); assert.ok(refreshed.text.includes("changedToken")); fs.unlinkSync(path.join(workspace, "src", "open.js")); events.publish("delete", ["src/open.js"], { wildcard: false }); await index.whenIdle(); assert.strictEqual(index.manifest().files["src/open.js"], undefined); const reloaded = new WorkspaceIndex({ workspace, dataDir }); await reloaded.whenIdle(); assert.ok(reloaded.manifest().files["src/active.js"]); assert.strictEqual(reloaded.manifest().files["src/open.js"], undefined); await reloaded.close(); if (ai.unsubscribeProjectContext) ai.unsubscribeProjectContext(); memoryEngine.close(); engine.close(); await index.close(); events.close(); fs.rmSync(root, { recursive: true, force: true }); console.log("smart-agent-context: ok"); } main().catch((error) => { console.error(error.stack || error); process.exitCode = 1; });