59 lines
4.1 KiB
TypeScript
59 lines
4.1 KiB
TypeScript
import { createChatRunAccumulator } from "../../src/routes/chat-run-accumulator.js";
|
|
import { expect, it } from "bun:test";
|
|
import express from "express";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { getLocalAgentContext } from "../../src/context/localContext.js";
|
|
import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js";
|
|
import type { ActiveRun } from "../../src/routes/chatUiState.js";
|
|
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
|
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
|
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
|
|
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
|
|
|
|
it.each([false, true])("persists permission replies in the owner's workspace (native failure=%s)", async (nativeFailure) => {
|
|
const root = await mkdtemp(join(tmpdir(), "permission-persistence-"));
|
|
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
|
|
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
|
|
const states = new SessionUiStateStore(workspaces);
|
|
const context = { ...getLocalAgentContext(), sessionId: "permission-session" };
|
|
await sessions.ensure(context);
|
|
const messages = [{ id: "answer", role: "assistant", permissions: [] }];
|
|
await states.write(context, { sessionId: context.sessionId, isTitleManuallyEdited: true, messages });
|
|
let calls = 0;
|
|
const runtime = { replyPermission: async () => { calls++; if (nativeFailure) throw new Error("native unavailable"); } } as unknown as OpencodeRuntimeAdapter;
|
|
const run: ActiveRun = {
|
|
clientSessionId: context.sessionId, controller: new AbortController(), messages,
|
|
pendingPermissions: new Map([["permission", { request_id: "permission", session_id: context.sessionId, permission: "shell", patterns: [], always: [], created_at: Date.now() }]]),
|
|
pendingQuestions: new Map(), subscribers: new Set(), status: "running",
|
|
};
|
|
const accumulator = createChatRunAccumulator({ run, messages: [], messageId: "answer", sessionId: context.sessionId,
|
|
collaboration: false, write() {}, onStatus() {} });
|
|
accumulator.apply("permission_request", run.pendingPermissions.get("permission")! as unknown as Record<string, unknown>);
|
|
await states.write(context, { sessionId: context.sessionId, isTitleManuallyEdited: true, messages: run.messages });
|
|
const app = express(); app.use(express.json());
|
|
registerChatInteractionRoutes(app, { activeRuns: new Map([[context.sessionId, run]]), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states });
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/permission/permission/reply`;
|
|
const reply = (headers = {}) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ session_id: context.sessionId, reply: "once" }) });
|
|
try {
|
|
expect((await reply({ "X-Agent-Local-User": "other" })).status).toBe(404);
|
|
expect(calls).toBe(0);
|
|
expect((await reply()).status).toBe(nativeFailure ? 502 : 202);
|
|
expect(calls).toBe(1);
|
|
expect(run.pendingPermissions.has("permission")).toBe(nativeFailure);
|
|
const saved = await states.read(context);
|
|
expect(saved?.isTitleManuallyEdited).toBe(true);
|
|
const parts = (saved?.messages[0] as { parts: Array<{ type: string; data: Record<string, unknown> }> }).parts;
|
|
expect(parts.find((part) => part.type === "data-permission_response")?.data).toMatchObject(nativeFailure
|
|
? { request_id: "permission", status: "error", error: "native unavailable" }
|
|
: { request_id: "permission", reply: "once" });
|
|
expect(run.messages[0]).toMatchObject({ permissions: [{ requestId: "permission", status: nativeFailure ? "error" : "approved_once" }] });
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|