Menu
popagent
publicLatest change 7fe5fe2acff014eed98cc59694a021837a6713c6 - Serialize code-intelligence sessions per repository and self-heal corrupt or locked indexes by AkurAI Build
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
CodeIntelligenceService,
OpenCodebaseIndexBackend,
type CodeIndexCall,
type CodeIndexResult,
type CodeIndexSession,
} from "./code-intelligence";
const temporaryPaths: string[] = [];
afterEach(async () => {
await Promise.all(temporaryPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })));
});
function fakeWorkspace(path: string) {
return {
resolveRepository: async (id: string) => ({
workspace: { id, name: id, repositoryPath: ".", createdAt: "", updatedAt: "" },
path,
}),
};
}
function recordingBackend(results: Record<string, string> = {}) {
const batches: Array<{ repositoryPath: string; calls: CodeIndexCall[] }> = [];
return {
batches,
backend: {
execute: async (repositoryPath: string, calls: CodeIndexCall[]): Promise<CodeIndexResult[]> => {
batches.push({ repositoryPath, calls });
return calls.map(({ name }) => ({ text: results[name] ?? `${name} evidence`, isError: false }));
},
},
};
}
describe("CodeIntelligenceService", () => {
test("uses the contained workspace and bounded context operation", async () => {
const { backend, batches } = recordingBackend({ codebase_context: "bounded context" });
const service = new CodeIntelligenceService(fakeWorkspace("/contained/repository"), backend);
await expect(service.codeContext("workspace-a", "browser host policy", 8)).resolves.toEqual({
evidence: "bounded context",
});
expect(batches).toEqual([{
repositoryPath: "/contained/repository",
calls: [{
name: "codebase_context",
arguments: { query: "browser host policy", limit: 8, tokenBudget: 4_000 },
}],
}]);
});
test("combines definition and direct relationship evidence for a symbol", async () => {
const { backend, batches } = recordingBackend({
implementation_lookup: "definition",
call_graph: "graph",
});
const service = new CodeIntelligenceService(fakeWorkspace("/contained/repository"), backend);
await expect(service.symbolContext("workspace-a", "RepositoryBriefService", "src/repo-brief.ts")).resolves.toEqual({
definition: "definition",
callers: "graph",
callees: "graph",
});
expect(batches[0]!.calls).toEqual([
{
name: "implementation_lookup",
arguments: { query: "RepositoryBriefService", limit: 5, directory: "src" },
},
{
name: "call_graph",
arguments: { name: "RepositoryBriefService", direction: "callers", filePath: "src/repo-brief.ts" },
},
{
name: "call_graph",
arguments: { name: "RepositoryBriefService", direction: "callees", filePath: "src/repo-brief.ts" },
},
]);
});
test("uses current changed paths when impact paths are omitted", async () => {
const { backend, batches } = recordingBackend({
codebase_edit_context: "file impact",
pr_impact: "branch impact",
});
const service = new CodeIntelligenceService(
fakeWorkspace("/contained/repository"),
backend,
async () => ["src/api-types.ts", "src/server.ts"],
);
await expect(service.changeImpact("workspace-a", [])).resolves.toEqual({
paths: ["src/api-types.ts", "src/server.ts"],
branchImpact: "branch impact",
files: [
{ path: "src/api-types.ts", evidence: "file impact" },
{ path: "src/server.ts", evidence: "file impact" },
],
unresolved: [],
});
expect(batches[0]!.calls).toEqual([
{
name: "pr_impact",
arguments: { maxDepth: 5, direction: "both", checkConflicts: false },
},
{
name: "codebase_edit_context",
arguments: {
query: "Assess the blast radius and affected tests for changes in src/api-types.ts.",
filePath: "src/api-types.ts",
callerLimit: 8,
calleeLimit: 8,
tokenBudget: 4_000,
},
},
{
name: "codebase_edit_context",
arguments: {
query: "Assess the blast radius and affected tests for changes in src/server.ts.",
filePath: "src/server.ts",
callerLimit: 8,
calleeLimit: 8,
tokenBudget: 4_000,
},
},
]);
});
});
describe("OpenCodebaseIndexBackend", () => {
test("refreshes the local index before queries and keeps index state out of source", async () => {
const repositoryPath = await mkdtemp(join(tmpdir(), "popagent-code-index-"));
temporaryPaths.push(repositoryPath);
await mkdir(join(repositoryPath, ".git", "info"), { recursive: true });
const calls: CodeIndexCall[] = [];
let closed = false;
const session: CodeIndexSession = {
call: async (call) => {
calls.push(call);
return { text: `${call.name} result`, isError: false };
},
close: async () => {
closed = true;
},
};
const dataRoot = await mkdtemp(join(tmpdir(), "popagent-code-index-data-"));
temporaryPaths.push(dataRoot);
const backend = new OpenCodebaseIndexBackend({
dataRoot,
embedding: {
baseUrl: "http://127.0.0.1:8081/v1",
model: "text-embedding-3-small",
dimensions: 1_024,
},
openSession: async () => session,
});
// Cold repository: the agent call answers immediately and starts the
// background warm-up instead of blocking on a full index build.
const cold = await backend.execute(repositoryPath, [{ name: "codebase_context", arguments: { query: "auth" } }]);
expect(cold).toHaveLength(1);
expect(cold[0]?.isError).toBe(true);
expect(cold[0]?.text).toContain("warm");
expect(backend.warmth(repositoryPath).state).toBe("warming");
await backend.warm(repositoryPath);
expect(backend.warmth(repositoryPath)).toEqual({ state: "ready" });
expect(calls).toEqual([
{ name: "index_codebase", arguments: { force: false, estimateOnly: false, verbose: false } },
]);
calls.length = 0;
await expect(backend.execute(repositoryPath, [{ name: "codebase_context", arguments: { query: "auth" } }]))
.resolves.toEqual([{ text: "codebase_context result", isError: false }]);
expect(calls).toEqual([
{ name: "index_codebase", arguments: { force: false, estimateOnly: false, verbose: false } },
{ name: "codebase_context", arguments: { query: "auth" } },
]);
expect(closed).toBe(true);
// Readiness survives a process restart through the on-disk marker.
const restarted = new OpenCodebaseIndexBackend({
dataRoot,
embedding: { baseUrl: "http://127.0.0.1:8081/v1", model: "text-embedding-3-small", dimensions: 1_024 },
openSession: async () => session,
});
calls.length = 0;
await expect(restarted.execute(repositoryPath, [{ name: "codebase_context", arguments: { query: "auth" } }]))
.resolves.toEqual([{ text: "codebase_context result", isError: false }]);
expect(calls.map((call) => call.name)).toEqual(["index_codebase", "codebase_context"]);
expect(await readFile(join(repositoryPath, ".git", "info", "exclude"), "utf8"))
.toContain("/.codebase-index/");
expect(await Bun.file(join(repositoryPath, ".gitignore")).exists()).toBe(false);
});
test("serializes sessions per repository and rebuilds a corrupt vector store instead of returning the raw mismatch", async () => {
const repositoryPath = await mkdtemp(join(tmpdir(), "popagent-code-index-"));
temporaryPaths.push(repositoryPath);
await mkdir(join(repositoryPath, ".git", "info"), { recursive: true });
await mkdir(join(repositoryPath, ".codebase-index", "index"), { recursive: true });
await Bun.write(join(repositoryPath, ".codebase-index", "index", "vectors"), "stale");
const dataRoot = await mkdtemp(join(tmpdir(), "popagent-code-index-data-"));
temporaryPaths.push(dataRoot);
let open = 0;
let peak = 0;
let corruptOnce = true;
const backend = new OpenCodebaseIndexBackend({
dataRoot,
embedding: { baseUrl: "http://127.0.0.1:8081/v1", model: "text-embedding-3-small", dimensions: 1_024 },
openSession: async () => {
open++;
peak = Math.max(peak, open);
return {
call: async (call) => {
await new Promise((resolve) => setTimeout(resolve, 5));
if (call.name === "change_impact" && corruptOnce) {
corruptOnce = false;
return { text: "Vector store structure mismatch: vectors=549, ids=548, keys=548, metadata=548", isError: true };
}
return { text: `${call.name} ok`, isError: false };
},
close: async () => { open--; },
};
},
});
await backend.warm(repositoryPath);
const [a, b, c] = await Promise.all([
backend.execute(repositoryPath, [{ name: "codebase_context", arguments: {} }]),
backend.execute(repositoryPath, [{ name: "change_impact", arguments: {} }]),
backend.execute(repositoryPath, [{ name: "codebase_context", arguments: {} }]),
]);
expect(peak).toBe(1);
expect(a).toEqual([{ text: "codebase_context ok", isError: false }]);
expect(b[0]?.isError).toBe(true);
expect(b[0]?.text).toContain("corrupt and is being rebuilt");
expect(b[0]?.text).not.toContain("structure mismatch");
// The corrupt store was discarded, and the marker dropped, so a rebuild follows.
expect(await Bun.file(join(repositoryPath, ".codebase-index", "index", "vectors")).exists()).toBe(false);
expect(c).toEqual([{ text: "codebase_context ok", isError: false }]);
await backend.warm(repositoryPath);
expect(backend.warmth(repositoryPath)).toEqual({ state: "ready" });
});
test("removes an indexing lock whose owner is dead on this host and retries the warm-up", async () => {
const repositoryPath = await mkdtemp(join(tmpdir(), "popagent-code-index-"));
temporaryPaths.push(repositoryPath);
await mkdir(join(repositoryPath, ".git", "info"), { recursive: true });
const lockDir = join(repositoryPath, ".codebase-index", "index", "indexing.lock");
await mkdir(lockDir, { recursive: true });
await Bun.write(join(lockDir, "owner.json"), JSON.stringify({ pid: 2_147_000_000, hostname: (await import("node:os")).hostname(), operation: "index", token: "t" }));
const dataRoot = await mkdtemp(join(tmpdir(), "popagent-code-index-data-"));
temporaryPaths.push(dataRoot);
let attempts = 0;
const backend = new OpenCodebaseIndexBackend({
dataRoot,
embedding: { baseUrl: "http://127.0.0.1:8081/v1", model: "text-embedding-3-small", dimensions: 1_024 },
openSession: async () => ({
call: async () => {
attempts++;
const locked = await Bun.file(join(lockDir, "owner.json")).exists();
return locked
? { text: "INDEX_BUSY: unreadable or remote lock owner (PID 2147000000, operation index). Automatic recovery was refused; manual verification is required.", isError: true }
: { text: "indexed", isError: false };
},
close: async () => undefined,
}),
});
await backend.warm(repositoryPath);
expect(attempts).toBe(2);
expect(await Bun.file(join(lockDir, "owner.json")).exists()).toBe(false);
expect(backend.warmth(repositoryPath)).toEqual({ state: "ready" });
});
test("allows embedding requests more time than a single contended batch takes", async () => {
const repositoryPath = await mkdtemp(join(tmpdir(), "popagent-code-index-"));
temporaryPaths.push(repositoryPath);
await mkdir(join(repositoryPath, ".git", "info"), { recursive: true });
const dataRoot = await mkdtemp(join(tmpdir(), "popagent-code-index-data-"));
temporaryPaths.push(dataRoot);
let configPath = "";
const backend = new OpenCodebaseIndexBackend({
dataRoot,
embedding: {
baseUrl: "http://127.0.0.1:8081/v1",
model: "text-embedding-3-small",
dimensions: 1_024,
},
openSession: async (input) => {
configPath = input.configPath;
return {
call: async () => ({ text: "", isError: false }),
close: async () => undefined,
};
},
});
await backend.warm(repositoryPath);
const config = JSON.parse(await readFile(configPath, "utf8")) as {
customProvider: { timeoutMs: number };
};
// A 30s budget expired under normal GPU contention on the LAN embedding
// service and failed every batch, which left the index empty.
expect(config.customProvider.timeoutMs).toBeGreaterThanOrEqual(120_000);
});
});