Files
next-tjwater-agent/tests/runtime/hostQuestion.test.ts
T

97 lines
6.2 KiB
TypeScript

import { expect, test } from "bun:test";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
function runtime(state: "missing" | "enqueued" | "delivered", active = false) {
const calls: unknown[] = [];
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { active: async () => active ? { s: {} } : {}, interrupt: async (input: unknown) => { calls.push(input); } } }),
agentMessageState: async () => state,
prompt: async (...args: unknown[]) => { calls.push(args); },
}) as OpencodeRuntimeAdapter;
return { adapter, calls };
}
test("host reply continuation injects fixed ID only when absent; queued and consumed IDs never reinject", async () => {
const missing = runtime("missing");
expect(await missing.adapter.continueInteraction("s", "msg_fixed", "answer")).toBe("started");
expect(missing.calls).toEqual([["s", "answer", undefined, "msg_fixed"]]);
const busy = runtime("missing", true);
await expect(busy.adapter.continueInteraction("s", "msg_fixed", "answer")).rejects.toThrow("still active");
expect(busy.calls).toEqual([]);
const enqueued = runtime("enqueued");
expect(await enqueued.adapter.continueInteraction("s", "msg_fixed", "answer")).toBe("observing");
expect(enqueued.calls).toEqual([{ sessionID: "s", resume: true }]);
const consumed = runtime("delivered");
expect(await consumed.adapter.continueInteraction("s", "msg_fixed", "answer")).toBe("completed");
expect(consumed.calls).toEqual([]);
const observing = runtime("delivered", true);
expect(await observing.adapter.continueInteraction("s", "msg_fixed", "answer")).toBe("observing");
expect(observing.calls).toEqual([]);
});
test("native question lookup distinguishes missing form from unavailable service", async () => {
let failure: unknown = { _tag: "FormNotFoundError" };
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { form: { get: async () => { if (failure) throw failure; return { state: { status: "answered" } }; } } } }),
}) as OpencodeRuntimeAdapter;
expect(await adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).toBe("missing");
failure = new Error("network unavailable");
await expect(adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).rejects.toThrow("network unavailable");
failure = undefined;
expect(await adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).toBe("answered");
});
test("message admission lookup fails closed on inbox and message transport errors", async () => {
let inboxFailure = true;
let messageFailure: unknown = { status: 404 };
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { inbox: { list: async () => { if (inboxFailure) throw new Error("inbox unavailable"); return []; } }, message: { get: async () => { throw messageFailure; } } } }),
}) as OpencodeRuntimeAdapter;
await expect(adapter.agentMessageState("s", "msg_fixed")).rejects.toThrow("inbox unavailable");
inboxFailure = false;
expect(await adapter.agentMessageState("s", "msg_fixed")).toBe("missing");
messageFailure = new Error("message unavailable");
await expect(adapter.agentMessageState("s", "msg_fixed")).rejects.toThrow("message unavailable");
});
test("stop serializes behind in-flight answer admission and cancels its exact inbox identity", async () => {
let release!: () => void;
let admitted!: () => void;
const entered = new Promise<void>((resolve) => { admitted = resolve; });
const barrier = new Promise<void>((resolve) => { release = resolve; });
let state: "missing" | "enqueued" = "missing";
const calls: string[] = [];
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { active: async () => ({}), inbox: { cancel: async (input: { inboxID: string }) => { calls.push(`cancel:${input.inboxID}`); state = "missing"; } } } }),
agentMessageState: async () => state,
prompt: async (_sessionId: string, _text: string, _model: unknown, messageId: string) => { calls.push(`admit:${messageId}`); admitted(); await barrier; state = "enqueued"; },
}) as OpencodeRuntimeAdapter;
const continuation = adapter.continueInteraction("s", "msg_fixed", "answer");
await entered;
const stopping = adapter.stopInteractionInput("s", "msg_fixed");
await Promise.resolve();
expect(calls).toEqual(["admit:msg_fixed"]);
release();
expect(await continuation).toBe("started");
expect(await stopping).toBe("cancelled");
expect(calls).toEqual(["admit:msg_fixed", "cancel:msg_fixed"]);
});
test("completed-answer recovery uses the matching user boundary and refuses missing final evidence", async () => {
let messages: unknown[] = [
{ id: "later-answer", type: "assistant", time: { completed: 1 }, finish: "stop", content: [{ type: "text", text: "Unrelated answer" }] },
{ id: "later-user", type: "user" },
{ id: "answer", type: "assistant", time: { completed: 1 }, finish: "stop", content: [{ type: "text", text: "Saved answer result" }] },
{ id: "msg_fixed", type: "user" },
];
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ message: { list: async () => ({ data: messages }) } }),
}) as OpencodeRuntimeAdapter;
expect(await adapter.readInteractionResult("s", "msg_fixed")).toBe("Saved answer result");
messages = [{ id: "partial", type: "assistant", time: {}, content: [{ type: "text", text: "Partial output" }] }, { id: "msg_fixed", type: "user" }];
await expect(adapter.readInteractionResult("s", "msg_fixed")).rejects.toThrow("do not resend");
await expect(adapter.readInteractionResult("s", "outside-window")).rejects.toThrow("outside the recovery window");
});
test("strict waiting boundary requires verified idle and rejects an exhausted deadline", async () => {
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { active: async () => ({}) } }),
}) as OpencodeRuntimeAdapter;
await expect(adapter.waitForSessionIdle("s", 100, true)).resolves.toBeUndefined();
await expect(adapter.waitForSessionIdle("s", 0, true)).rejects.toThrow("waiting boundary");
});