Menu
popagent
publicLatest change ec66504a301a4150cf6cd2203554901a84fdc769 - Complete repository brief integration and CI deployment 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 {
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("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 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,
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("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);
}
}, 60000);
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);
});