62 lines
5.2 KiB
TypeScript
62 lines
5.2 KiB
TypeScript
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 { 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("reconciles persisted legacy forms and replies after the active run was lost", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "legacy-question-routes-"));
|
|
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
|
|
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
|
|
const states = new SessionUiStateStore(join(root, "unused"), workspaces);
|
|
const context = { ...getLocalAgentContext(), sessionId: "session" };
|
|
await sessions.ensure(context);
|
|
await states.write(context, { sessionId: "session", messages: [{ id: "old", role: "assistant", questions: [{ requestId: "form", status: "pending", tool: { callID: "call" } }] }, { id: "new", role: "assistant", text: "later message", parts: [{ type: "data-question_request", data: { request_id: "form2", session_id: "session", questions: [], tool: { callID: "call2" } } }, { type: "data-question_request", data: { request_id: "form3", session_id: "session", questions: [] } }, { type: "data-question_request", data: { request_id: "form4", session_id: "session", questions: [] } }] }] });
|
|
let status: "pending" | "answered" | "cancelled" | "missing" = "pending";
|
|
let unavailable = false;
|
|
let reads = 0;
|
|
let replies = 0;
|
|
const runtime = { getQuestionStatus: async () => { reads++; if (unavailable) throw new Error("private-native-path"); return status; }, replyQuestion: async () => { replies++; status = "answered"; }, rejectQuestion: async () => { status = "cancelled"; } } as unknown as OpencodeRuntimeAdapter;
|
|
const app = express(); app.use(express.json());
|
|
registerChatInteractionRoutes(app, { activeRuns: new Map(), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states });
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
|
const get = (id = "form", headers = {}) => fetch(`${base}/session/session/questions/${id}`, { headers });
|
|
const reply = (id = "form") => fetch(`${base}/question/${id}/reply`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: "session", answers: [["A"]] }) });
|
|
try {
|
|
expect((await get("unknown")).status).toBe(404); expect(reads).toBe(0);
|
|
expect((await get("form", { "X-Agent-Local-User": "other" })).status).toBe(404); expect(reads).toBe(0);
|
|
expect(await (await get("call")).json()).toMatchObject({ request_id: "form", status: "pending" });
|
|
unavailable = true;
|
|
const failed = await get(); expect(failed.status).toBe(503); expect(await failed.text()).not.toContain("private-native-path");
|
|
expect((await states.read(context))?.messages[0]).toMatchObject({ questions: [{ status: "pending" }] });
|
|
unavailable = false;
|
|
expect((await reply()).status).toBe(202); expect(replies).toBe(1);
|
|
expect((await states.read(context))?.messages[0]).toMatchObject({ questions: [{ status: "answered", answers: [["A"]] }] });
|
|
expect((await states.read(context))?.messages[1]).toMatchObject({ text: "later message" });
|
|
expect((await reply()).status).toBe(409); expect(replies).toBe(1);
|
|
status = "missing"; expect(await (await get()).json()).toMatchObject({ status: "answered" });
|
|
status = "cancelled"; expect(await (await get("call2")).json()).toMatchObject({ request_id: "form2", status: "rejected" });
|
|
expect((await states.read(context))?.messages[1]).toMatchObject({ parts: [{ type: "data-question_request" }, { type: "data-question_request" }, { type: "data-question_request" }, { type: "data-question_response", data: { request_id: "form2", status: "rejected" } }] });
|
|
status = "missing"; expect(await (await get("form2")).json()).toMatchObject({ status: "rejected" });
|
|
expect(await (await get("form3")).json()).toMatchObject({ status: "invalidated" });
|
|
status = "pending";
|
|
expect((await reply("form4")).status).toBe(202);
|
|
const canonical = (await states.read(context))?.messages[1] as { parts: Array<{ type: string; data?: unknown }> };
|
|
expect(canonical.parts.find((part) => part.type === "data-question_response" && (part.data as { request_id: string }).request_id === "form4")).toMatchObject({ data: { request_id: "form4", status: "answered", answers: [["A"]] } });
|
|
status = "missing"; expect(await (await get("form4")).json()).toMatchObject({ status: "answered" });
|
|
await sessions.remove((await sessions.get(context, "session"))!);
|
|
expect((await get()).status).toBe(404);
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|