Files
next-tjwater-agent/tests/routes/legacyQuestionRoutes.test.ts
T

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