// Native inbox acceptance and delivery changes against a local deterministic model. import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { startOpencode2 } from "../../src/runtime/opencode2-server.js"; import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; import { ResponseInputTracker } from "../../src/chat/response-inputs.js"; import { streamPromptResponse } from "../../src/routes/chatStream.js"; import { config } from "../../src/config.js"; const root = await mkdtemp(join(tmpdir(), "tjwater-response-input-")); config.LLM_REQUEST_AUDIT_LOG_PATH = join(root, "fixture-audit.log"); let release = () => {}; const gate = new Promise((resolve) => { release = resolve; }); let releaseSecond = () => {}; const secondGate = new Promise((resolve) => { releaseSecond = resolve; }); const prompts: unknown[] = []; const fixture = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) { if (new URL(request.url).pathname !== "/v1/chat/completions") return new Response(null, { status: 404 }); prompts.push(await request.json()); if (prompts.length === 1) await gate; if (prompts.length === 2) await secondGate; const chunk = (delta: object, finish_reason: string | null) => JSON.stringify({ id: "fixture", object: "chat.completion.chunk", created: 1, model: "fake", choices: [{ index: 0, delta, finish_reason }] }); return new Response(`data: ${chunk({ role: "assistant", content: "Local completed." }, null)}\n\ndata: ${chunk({}, "stop")}\n\ndata: [DONE]\n\n`, { headers: { "Content-Type": "text/event-stream" } }); } }); let native: Awaited> | undefined; const wait = async (label: string, predicate: () => boolean | Promise) => { const deadline = Date.now() + 15_000; while (!await predicate()) { if (Date.now() > deadline) throw new Error(`native input smoke timed out: ${label}, fixture requests=${prompts.length}`); await Bun.sleep(20); } }; try { native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000, config: { model: "fixture/fake", agents: { instruction: { mode: "primary", model: "fixture/fake", system: "Reply with a short local fixture answer." } }, providers: { fixture: { name: "Fixture", package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: `http://127.0.0.1:${fixture.port}/v1`, apiKey: "LOCAL_FIXTURE" }, models: { fake: { capabilities: { tools: true }, limit: { context: 32768, output: 1024 } } } } } }, env: { OPENCODE_DB: join(root, "native.db"), XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"), TJWATER_AGENT_MCP_CONNECTION_PATH: join(root, "no-connection") }, }); const client = native.client; const session = await client.session.create({ title: "Inbox smoke fixture", location: { directory: root } }); await client.session.switchModel({ sessionID: session.id, model: { providerID: "fixture", id: "fake" } }); const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter; const consumed: string[] = []; const tracker = new ResponseInputTracker(runtime, session.id, (input) => { if (input.status === "delivered") consumed.push(input.id); }); const tokens: string[] = []; let finished = false; const running = streamPromptResponse({ runtime, sessionId: session.id, clientSessionId: session.id, message: "Original local task", model: "fixture/fake", beforePrompt: () => tracker.restore(), onPromptDispatched: () => tracker.markReady(), onInboxEvent: (type, data) => tracker.observe(type, data), waitForUserInput: async () => !await tracker.canClose(), write: (event, data) => { if (event === "token") tokens.push(String(data.content)); } }); void running.catch(() => {}); void running.then(() => { finished = true; }, () => { finished = true; }); await wait("first local generation", () => prompts.length >= 1); const item = await tracker.submit({ id: `msg_${"1".repeat(32)}`, text: "Additional local user input", delivery: "queue", status: "pending" }); assert.equal(item.delivery, "queue"); assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "queue"); await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "steer" }); assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "steer"); await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "queue" }); const withdrawn = await tracker.submit({ id: `msg_${"2".repeat(32)}`, text: "Withdraw this before delivery", delivery: "queue", status: "pending" }); assert.equal((await runtime.cancelPendingUserInput(session.id, withdrawn.id)).status, "cancelled"); assert.ok(!(await client.session.inbox.list({ sessionID: session.id })).some((entry) => entry.id === withdrawn.id)); release(); await wait("queued continuation", () => prompts.length >= 2); await Bun.sleep(100); assert.equal(finished, false, "managed stream closed while queued generation was running"); releaseSecond(); let finishTimer: ReturnType | undefined; const result = await Promise.race([running, new Promise((_resolve, reject) => { finishTimer = setTimeout(() => reject(new Error("managed stream did not finish")), 10_000); finishTimer.unref(); })]).finally(() => clearTimeout(finishTimer)); assert.equal(result.failed, false); assert.equal(result.aborted, false); assert.ok(consumed.includes(item.id)); assert.ok(tokens.join("").split("Local completed.").length >= 3, "managed stream closed before queued answer"); assert.ok(JSON.stringify(prompts[1]).includes("Additional local user input")); assert.ok(!(await client.session.inbox.list({ sessionID: session.id })).some((entry) => entry.id === item.id)); assert.ok(!JSON.stringify(prompts).includes(withdrawn.text)); await assert.rejects(runtime.cancelPendingUserInput(session.id, item.id), { status: 409 }); // Simulate delivery winning the race after the adapter's pending-list read. const staleRuntime = Object.assign(Object.create(runtime), { listPendingUserInputs: async () => [item] }) as OpencodeRuntimeAdapter; await assert.rejects(staleRuntime.cancelPendingUserInput(session.id, item.id), { status: 409 }); await client.session.remove({ sessionID: session.id }); console.log("Native queue delivery, pending changes, withdrawal and delivery-race rejection passed; local fixture only."); } finally { release(); releaseSecond(); native?.close(); if (native) await native.exited; fixture.stop(true); await rm(root, { recursive: true, force: true }); }