AkurAI Build
Menu

popagent

public

Latest 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);
  });
});