AkurAI Build
Menu

popagent

public

Latest change 21a0d10510ee6ff34208eeed9c7298291f9874f6 - Allow queued local chat verification by AkurAI Build

import { afterAll, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import {
  FREE_MODEL_ROUTE,
  MAX_AGENT_INSTRUCTIONS_CHARACTERS,
  type AgentListResponse,
  type AgentRuntimeSettings,
  type AutonomySettings,
  type EvolutionSignal,
  type BrowserSettings,
  type ModelCatalogResponse,
  type SessionListResponse,
  type WorkspaceListResponse,
} from "./api-types";
import { WORKSPACE_ROOT } from "./agent-workspaces";
import { SERVER_IDLE_TIMEOUT_SECONDS, startServer, sessions, taskStore } from "./server";
import { evolutionStore } from "./evolution-store";
import { HookRuntime, type HookEvent } from "./hooks";
import { hookAudit } from "./hook-audit";
import { longTermMemory } from "./long-term-memory";

const testSessions = sessions;
const server = startServer(0, testSessions);
const base = `http://127.0.0.1:${server.port}`;
async function waitForEvolutionSignal(sessionId: string, kind: "turn-success" | "turn-failure") {
  // Integration boundary: AI SDK invokes stream finalization asynchronously, so await the persisted row.
  for (let attempt = 0; attempt < 100; attempt++) {
    const signal = await evolutionStore.storage.db.oneOrNone<EvolutionSignal>(`
      SELECT id, session_id AS "sessionId", turn_id AS "turnId", trace_id AS "traceId",
             agent_id AS "agentId", workspace_id AS "workspaceId", kind, summary, status,
             attempts, next_attempt_at::text AS "nextAttemptAt", error,
             created_at::text AS "createdAt", updated_at::text AS "updatedAt",
             processed_at::text AS "processedAt"
      FROM popagent_evolution_signals
      WHERE session_id=$1 AND kind=$2
      ORDER BY created_at DESC LIMIT 1
    `, [sessionId, kind]);
    if (signal) return signal;
    await Bun.sleep(10);
  }
  return undefined;
}

const hookEvents: HookEvent[] = [];
const hookRuntime = new HookRuntime({
  config: {
    version: 1,
    hooks: {
      SessionStart: [{ id: "session", type: "http", url: "https://hooks.example/session" }],
      UserPromptSubmit: [{ id: "prompt", type: "http", url: "https://hooks.example/prompt" }],
    },
  },
  transport: async (_handler, event) => {
    hookEvents.push(event);
    return event.eventName === "UserPromptSubmit"
      ? { schemaVersion: 1, outcome: "deny", reason: "test policy denied prompt" }
      : { schemaVersion: 1, outcome: "pass" };
  },
});
const hookedServer = startServer(0, testSessions, hookRuntime);
const hookedBase = `http://127.0.0.1:${hookedServer.port}`;
const cleanupSessionIds = new Set<string>();
const cleanupThreadIds = new Set<string>();
const cleanupMemoryKeys = new Set<string>();
const cleanupSkillIds = new Set<string>();

async function cleanupThread(id: string) {
  await testSessions.memory.deleteThread(id).catch(() => undefined);
  await testSessions.storage.db.none("DELETE FROM mastra_thread_state WHERE \"threadId\" = $1", [id]);
  await longTermMemory.storage.db.none("DELETE FROM popagent_memories WHERE session_id = $1", [id]);
  await hookAudit.deleteForSession(id);
}

afterAll(async () => {
  for (const id of cleanupSessionIds) await testSessions.delete(id);
  for (const id of cleanupThreadIds) await cleanupThread(id);
  for (const key of cleanupMemoryKeys) {
    await longTermMemory.storage.db.none(
      "DELETE FROM popagent_memories WHERE resource_id = $1 AND memory_key = $2",
      ["popagent-user", key],
    );
  }
  for (const id of cleanupSkillIds) {
    await fetch(`${base}/api/agents/orchistrator/skills/${id}`, { method: "DELETE" });
  }
  server.stop(true);
  hookedServer.stop(true);
});

function joinDeltas(sse: string): string {
  return [...sse.matchAll(/"type":"text-delta".*?"delta":"((?:[^"\\]|\\.)*)"/g)]
    .map((m) => JSON.parse(`"${m[1]}"`))
    .join("");
}

function chatBody(model?: string, id?: string) {
  return JSON.stringify({
    ...(id ? { id } : {}),
    ...(model ? { model } : {}),
    messages: [
      {
        id: "1",
        role: "user",
        parts: [{ type: "text", text: "Reply with exactly the word: pong" }],
      },
    ],
  });
}

describe("browser application routes", () => {
  test("allows slow models to produce their first stream event", () => {
    expect(SERVER_IDLE_TIMEOUT_SECONDS).toBe(255);
  });

  test("serves the application shell for settings deep links", async () => {
    const response = await fetch(`${base}/settings/browser`);
    expect(response.status).toBe(200);
    expect(response.headers.get("content-type")).toContain("text/html");
    expect(await response.text()).toContain("<title>popagent</title>");
  });


  test("keeps unmatched API routes out of the application shell", async () => {
    const response = await fetch(`${base}/api/not-a-route`);
    expect(response.status).toBe(404);
    expect(await response.json()).toEqual({ error: "Not found" });
  });
});

describe("GET /api/health", () => {
  test("reports application and observability readiness", async () => {
    const response = await fetch(`${base}/api/health`);
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({
      status: "ok",
      applicationDatabase: "ok",
      observabilityDatabase: "ok",
    });
  });
});

describe("GET /api/models", () => {
  test("lists selectable models", async () => {
    const res = await fetch(`${base}/api/models`);
    expect(res.status).toBe(200);
    const { models, defaultModel, contextWindows } = (await res.json()) as ModelCatalogResponse;
    expect(defaultModel.length).toBeGreaterThan(0);
    expect(models).toContain(defaultModel);
    expect(contextWindows[defaultModel]).toBeGreaterThan(0);
  });
});
describe("/api/agents", () => {
  test("lists agents and persists complete identity edits", async () => {
    const listResponse = await fetch(`${base}/api/agents`);
    expect(listResponse.status).toBe(200);
    const listed = (await listResponse.json()) as AgentListResponse;
    const original = listed.agents.find((item) => item.id === "orchistrator");
    if (!original) throw new Error("orchistrator settings missing");

    const input = {
      name: `API agent ${crypto.randomUUID().slice(0, 8)}`,
      description: "API-managed supervisor",
      instructions: `API test ${crypto.randomUUID()}`,
      workspaceAccess: original.workspaceAccess,
      browserAccess: original.browserAccess,
      delegationEnabled: original.delegationEnabled,
      tools: original.tools,
      model: original.model,
    };
    try {
      const updateResponse = await fetch(`${base}/api/agents/orchistrator`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
      expect(updateResponse.status).toBe(200);
      expect(await updateResponse.json()).toMatchObject(input);
    } finally {
      await fetch(`${base}/api/agents/orchistrator`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          name: original.name,
          description: original.description,
          workspaceAccess: original.workspaceAccess,
          browserAccess: original.browserAccess,
          delegationEnabled: original.delegationEnabled,
          tools: original.tools,
          model: original.model,
          instructions: original.instructions,
        }),
      });
    }
  });
  test("exposes governed Build roles and rejects protected tools for community", async () => {
    const listed = (await (await fetch(`${base}/api/agents`)).json()) as AgentListResponse;
    const community = listed.agents.find((item) => item.id === "community-steward");
    const maintainer = listed.agents.find((item) => item.id === "build-maintainer");
    if (!community || !maintainer) throw new Error("Build agent settings missing");
    expect(community.workspaceAccess).toBe("none");
    expect(community.browserAccess).toBe("none");
    expect(community.tools).toContain("akuraiIssueComment");
    expect(community.tools).not.toContain("akuraiRunPromote");
    expect(maintainer.tools).toContain("akuraiPipelineValidate");
    const response = await fetch(`${base}/api/agents/community-steward`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        name: community.name,
        description: community.description,
        instructions: community.instructions,
        model: community.model,
        workspaceAccess: community.workspaceAccess,
        browserAccess: community.browserAccess,
        delegationEnabled: community.delegationEnabled,
        tools: [...community.tools, "akuraiRunPromote"],
      }),
    });
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual(expect.objectContaining({
      error: "Build agent tools exceed its role allowlist",
    }));
  });


  test("rejects invalid complete identity payloads", async () => {
    const oversized = await fetch(`${base}/api/agents/orchistrator`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        name: "orchistrator",
        description: "Supervisor",
        instructions: "x".repeat(MAX_AGENT_INSTRUCTIONS_CHARACTERS + 1),
        workspaceAccess: "read-write",
        model: null,
        browserAccess: "interactive",
        delegationEnabled: false,
        tools: ["webSearch"],
      }),
    });
    expect(oversized.status).toBe(400);
    expect(await oversized.json()).toEqual(expect.objectContaining({
      error: "Invalid agent settings",
      issues: expect.any(Array),
    }));

    const incomplete = await fetch(`${base}/api/agents/orchistrator`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ instructions: "instructions only" }),
    });
    expect(incomplete.status).toBe(400);
  });

  test("rejects an unknown agent model", async () => {
    const listed = (await (await fetch(`${base}/api/agents`)).json()) as AgentListResponse;
    const original = listed.agents.find((item) => item.id === "researcher");
    if (!original) throw new Error("researcher settings missing");
    const { id: _id, sourceUrls: _sourceUrls, createdAt: _createdAt, updatedAt: _updatedAt, ...input } = original;
    const response = await fetch(`${base}/api/agents/researcher`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ ...input, model: "missing/model" }),
    });
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "Unknown model: missing/model" });
  });
});
describe("/api/settings/browser", () => {
  test("persists browser enablement", async () => {
    const getResponse = await fetch(`${base}/api/settings/browser`);
    expect(getResponse.status).toBe(200);
    const original = (await getResponse.json()) as BrowserSettings;
    const { updatedAt: _updatedAt, ...originalInput } = original;
    try {
      const updateResponse = await fetch(`${base}/api/settings/browser`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...originalInput, enabled: !original.enabled }),
      });
      expect(updateResponse.status).toBe(200);
      expect(((await updateResponse.json()) as { enabled: boolean }).enabled).toBe(!original.enabled);
    } finally {
      await fetch(`${base}/api/settings/browser`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(originalInput),
      });
    }
  });
});

describe("/api/settings/memory", () => {
  test("validates and persists conversation compaction settings", async () => {
    const getResponse = await fetch(`${base}/api/settings/memory`);
    expect(getResponse.status).toBe(200);
    const original = await getResponse.json() as Record<string, unknown>;
    const input = {
      autoCompact: false,
      observationTokens: 24_000,
      reflectionTokens: 48_000,
      recentMessagePercent: 25,
      asyncBuffering: false,
      bufferIntervalPercent: 25,
      bufferOnIdle: false,
      observationBlockPercent: 130,
      reflectionBufferPercent: 60,
      reflectionBlockPercent: 140,
      optimizeObserverContext: true,
      previousObserverTokens: 3_000,
      retrievalEnabled: true,
      retrievalScope: "thread",
      temporalMarkers: true,
      activateAfterIdle: "5m",
      activateOnProviderChange: true,
      shareTokenBudget: true,
      observeAttachments: "auto",
      observationInstruction: "Keep exact project decisions.",
      reflectionInstruction: "Preserve dates and unresolved blockers.",
      internalRecall: false,
      internalRetention: false,
    };
    try {
      const updateResponse = await fetch(`${base}/api/settings/memory`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
      expect(updateResponse.status).toBe(200);
      expect(await updateResponse.json()).toMatchObject(input);
      expect((await fetch(`${base}/api/settings/memory`)).status).toBe(200);
      expect((await fetch(`${base}/api/settings/memory`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...input, observationTokens: 100 }),
      })).status).toBe(400);
    } finally {
      await fetch(`${base}/api/settings/memory`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(original),
      });
    }
  });
});
describe("/api/settings/agent-runtime", () => {
  test("validates and persists execution settings", async () => {
    const getResponse = await fetch(`${base}/api/settings/agent-runtime`);
    expect(getResponse.status).toBe(200);
    const original = await getResponse.json() as AgentRuntimeSettings;
    const { updatedAt, ...originalInput } = original;
    void updatedAt;
    const input = {
      ...originalInput,
      supervisorMaxSteps: original.supervisorMaxSteps === 23 ? 24 : 23,
      toolConcurrency: original.toolConcurrency === 2 ? 3 : 2,
    };
    try {
      const updateResponse = await fetch(`${base}/api/settings/agent-runtime`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
      expect(updateResponse.status).toBe(200);
      expect(await updateResponse.json()).toMatchObject(input);
      expect(await (await fetch(`${base}/api/settings/agent-runtime`)).json()).toMatchObject(input);

      const freeResponse = await fetch(`${base}/api/settings/agent-runtime`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...input, modelSource: "openrouter-free" }),
      });
      expect(freeResponse.status).toBe(200);
      expect(await freeResponse.json()).toMatchObject({
        modelSource: "openrouter-free",
        defaultModel: FREE_MODEL_ROUTE,
      });

      const invalidResponse = await fetch(`${base}/api/settings/agent-runtime`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...input, supervisorMaxSteps: 0 }),
      });
      expect(invalidResponse.status).toBe(400);
    } finally {
      await fetch(`${base}/api/settings/agent-runtime`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(originalInput),
      });
    }
  });
});
describe("/api/autonomy", () => {
  test("validates and persists autonomy settings without accepting extra controls", async () => {
    const getResponse = await fetch(`${base}/api/settings/autonomy`);
    expect(getResponse.status).toBe(200);
    const original = await getResponse.json() as AutonomySettings;
    const { updatedAt, ...originalInput } = original;
    void updatedAt;
    const input = {
      ...originalInput,
      enabled: !original.enabled,
      autoRetainFacts: !original.autoRetainFacts,
      idleImprovementEnabled: !original.idleImprovementEnabled,
      idleWorkspaceIds: [],
    };
    try {
      const updateResponse = await fetch(`${base}/api/settings/autonomy`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
      expect(updateResponse.status).toBe(200);
      expect(await updateResponse.json()).toMatchObject(input);

      for (const invalid of [
        { ...input, reflectionIntervalMs: 59_999 },
        { ...input, batchSize: 101 },
        { ...input, maxAttempts: 0 },
        { ...input, selfUpdateCron: "not a cron" },
        { ...input, capabilityMembership: [] },
      ]) {
        const response = await fetch(`${base}/api/settings/autonomy`, {
          method: "PATCH",
          headers: { "content-type": "application/json" },
          body: JSON.stringify(invalid),
        });
        expect(response.status).toBe(400);
      }
    } finally {
      await fetch(`${base}/api/settings/autonomy`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(originalInput),
      });
    }
  });

  test("reports continuous automation state", async () => {
    const response = await fetch(`${base}/api/autonomy/status`);
    expect(response.status).toBe(200);
    expect(await response.json()).toMatchObject({
      state: expect.stringMatching(/^(paused|active|improving)$/),
      activeTask: null,
      selectedWorkspaceIds: expect.any(Array),
    });
  });

  test("reads, ignores, and retries evolution signals with strict state transitions", async () => {
    const pending = await evolutionStore.enqueueFailedTurn({
      sessionId: `signal-api-${crypto.randomUUID()}`,
      agentId: "orchistrator",
      summary: "Signal API contract",
    });
    try {
      const signalsResponse = await fetch(`${base}/api/autonomy/signals?limit=5`);
      const revisionsResponse = await fetch(`${base}/api/autonomy/revisions?limit=5`);
      expect(signalsResponse.status).toBe(200);
      expect((await signalsResponse.json()) as unknown).toEqual({ signals: expect.any(Array) });
      expect(revisionsResponse.status).toBe(200);
      expect((await revisionsResponse.json()) as unknown).toEqual({ revisions: expect.any(Array) });

      const ignoredResponse = await fetch(`${base}/api/autonomy/signals/${pending.id}/ignore`, { method: "POST" });
      expect(ignoredResponse.status).toBe(200);
      expect(await ignoredResponse.json()).toMatchObject({ id: pending.id, status: "ignored", error: null });
      expect((await fetch(`${base}/api/autonomy/signals/${pending.id}/ignore`, { method: "POST" })).status).toBe(409);

      await evolutionStore.storage.db.none(`
        UPDATE popagent_evolution_signals
        SET status='dead-letter', attempts=2, next_attempt_at=NULL, processed_at=NOW(), error='test failure'
        WHERE id=$1
      `, [pending.id]);
      const retriedResponse = await fetch(`${base}/api/autonomy/signals/${pending.id}/retry`, { method: "POST" });
      expect(retriedResponse.status).toBe(200);
      expect(await retriedResponse.json()).toMatchObject({ id: pending.id, status: "pending", attempts: 0, error: null });
      expect((await fetch(`${base}/api/autonomy/signals/${pending.id}/retry`, { method: "POST" })).status).toBe(409);

      for (const query of ["limit=0", "limit=201", "limit=1.5", "limit=nope", "limit=5&offset=1"]) {
        expect((await fetch(`${base}/api/autonomy/signals?${query}`)).status).toBe(400);
        expect((await fetch(`${base}/api/autonomy/revisions?${query}`)).status).toBe(400);
      }
      expect((await fetch(`${base}/api/autonomy/signals/not-a-uuid/retry`, { method: "POST" })).status).toBe(400);
      expect((await fetch(`${base}/api/autonomy/signals/${crypto.randomUUID()}/retry`, { method: "POST" })).status).toBe(404);

      const unknown = await fetch(`${base}/api/autonomy/revisions/${crypto.randomUUID()}/revert`, { method: "POST" });
      expect(unknown.status).toBe(404);
      const malformed = await fetch(`${base}/api/autonomy/revisions/not-a-uuid/revert`, { method: "POST" });
      expect(malformed.status).toBe(400);
    } finally {
      await evolutionStore.storage.db.none("DELETE FROM popagent_evolution_signals WHERE id=$1", [pending.id]);
    }
  });
});
describe("/api/agents/:id/skills", () => {
  test("creates, updates, lists, and deletes a skill", async () => {
    const name = `api-skill-${crypto.randomUUID().slice(0, 8)}`;
    const input = { name, description: "API skill test", instructions: "Use API_SKILL_READY.", references: {}, enabled: true, userInvocable: true };
    const createdResponse = await fetch(`${base}/api/agents/orchistrator/skills`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    });
    expect(createdResponse.status).toBe(201);
    const created = (await createdResponse.json()) as { id: string; evolutionManaged: boolean };
    expect(created.evolutionManaged).toBe(false);
    cleanupSkillIds.add(created.id);
    const listResponse = await fetch(`${base}/api/agents/orchistrator/skills`);
    expect(((await listResponse.json()) as { skills: Array<{ name: string }> }).skills.map((skill) => skill.name)).toContain(name);
    const updateResponse = await fetch(`${base}/api/agents/orchistrator/skills/${created.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ ...input, enabled: false }),
    });
    expect(await updateResponse.json()).toEqual(expect.objectContaining({
      enabled: false,
      evolutionManaged: false,
    }));
    expect((await fetch(`${base}/api/agents/orchistrator/skills/${created.id}`, { method: "DELETE" })).status).toBe(204);
    cleanupSkillIds.delete(created.id);
  });
});
describe("/api/workspaces", () => {
  test("creates a repository workspace and scopes sessions to it", async () => {
    const repositoryPath = `api-workspace-${crypto.randomUUID()}`;
    await mkdir(resolve(WORKSPACE_ROOT, repositoryPath), { recursive: true });
    let workspaceId: string | undefined;
    let sessionId: string | undefined;
    try {
      const createdResponse = await fetch(`${base}/api/workspaces`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ name: "API workspace", repositoryPath }),
      });
      expect(createdResponse.status).toBe(201);
      workspaceId = ((await createdResponse.json()) as { id: string }).id;

      const listedResponse = await fetch(`${base}/api/workspaces`);
      expect(listedResponse.status).toBe(200);
      expect(((await listedResponse.json()) as WorkspaceListResponse).workspaces).toContainEqual(
        expect.objectContaining({ id: workspaceId, repositoryPath }),
      );

      const sessionResponse = await fetch(`${base}/api/sessions`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ workspaceId }),
      });
      expect(sessionResponse.status).toBe(201);
      sessionId = ((await sessionResponse.json()) as { id: string; workspaceId: string }).id;

      const sessionsResponse = await fetch(`${base}/api/sessions?workspaceId=${workspaceId}`);
      expect(sessionsResponse.status).toBe(200);
      expect(((await sessionsResponse.json()) as SessionListResponse).sessions).toContainEqual(
        expect.objectContaining({ id: sessionId, workspaceId }),
      );

      expect((await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" })).status).toBe(409);
      expect((await fetch(`${base}/api/sessions/${sessionId}`, { method: "DELETE" })).status).toBe(204);
      sessionId = undefined;
      expect((await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" })).status).toBe(204);
      workspaceId = undefined;
    } finally {
      if (sessionId) await fetch(`${base}/api/sessions/${sessionId}`, { method: "DELETE" });
      if (workspaceId) await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" });
      await rm(resolve(WORKSPACE_ROOT, repositoryPath), { recursive: true, force: true });
    }
  });

  test("returns deterministic repository evidence for a workspace", async () => {
    const repositoryPath = `repo-brief-${crypto.randomUUID()}`;
    const repository = resolve(WORKSPACE_ROOT, repositoryPath);
    await mkdir(join(repository, "src"), { recursive: true });
    await writeFile(join(repository, "src", "payment.ts"), "export function processPayment() { return true; }\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "fixture"],
    ]) {
      const process = Bun.spawn(["git", "-C", repository, ...args], { stdout: "pipe", stderr: "pipe" });
      expect(await process.exited).toBe(0);
    }
    let workspaceId: string | undefined;
    try {
      const createdResponse = await fetch(`${base}/api/workspaces`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ name: "Repository brief workspace", repositoryPath }),
      });
      workspaceId = ((await createdResponse.json()) as { id: string }).id;

      const response = await fetch(`${base}/api/workspaces/${workspaceId}/repo-brief?q=fix%20payment`);

      expect(response.status).toBe(200);
      expect(await response.json()).toEqual(expect.objectContaining({
        repository: { clean: true, changedFiles: [] },
        candidates: [expect.objectContaining({ path: "src/payment.ts" })],
      }));
    } finally {
      if (workspaceId) await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" });
      await rm(repository, { recursive: true, force: true });
    }
  });

  test("permanently deletes every active and archived chat in one workspace", async () => {
    const repositoryPath = `bulk-delete-${crypto.randomUUID()}`;
    await mkdir(resolve(WORKSPACE_ROOT, repositoryPath), { recursive: true });
    let workspaceId: string | undefined;
    try {
      const workspaceResponse = await fetch(`${base}/api/workspaces`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ name: "Bulk delete workspace", repositoryPath }),
      });
      workspaceId = ((await workspaceResponse.json()) as { id: string }).id;
      const createSession = async () => {
        const response = await fetch(`${base}/api/sessions`, {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ workspaceId }),
        });
        return ((await response.json()) as { id: string }).id;
      };
      const activeId = await createSession();
      const archivedId = await createSession();
      expect((await fetch(`${base}/api/sessions/${archivedId}`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ archived: true }),
      })).status).toBe(200);

      const response = await fetch(`${base}/api/workspaces/${workspaceId}/sessions`, { method: "DELETE" });
      expect(response.status).toBe(200);
      expect(await response.json()).toEqual({ deleted: 2 });
      expect((await fetch(`${base}/api/sessions/${activeId}`)).status).toBe(404);
      expect((await fetch(`${base}/api/sessions/${archivedId}`)).status).toBe(404);
      expect((await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" })).status).toBe(204);
      workspaceId = undefined;
    } finally {
      if (workspaceId) {
        await fetch(`${base}/api/workspaces/${workspaceId}/sessions`, { method: "DELETE" });
        await fetch(`${base}/api/workspaces/${workspaceId}`, { method: "DELETE" });
      }
      await rm(resolve(WORKSPACE_ROOT, repositoryPath), { recursive: true, force: true });
    }
  });
});

describe("/api/sessions", () => {
  test("persists a session and lists its generated title", async () => {
    const createdResponse = await fetch(`${base}/api/sessions`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ model: "cx/gpt-5.6-sol" }),
    });
    expect(createdResponse.status).toBe(201);
    const created = (await createdResponse.json()) as { id: string; revision: number };
    cleanupSessionIds.add(created.id);

    const messages = [
      {
        id: "user-1",
        role: "user",
        parts: [{ type: "text", text: "Remember this conversation" }],
      },
    ];
    const savedResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ model: "cx/gpt-5.6-sol", messages, revision: created.revision }),
    });
    expect(savedResponse.status).toBe(200);

    const staleResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ model: "cx/gpt-5.6-sol", messages, revision: created.revision }),
    });
    expect(staleResponse.status).toBe(409);
    const conflict = (await staleResponse.json()) as { error: string; session: { revision: number } };
    expect(conflict.error).toBe("revision conflict");
    expect(conflict.session.revision).toBeGreaterThan(created.revision);

    const invalidResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: "not json",
    });
    expect(invalidResponse.status).toBe(400);

    const listResponse = await fetch(`${base}/api/sessions`);
    const { sessions: listed } = (await listResponse.json()) as SessionListResponse;
    expect(listed[0]).toEqual(
      expect.objectContaining({ id: created.id, title: "Remember this conversation" }),
    );

    const loadedResponse = await fetch(`${base}/api/sessions/${created.id}`);
    expect(await loadedResponse.json()).toEqual(expect.objectContaining({ messages }));
    expect((await fetch(`${base}/api/sessions/${created.id}`, { method: "DELETE" })).status).toBe(204);
    cleanupSessionIds.delete(created.id);
  });
});

describe("session lifecycle", () => {
  test("renames, archives, restores, and permanently deletes a chat", async () => {
    const createdResponse = await fetch(`${base}/api/sessions`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ model: "cx/gpt-5.6-sol" }),
    });
    const created = (await createdResponse.json()) as { id: string };
    cleanupSessionIds.add(created.id);
    const renamedResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ title: "Pinned title" }),
    });
    expect(renamedResponse.status).toBe(200);
    expect(await renamedResponse.json()).toEqual(expect.objectContaining({ title: "Pinned title" }));
    const archivedResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ archived: true }),
    });
    expect(archivedResponse.status).toBe(200);
    expect(await archivedResponse.json()).toEqual(expect.objectContaining({ archivedAt: expect.any(String) }));
    const archivedList = await fetch(`${base}/api/sessions?archived=true`);
    expect((await archivedList.json()) as unknown).toEqual({
      sessions: expect.arrayContaining([expect.objectContaining({ id: created.id })]),
    });
    const restoredResponse = await fetch(`${base}/api/sessions/${created.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ archived: false }),
    });
    const timestamp = new Date().toISOString();
    await hookAudit.record({
      id: crypto.randomUUID(),
      eventId: crypto.randomUUID(),
      sessionId: created.id,
      eventName: "SessionEnd",
      handlerId: "session-cleanup-test",
      startedAt: timestamp,
      completedAt: timestamp,
      durationMs: 0,
      status: "passed",
    });
    expect(await hookAudit.listForSession(created.id)).toContainEqual(
      expect.objectContaining({ handlerId: "session-cleanup-test" }),
    );
    expect(restoredResponse.status).toBe(200);
    const deletedResponse = await fetch(`${base}/api/sessions/${created.id}`, { method: "DELETE" });
    expect(deletedResponse.status).toBe(204);
    expect((await fetch(`${base}/api/sessions/${created.id}`)).status).toBe(404);
    expect(await hookAudit.listForSession(created.id)).toEqual([]);
    cleanupSessionIds.delete(created.id);
  });
});

describe("task management", () => {
  test("reads exact task evidence by id", async () => {
    const task = await taskStore.createTask({ prompt: "Inspect exact task", model: "test/model" });
    try {
      const response = await fetch(`${base}/api/tasks/${task.id}`);
      expect(response.status).toBe(200);
      expect(await response.json()).toEqual(expect.objectContaining({
        id: task.id,
        prompt: "Inspect exact task",
        status: "queued",
      }));
      expect((await fetch(`${base}/api/tasks/missing-task`)).status).toBe(404);
    } finally {
      await taskStore.storage.db.none("DELETE FROM popagent_agent_tasks WHERE id=$1", [task.id]);
    }
  });

  test("lists only autonomous workflows when requested", async () => {
    const workflow = await taskStore.createTask({ prompt: "Autonomous workflow", model: "test/model" });
    const ordinary = await taskStore.createTask({ prompt: "Ordinary task", model: "test/model" });
    try {
      await taskStore.storage.db.none(
        "UPDATE popagent_agent_tasks SET source='self-update',workflow=$2::jsonb WHERE id=$1",
        [workflow.id, JSON.stringify({ state: "running" })],
      );
      const response = await fetch(`${base}/api/tasks?workflow=true`);
      expect(response.status).toBe(200);
      expect(await response.json()).toEqual({
        tasks: [expect.objectContaining({ id: workflow.id, source: "self-update" })],
      });
    } finally {
      await taskStore.storage.db.none("DELETE FROM popagent_agent_tasks WHERE id IN ($1,$2)", [workflow.id, ordinary.id]);
    }
  });

  test("removes terminal work but preserves active tasks", async () => {
    const terminal = await taskStore.createTask({ prompt: "Remove terminal task", model: "test/model" });
    const active = await taskStore.createTask({ prompt: "Keep active task", model: "test/model" });
    try {
      expect(await taskStore.claim(terminal.id)).toBe(true);
      await taskStore.fail(terminal.id, "Expected failure", "unknown");
      expect((await fetch(`${base}/api/tasks/${terminal.id}`, { method: "DELETE" })).status).toBe(204);
      expect((await fetch(`${base}/api/tasks/${terminal.id}`, { method: "DELETE" })).status).toBe(404);
      expect((await fetch(`${base}/api/tasks/${active.id}`, { method: "DELETE" })).status).toBe(409);
    } finally {
      await taskStore.storage.db.none("DELETE FROM popagent_agent_tasks WHERE id IN ($1,$2)", [terminal.id, active.id]);
    }
  });

  test("marks reviewed failed work completed with inline evidence", async () => {
    const task = await taskStore.createTask({ prompt: "Resolve reviewed task", model: "test/model" });
    try {
      expect(await taskStore.claim(task.id)).toBe(true);
      await taskStore.fail(task.id, "Expected failure", "unknown");
      const response = await fetch(`${base}/api/tasks/${task.id}/resolve`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ output: "Reviewed inline; delivered behavior is verified." }),
      });
      expect(response.status).toBe(200);
      expect(await response.json()).toEqual(expect.objectContaining({
        id: task.id,
        status: "completed",
        output: "Reviewed inline; delivered behavior is verified.",
      }));
    } finally {
      await taskStore.storage.db.none("DELETE FROM popagent_agent_tasks WHERE id=$1", [task.id]);
    }
  });
});

describe("memory management", () => {
  test("creates, filters, edits, and deletes a durable fact", async () => {
    const marker = `gui-memory-${crypto.randomUUID()}`;
    cleanupMemoryKeys.add(marker);
    const createdResponse = await fetch(`${base}/api/memories`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ key: marker, content: `Original ${marker}`, importance: 0.7 }),
    });
    expect(createdResponse.status).toBe(201);
    const created = (await createdResponse.json()) as { id: string };
    const listResponse = await fetch(`${base}/api/memories?kind=fact&query=${marker}`);
    expect(await listResponse.json()).toEqual({
      memories: [expect.objectContaining({ id: created.id, key: marker, kind: "fact" })],
    });
    const updatedResponse = await fetch(`${base}/api/memories/${created.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ key: marker, content: `Updated ${marker}`, importance: 0.95 }),
    });
    expect(updatedResponse.status).toBe(200);
    expect(await updatedResponse.json()).toEqual(expect.objectContaining({
      content: `Updated ${marker}`,
      importance: 0.95,
    }));
    expect((await fetch(`${base}/api/memories/${created.id}`, { method: "DELETE" })).status).toBe(204);
    expect((await fetch(`${base}/api/memories/${created.id}`, { method: "DELETE" })).status).toBe(404);
  });

  test("reports durable memory status", async () => {
    const response = await fetch(`${base}/api/memories/status`);
    expect(response.status).toBe(200);
    const status = await response.json() as Record<string, unknown>;
    expect(status).toEqual(expect.objectContaining({
      facts: expect.any(Number),
      episodes: expect.any(Number),
      recalled: expect.any(Number),
    }));
    expect(status.latestUpdatedAt === null || typeof status.latestUpdatedAt === "string").toBe(true);
  });

  test("recalls semantically and retains bounded OMP episodes", async () => {
    const marker = `omp-native-${crypto.randomUUID()}`;
    cleanupMemoryKeys.add(marker);
    const createdResponse = await fetch(`${base}/api/memories`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ key: marker, content: `The durable ${marker} preference is compact output.`, importance: 0.95 }),
    });
    expect(createdResponse.status).toBe(201);

    const recallResponse = await fetch(`${base}/api/memories/recall?query=${encodeURIComponent(`What is the ${marker} preference?`)}`);
    expect(recallResponse.status).toBe(200);
    expect(await recallResponse.json()).toEqual({
      memories: [expect.objectContaining({ key: marker, kind: "fact" })],
    });

    const episodeResponse = await fetch(`${base}/api/memories/episodes`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ sessionId: `omp:${marker}`, userText: marker, assistantText: "Verified native OMP memory." }),
    });
    expect(episodeResponse.status).toBe(204);
    await longTermMemory.storage.db.none("DELETE FROM popagent_memories WHERE session_id = $1", [`omp:${marker}`]);
  });

  test("rejects invalid and oversized managed fact payloads", async () => {
    const invalidCreates = [
      { content: "x".repeat(2_001), importance: 0.8 },
      { content: "probe", key: 42, importance: 0.8 },
      { content: "probe", key: "valid", importance: "not-a-number" },
      { content: "probe", importance: 2 },
      { content: "probe", importance: 0.8, unexpected: true },
    ];
    for (const body of invalidCreates) {
      const response = await fetch(`${base}/api/memories`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(body),
      });
      expect(response.status).toBe(400);
      expect(await response.json()).toEqual(expect.objectContaining({
        error: "Invalid memory",
        issues: expect.any(Array),
      }));
    }

    const invalidUpdate = await fetch(`${base}/api/memories/missing`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ content: "probe", key: 42, importance: "not-a-number" }),
    });
    expect(invalidUpdate.status).toBe(400);
    expect(await invalidUpdate.json()).toEqual(expect.objectContaining({
      error: "Invalid memory",
      issues: expect.any(Array),
    }));
  });

  test("lists hook activity for a session", async () => {
    const response = await fetch(`${base}/api/sessions/missing/hooks`);
    expect(response.status).toBe(200);
    expect(await response.json()).toEqual({ runs: [] });
  });
});

describe("hook lifecycle", () => {
  test("fires SessionStart after creating a persistent session", async () => {
    const response = await fetch(`${hookedBase}/api/sessions`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ model: "cx/gpt-5.6-sol" }),
    });
    const session = (await response.json()) as { id: string };
    cleanupSessionIds.add(session.id);

    expect(response.status).toBe(201);
    expect(hookEvents).toContainEqual(expect.objectContaining({
      eventName: "SessionStart",
      sessionId: session.id,
      detail: expect.objectContaining({ source: "create" }),
    }));
    await testSessions.delete(session.id);
    cleanupSessionIds.delete(session.id);
  });

  test("records trace-correlated redacted evidence when a prompt hook denies before streaming", async () => {
    const threadId = crypto.randomUUID();
    cleanupThreadIds.add(threadId);
    try {
      const response = await fetch(`${hookedBase}/api/chat`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: chatBody(undefined, threadId),
      });

      expect(response.status).toBe(403);
      expect(await response.json()).toEqual({ error: "test policy denied prompt" });
      expect(hookEvents).toContainEqual(expect.objectContaining({ eventName: "UserPromptSubmit" }));
      const signal = await waitForEvolutionSignal(threadId, "turn-failure");
      const summary = signal?.summary;
      expect(signal).toMatchObject({
        traceId: expect.stringMatching(/^[a-f0-9]{32}$/),
        summary: expect.stringContaining("Goal: Reply with exactly the word: pong"),
      });
      expect(summary).toContain("Classification: HookBlockedError");
      expect(summary).toContain("Failure: test policy denied prompt");
    } finally {
      await evolutionStore.storage.db.none(
        "DELETE FROM popagent_evolution_signals WHERE session_id=$1",
        [threadId],
      );
      await cleanupThread(threadId);
      cleanupThreadIds.delete(threadId);
    }
  });
});

describe("cross-session memory", () => {
  test("recalls a durable fact in a different chat session", async () => {
    const key = `favorite-code-word-${crypto.randomUUID().slice(0, 8)}`;
    cleanupMemoryKeys.add(key);
    const codeWord = `heliotrope-${crypto.randomUUID().slice(0, 8)}`;
    await longTermMemory.rememberFact({
      resourceId: "popagent-user",
      sessionId: "memory-source-session",
      key,
      content: `The user's ${key} is ${codeWord}.`,
      importance: 1,
    });

    const threadId = crypto.randomUUID();
    cleanupThreadIds.add(threadId);
    const response = await fetch(`${base}/api/chat`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        id: threadId,
        messages: [{
          id: "memory-query",
          role: "user",
          parts: [{ type: "text", text: `What is my ${key}? Reply with only the value.` }],
        }],
      }),
    });

    expect(response.status).toBe(200);
    expect(joinDeltas(await response.text())).toContain(codeWord);
    await cleanupThread(threadId);
    cleanupThreadIds.delete(threadId);
    await longTermMemory.storage.db.none(
      "DELETE FROM popagent_memories WHERE resource_id = $1 AND memory_key = $2",
      ["popagent-user", key],
    );
    cleanupMemoryKeys.delete(key);
  }, 60000);
});


describe("observability API", () => {
  test("bounds trace queries before scanning storage", async () => {
    const response = await fetch(`${base}/api/observability/traces?from=2026-01-01T00:00:00Z&to=2026-08-01T00:00:00Z`);
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual(expect.objectContaining({ error: expect.stringContaining("30 days") }));
  });

  test("validates trace-linked feedback", async () => {
    const response = await fetch(`${base}/api/observability/feedback`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ traceId: "not-a-trace", value: 1 }),
    });
    expect(response.status).toBe(400);
  });
});

describe("POST /api/chat", () => {
  test("streams a UI message response and records useful trace-correlated success evidence", async () => {
    const threadId = crypto.randomUUID();
    cleanupThreadIds.add(threadId);
    try {
      const res = await fetch(`${base}/api/chat`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: chatBody(undefined, threadId),
      });
      expect(res.status).toBe(200);
      expect(joinDeltas(await res.text()).toLowerCase()).toContain("pong");
      const signal = await waitForEvolutionSignal(threadId, "turn-success");
      const summary = signal?.summary;
      expect(signal).toMatchObject({
        traceId: expect.stringMatching(/^[a-f0-9]{32}$/),
        summary: expect.stringContaining("Goal: Reply with exactly the word: pong"),
      });
      expect(summary).toContain("Classification: success");
      expect(summary?.toLowerCase()).toContain("outcome: pong");
    } finally {
      await evolutionStore.storage.db.none(
        "DELETE FROM popagent_evolution_signals WHERE session_id=$1",
        [threadId],
      );
      await cleanupThread(threadId);
      cleanupThreadIds.delete(threadId);
    }
  }, 180000);

  test("honors a model override in the body", async () => {
    const threadId = crypto.randomUUID();
    cleanupThreadIds.add(threadId);
    const res = await fetch(`${base}/api/chat`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: chatBody("cx/gpt-5.3-codex-spark", threadId),
    });
    expect(res.status).toBe(200);
    expect(joinDeltas(await res.text()).toLowerCase()).toContain("pong");
    await evolutionStore.storage.db.none(
      "DELETE FROM popagent_evolution_signals WHERE session_id=$1",
      [threadId],
    );
    await cleanupThread(threadId);
    cleanupThreadIds.delete(threadId);
  }, 60000);
});