import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs/promises"; import path from "node:path"; import { makeHarness } from "./helpers.mjs"; import { MockBackendFactory } from "../src/backend/mock.mjs"; import { StateStore } from "../src/store.mjs"; import { cwdToSessionDirName } from "../src/omp-sessions.mjs"; async function withHarness(options, fn) { const harness = await makeHarness(options); try { await fn(harness); } finally { await harness.close(); } } test("subscribed native OMP threads broadcast external activity changes", () => withHarness({}, async (h) => { await h.initialize(); const sessionDir = path.join(h.temp, "sessions", cwdToSessionDirName(h.temp)); const sessionFile = path.join(sessionDir, "2026-08-24T00-00-00-000Z_native-watched.jsonl"); await fs.mkdir(sessionDir, { recursive: true }); await fs.writeFile(sessionFile, [ JSON.stringify({ type: "session", id: "native-watched", timestamp: new Date().toISOString(), cwd: h.temp }), JSON.stringify({ type: "message", id: "user-first", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "first" }] } }), JSON.stringify({ type: "message", id: "assistant-first", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "done" }] } }), ].join("\n") + "\n"); const read = await h.request("thread/read", { threadId: "native-watched" }); assert.equal(read.thread.status.type, "idle"); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "user-second", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "second" }] }, }) + "\n"); const active = await h.waitFor((message) => message.method === "thread/status/changed" && message.params?.threadId === "native-watched" && message.params?.status?.type === "active" ); assert.deepEqual(active.params.status, { type: "active", activeFlags: [] }); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "assistant-second", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "done again" }] }, }) + "\n"); const idle = await h.waitFor((message) => message.method === "thread/status/changed" && message.params?.threadId === "native-watched" && message.params?.status?.type === "idle" ); assert.deepEqual(idle.params.status, { type: "idle" }); })); test("externally started OMP turns stream items and turn lifecycle to subscribers", () => withHarness({}, async (h) => { await h.initialize(); const sessionDir = path.join(h.temp, "sessions", cwdToSessionDirName(h.temp)); const sessionFile = path.join(sessionDir, "2026-08-24T00-00-00-000Z_native-streamed.jsonl"); await fs.mkdir(sessionDir, { recursive: true }); await fs.writeFile(sessionFile, [ JSON.stringify({ type: "session", id: "native-streamed", timestamp: new Date().toISOString(), cwd: h.temp }), JSON.stringify({ type: "message", id: "user-1", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "first" }] } }), JSON.stringify({ type: "message", id: "assistant-1", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "first answer" }] } }), ].join("\n") + "\n"); const read = await h.request("thread/read", { threadId: "native-streamed" }); assert.equal(read.thread.turns.length, 1); const historyReplayed = h.messages.some((message) => message.method === "turn/started"); assert.equal(historyReplayed, false, "existing history must not be replayed as new turns"); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "user-2", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "second" }] }, }) + "\n"); const started = await h.waitFor((message) => message.method === "turn/started" && message.params?.threadId === "native-streamed" ); assert.ok(started.params.turn.id); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "assistant-2", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "second answer" }] }, }) + "\n"); const itemEvent = await h.waitFor((message) => (message.method === "item/started" || message.method === "item/completed" || message.method === "item/updated") && message.params?.threadId === "native-streamed" && message.params?.item?.type === "agentMessage" ); assert.match(JSON.stringify(itemEvent.params.item), /second answer/); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params?.threadId === "native-streamed" && message.params?.turn?.id === started.params.turn.id ); assert.equal(completed.params.turn.status, "completed"); })); test("externally projected commands complete once with durable output", () => withHarness({}, async (h) => { await h.initialize(); const sessionDir = path.join(h.temp, "sessions", cwdToSessionDirName(h.temp)); const sessionFile = path.join(sessionDir, "2026-08-24T00-00-00-000Z_native-command.jsonl"); await fs.mkdir(sessionDir, { recursive: true }); await fs.writeFile(sessionFile, [ JSON.stringify({ type: "session", id: "native-command", timestamp: new Date().toISOString(), cwd: h.temp }), JSON.stringify({ type: "message", id: "user-first", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "first" }] } }), JSON.stringify({ type: "message", id: "assistant-first", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "done" }] } }), ].join("\n") + "\n"); await h.request("thread/read", { threadId: "native-command" }); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "user-second", timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "check jobs" }] }, }) + "\n"); const turnStarted = await h.waitFor((message) => message.method === "turn/started" && message.params?.threadId === "native-command" ); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "assistant-command", timestamp: new Date().toISOString(), message: { role: "assistant", content: [{ type: "toolCall", id: "command-call", name: "bash", arguments: { command: "check-spindle-jobs" }, }], }, }) + "\n"); const itemStarted = await h.waitFor((message) => message.method === "item/started" && message.params?.threadId === "native-command" && message.params?.item?.id === "command-call" ); assert.equal(itemStarted.params.item.status, "inProgress"); assert.equal(itemStarted.params.item.aggregatedOutput, ""); assert.equal(itemStarted.params.item.durationMs, null); const output = "running=3 queued=0\nWall time: 18.14 seconds"; await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "command-result", timestamp: new Date().toISOString(), message: { role: "toolResult", toolCallId: "command-call", toolName: "bash", content: [{ type: "text", text: output }], details: { wallTimeMs: 18140 }, isError: false, }, }) + "\n"); const itemCompleted = await h.waitFor((message) => message.method === "item/completed" && message.params?.threadId === "native-command" && message.params?.item?.id === "command-call" ); assert.equal(itemCompleted.params.item.status, "completed"); assert.equal(itemCompleted.params.item.aggregatedOutput, output); assert.equal(itemCompleted.params.item.durationMs, 18140); await fs.appendFile(sessionFile, JSON.stringify({ type: "message", id: "assistant-final", timestamp: new Date().toISOString(), message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "still running" }] }, }) + "\n"); await h.waitFor((message) => message.method === "turn/completed" && message.params?.threadId === "native-command" && message.params?.turn?.id === turnStarted.params.turn.id ); const lifecycle = h.messages.filter((message) => (message.method === "item/started" || message.method === "item/completed") && message.params?.threadId === "native-command" && message.params?.item?.id === "command-call" ); assert.deepEqual(lifecycle.map((message) => message.method), ["item/started", "item/completed"]); })); test("reordered item projections do not republish an unchanged external item", () => withHarness({}, async (h) => { await h.initialize(); const threadId = "native-reordered"; const item = { id: "item-1", type: "agentMessage", status: "completed", text: "stable answer", createdAt: 1787460346, }; const turn = (projectedItem) => ({ id: "turn-1", status: "completed", items: [projectedItem], tokenUsage: null, }); // Same content, different key insertion order: exactly what the local and // reparsed projections produce for one item. const reordered = Object.fromEntries(Object.entries(item).reverse()); assert.notEqual(JSON.stringify(item), JSON.stringify(reordered), "fixture must differ only in key order"); let projected = item; h.server.store.getThread = async (id) => (id === threadId ? { id: threadId, turns: [turn(projected)], model: null } : null); h.server.store.getExternalThreadStatus = async () => ({ type: "active", activeFlags: [] }); // thread/read subscribes and seeds the projection from the first ordering. await h.request("thread/read", { threadId }); projected = reordered; // Two full poll intervals: the projection now only differs in key order. await new Promise((resolve) => setTimeout(resolve, 2_500)); const republished = h.messages.filter((message) => (message.method === "item/started" || message.method === "item/updated" || message.method === "item/completed") && message.params?.threadId === threadId ); assert.deepEqual(republished, [], "key-order-only changes must not republish an item"); })); test("failed session setup releases the turn-start reservation without persisting a ghost turn", () => { const base = new MockBackendFactory(); let attempts = 0; const backendFactory = { listModels: (...args) => base.listModels(...args), async openSession(options) { attempts += 1; if (attempts === 1) throw new Error("synthetic open failure"); return base.openSession(options); }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const thread = await h.store.createThread({ cwd: h.temp }); await assert.rejects( h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "first" }] }), /synthetic open failure/ ); const afterFailure = await h.request("thread/read", { threadId: thread.id }); assert.equal(afterFailure.thread.status.type, "idle"); assert.equal(afterFailure.thread.turns.length, 0); const retry = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "retry" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params?.turn?.id === retry.turn.id ); assert.equal(completed.params.turn.status, "completed"); }); }); test("initialization and model discovery", () => withHarness({}, async (h) => { const initialized = await h.initialize(); assert.match(initialized.userAgent, /omp-codex-bridge/); const models = await h.request("model/list", {}); assert.equal(models.data[0].id, "mock/omp"); assert.deepEqual(await h.request("plugin/list", {}), { marketplaces: [], featuredPluginIds: [], marketplaceLoadErrors: [], }); assert.deepEqual(await h.request("plugin/installed", {}), { marketplaces: [], featuredPluginIds: [], marketplaceLoadErrors: [], }); assert.deepEqual(await h.request("remoteControl/enable", { enabled: true }), {}); })); test("config/read keeps backend routing tags out of iOS new-thread picker state", () => { const backendFactory = new MockBackendFactory(); backendFactory.modelProviderId = "omp-codex-bridge"; return withHarness({ backendFactory, config: { defaultModel: "gpt-5.6-luna-via-prime-agent" }, }, async (h) => { await h.request("initialize", { clientInfo: { name: "codex_chatgpt_ios_remote", version: "1" }, capabilities: {}, }); await h.notify("initialized"); const { config } = await h.request("config/read", { cwd: h.temp }); assert.equal(config.model, "gpt-5.6-luna"); assert.equal(config.model_provider, "omp-codex-bridge"); }); }); test("new-thread model picker writes update the defaults used by thread/start", () => withHarness({ config: { defaultModel: "mock/omp", defaultReasoningEffort: "medium" }, }, async (h) => { await h.initialize(); const result = await h.request("config/batchWrite", { edits: [ { keyPath: "model", value: "claude-opus-5", mergeStrategy: "upsert" }, { keyPath: "model_reasoning_effort", value: "xhigh", mergeStrategy: "upsert" }, ], filePath: null, expectedVersion: null, reloadUserConfig: true, }); assert.equal(result.status, "ok"); const { config } = await h.request("config/read", { cwd: h.temp }); assert.equal(config.model, "claude-opus-5"); assert.equal(config.model_reasoning_effort, "xhigh"); const { thread } = await h.request("thread/start", { cwd: h.temp }); assert.equal(thread.model, "claude-opus-5"); assert.equal(thread.reasoningEffort, "xhigh"); const persisted = JSON.parse(await fs.readFile(path.join(h.temp, "config.json"), "utf8")); assert.equal(persisted.defaultModel, "claude-opus-5"); assert.equal(persisted.defaultReasoningEffort, "xhigh"); })); test("fuzzy file search supports single-shot and streaming mention completion", () => withHarness({}, async (h) => { await h.initialize(); await fs.mkdir(path.join(h.temp, "src"), { recursive: true }); await fs.mkdir(path.join(h.temp, "test"), { recursive: true }); await fs.mkdir(path.join(h.temp, "node_modules", "ignored"), { recursive: true }); await fs.writeFile(path.join(h.temp, "src", "app-server.mjs"), ""); await fs.writeFile(path.join(h.temp, "test", "app-server.test.mjs"), ""); await fs.writeFile(path.join(h.temp, "node_modules", "ignored", "app-server-copy.mjs"), ""); const single = await h.request("fuzzyFileSearch", { query: "apptest", roots: [h.temp], cancellationToken: "mention-1", }); assert.ok(single.files.some((file) => file.path === "test/app-server.test.mjs")); assert.equal(single.files.some((file) => file.path.includes("node_modules")), false); assert.ok(single.files.every((file) => file.root && file.file_name && ["file", "directory"].includes(file.match_type) && Number.isFinite(file.score) )); assert.deepEqual(await h.request("fuzzyFileSearch/sessionStart", { sessionId: "mention-session", roots: [h.temp], }), {}); assert.deepEqual(await h.request("fuzzyFileSearch/sessionUpdate", { sessionId: "mention-session", query: "appserver", }), {}); const updated = await h.waitFor((message) => message.method === "fuzzyFileSearch/sessionUpdated" && message.params?.sessionId === "mention-session" ); assert.equal(updated.params.query, "appserver"); assert.ok(updated.params.files.some((file) => file.path === "src/app-server.mjs")); await h.waitFor((message) => message.method === "fuzzyFileSearch/sessionCompleted" && message.params?.sessionId === "mention-session" ); assert.deepEqual(await h.request("fuzzyFileSearch/sessionStop", { sessionId: "mention-session", }), {}); })); test("model discovery keeps the dynamic catalog visible and prioritizes requested models", () => { const openedSessions = []; const turnModels = []; const sessions = new MockBackendFactory(); const backendFactory = { async listModels() { return [ { id: "gpt-5.6-luna", model: "gpt-5.6-luna", displayName: "GPT-5.6-Luna" }, { id: "google-antigravity/gemini-3.7-flash", model: "google-antigravity/gemini-3.7-flash", displayName: "Gemini 3.7 Flash (Google)", hidden: true }, { id: "gemini-3.7-flash", model: "gemini-3.7-flash", displayName: "Gemini 3.7 Flash" }, { id: "anthropic/claude-opus-5", model: "anthropic/claude-opus-5", displayName: "Claude Opus 5 (Anthropic)", hidden: true }, { id: "claude-opus-5", model: "claude-opus-5", displayName: "Claude Opus 5" }, { id: "gpt-5.6-sol", model: "gpt-5.6-sol", displayName: "GPT-5.6-Sol" }, { id: "MercuriusDream/Nanbeige4.2-3B-mlx-8bit", model: "MercuriusDream/Nanbeige4.2-3B-mlx-8bit", displayName: "Nanbeige 4.2" }, { id: "anthropic/shared-model", model: "anthropic/shared-model", displayName: "Shared (Anthropic)" }, { id: "google-antigravity/shared-model", model: "google-antigravity/shared-model", displayName: "Shared (Google)" }, ]; }, async openSession(options) { openedSessions.push(options); const session = await sessions.openSession(options); const runTurn = session.runTurn.bind(session); session.runTurn = (prompt, context) => { turnModels.push(context.options.model); return runTurn(prompt, context); }; return session; }, }; return withHarness({ backendFactory, config: { defaultModel: "gpt-5.6-sol", modelAliases: { "claude-opus-5": "anthropic/claude-opus-5", "gemini-flash": "google-antigravity/gemini-3.7-flash", "gpt-5.6": "openai-codex/gpt-5.6-luna", "gpt-5.6-sol": "openai-codex/gpt-5.6-sol", luna: "openai-codex/gpt-5.6-luna", }, }, }, async (h) => { await h.initialize(); const config = await h.request("config/read", {}); assert.equal(config.config.model_provider, null); assert.deepEqual(config.config.model_providers, {}); const visible = await h.request("model/list", { includeHidden: false, limit: 100 }); assert.deepEqual( visible.data.slice(0, 4).map((model) => model.id), ["gpt-5.6-sol", "claude-opus-5", "gemini-3.7-flash", "gpt-5.6-luna"] ); const complete = await h.request("model/list", { includeHidden: true, limit: 100 }); assert.deepEqual( complete.data.slice(0, 4).map((model) => model.id), ["gpt-5.6-sol", "claude-opus-5", "gemini-3.7-flash", "gpt-5.6-luna"] ); assert.equal(complete.data.filter((model) => model.isDefault).length, 1); assert.equal(complete.data.find((model) => model.isDefault).id, "gpt-5.6-sol"); assert.equal(complete.data.some((model) => model.id === "anthropic/claude-opus-5"), false); assert.equal(complete.data.some((model) => model.id === "google-antigravity/gemini-3.7-flash"), false); assert.equal(complete.data.some((model) => ["gemini-flash", "gpt-5.6", "luna"].includes(model.id)), false); assert.ok(complete.data.every((model) => Object.hasOwn(model, "modelSpecialty"))); assert.ok(complete.data.every((model) => Object.hasOwn(model, "multiAgentVersion"))); const openaiStarted = await h.request("thread/start", { cwd: h.temp, model: "gpt-5.6-sol" }); assert.equal(openaiStarted.model, "gpt-5.6-sol"); assert.equal(openaiStarted.modelProvider, "openai"); assert.equal(openaiStarted.thread.modelProvider, "openai"); assert.equal(openaiStarted.thread.modelContextWindow, 1_000_000); assert.equal(openaiStarted.thread.modelSelector, "openai-codex/gpt-5.6-sol"); const qualifiedOpenAi = "openai-codex/gpt-5.6-sol"; const qualifiedStarted = await h.request("thread/start", { cwd: h.temp, model: qualifiedOpenAi }); assert.equal(qualifiedStarted.model, "gpt-5.6-sol"); assert.equal(qualifiedStarted.modelProvider, "openai"); assert.equal(qualifiedStarted.thread.model, "gpt-5.6-sol"); assert.equal(qualifiedStarted.thread.modelProvider, "openai"); assert.equal(qualifiedStarted.thread.modelSelector, qualifiedOpenAi); const qualifiedResumed = await h.request("thread/resume", { threadId: qualifiedStarted.thread.id, model: "anthropic/claude-opus-5", config: { model_reasoning_effort: "xhigh" }, }); assert.equal(qualifiedResumed.model, "claude-opus-5"); assert.equal(qualifiedResumed.thread.model, "claude-opus-5"); assert.equal(qualifiedResumed.thread.modelSelector, "anthropic/claude-opus-5"); assert.equal(qualifiedResumed.reasoningEffort, "xhigh"); assert.equal(qualifiedResumed.thread.reasoningEffort, "xhigh"); const collaborationStarted = await h.request("thread/start", { cwd: h.temp, collaborationMode: { mode: "default", settings: { model: "claude-opus-5", reasoning_effort: "high" } }, }); assert.equal(collaborationStarted.thread.modelSelector, "anthropic/claude-opus-5"); const collaborationResumed = await h.request("thread/resume", { threadId: collaborationStarted.thread.id, collaborationMode: { mode: "default", settings: { model: "google-antigravity/shared-model", reasoning_effort: "high" } }, }); assert.equal(collaborationResumed.thread.modelSelector, "google-antigravity/shared-model"); await h.request("thread/start", { cwd: h.temp, model: "claude-opus-5" }); assert.equal(openedSessions.at(-1).model, "anthropic/claude-opus-5"); const nested = "MercuriusDream/Nanbeige4.2-3B-mlx-8bit"; const started = await h.request("thread/start", { cwd: h.temp, model: nested }); assert.equal(started.model, nested); assert.equal(started.thread.model, nested); assert.equal(openedSessions.at(-1).model, nested); const { thread } = started; const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "nested model" }], }); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(turnModels.at(-1), nested); await h.request("thread/settings/update", { threadId: thread.id, model: "google-antigravity/shared-model", }); const settingsUpdate = h.messages.findLast((message) => message.method === "thread/settings/updated"); assert.equal(settingsUpdate.params.threadSettings.model, "shared-model"); assert.equal(settingsUpdate.params.threadSettings.modelProvider, "google-antigravity"); }); }); test("provider-qualified defaults stay visible and the Desktop catalog stays within one page", () => { const canonical = { id: "gemini-3.7-flash", model: "gemini-3.7-flash", selector: "google-antigravity/gemini-3.7-flash", displayName: "Gemini 3.7 Flash", }; const qualified = { ...canonical, id: canonical.selector, model: canonical.selector, hidden: true, }; const backendFactory = { async listModels() { return [ qualified, canonical, ...Array.from({ length: 110 }, (_, index) => ({ id: `provider-model-${String(index).padStart(3, "0")}`, model: `provider-model-${String(index).padStart(3, "0")}`, displayName: `Provider Model ${index}`, })), ]; }, }; return withHarness({ backendFactory, config: { defaultModel: "google-antigravity/gemini-3.7-flash" }, }, async (h) => { await h.initialize(); const complete = await h.request("model/list", { includeHidden: true, limit: 100 }); assert.equal(complete.data.length, 111); assert.equal(complete.data[0].id, "gemini-3.7-flash"); assert.equal(complete.data[0].isDefault, true); assert.equal(complete.data[0].hidden, false); assert.equal(complete.data.slice(1).every((model) => model.hidden), true); const visible = await h.request("model/list", { includeHidden: false, limit: 200 }); assert.equal(visible.data.length, 1); assert.equal(visible.data.filter((model) => model.isDefault).length, 1); }); }); test("a turn streams a native agent item and completes", () => withHarness({}, async (h) => { await h.initialize(); const started = await h.request("thread/start", { cwd: h.temp, model: "mock/omp" }); const threadId = started.thread.id; const response = await h.request("turn/start", { threadId, input: [{ type: "text", text: "hello" }], }); const turnId = response.turn.id; await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turnId); const methods = h.messages.map((message) => message.method).filter(Boolean); assert.ok(methods.includes("turn/started")); assert.ok(methods.includes("item/started")); assert.ok(methods.includes("item/agentMessage/delta")); assert.ok(methods.includes("item/completed")); assert.ok(methods.includes("thread/tokenUsage/updated")); const usageMsg = h.messages.find((message) => message.method === "thread/tokenUsage/updated"); assert.ok(usageMsg.params.tokenUsage); assert.ok(usageMsg.params.tokenUsage.modelContextWindow > 0); const completed = h.messages.find((message) => message.method === "turn/completed" && message.params.turn.id === turnId); assert.equal(completed.params.turn.status, "completed"); const agent = completed.params.turn.items.find((item) => item.type === "agentMessage"); assert.match(agent.text, /Mock OMP response: hello/); })); test("tool user-input requests use the complete Desktop schema and return answers", () => { const base = new MockBackendFactory(); let clientRequest = null; const backendFactory = { listModels: (...args) => base.listModels(...args), async openSession() { return { async runTurn(_prompt, context) { const answer = await context.requestInput({ itemId: "ask-item", questions: [{ id: "vsock", header: "vsock blocked", question: "Load vhost_vsock?", options: [{ label: "loaded", description: "Continue the test." }], }], }); await context.emit({ type: "textDelta", delta: `answer:${answer}` }); return { status: "completed" }; }, async getPersistentState() { return {}; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory, clientResponse(message) { if (message.method === "item/tool/requestUserInput") { clientRequest = message; return { answers: { vsock: { answers: ["loaded"] } } }; } return { decision: "accept" }; }, }, async (h) => { await h.initialize(); const started = await h.request("thread/start", { cwd: h.temp, model: "mock/omp" }); const response = await h.request("turn/start", { threadId: started.thread.id, input: [{ type: "text", text: "ask me" }], }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params?.turn?.id === response.turn.id ); assert.ok(clientRequest); assert.deepEqual(clientRequest.params, { threadId: started.thread.id, turnId: response.turn.id, itemId: "ask-item", questions: [{ id: "vsock", header: "vsock blocked", question: "Load vhost_vsock?", isOther: true, isSecret: false, options: [{ label: "loaded", description: "Continue the test." }], }], isBlocking: true, autoResolutionMs: null, }); assert.match(completed.params.turn.items.find((item) => item.type === "agentMessage").text, /answer:loaded/); const resolved = h.messages.find((message) => message.method === "serverRequest/resolved" && message.params?.threadId === started.thread.id ); assert.deepEqual(resolved?.params, { threadId: started.thread.id, requestId: clientRequest.id, }); }); }); test("a terminal session marker prevents cancelled external asks from blocking the next turn", () => withHarness({}, async (h) => { await h.initialize(); const sessionFile = path.join(h.temp, "cancelled-ask.jsonl"); await fs.writeFile(sessionFile, [ JSON.stringify({ type: "session", id: "cancelled-ask-owner", timestamp: new Date().toISOString(), cwd: h.temp }), JSON.stringify({ type: "title", title: "Cancelled ask" }), JSON.stringify({ type: "message", message: { role: "user", content: [{ type: "text", text: "first" }] } }), JSON.stringify({ type: "message", message: { role: "toolResult", toolName: "ask", isError: true, content: [{ type: "text", text: "Ask input was cancelled" }] } }), JSON.stringify({ type: "custom", customType: "session_exit", data: { reason: "sigterm" } }), ].join("\n") + "\n"); const thread = await h.store.createThread({ threadId: "cancelled-ask-thread", cwd: h.temp, preview: "Cancelled ask", status: { type: "active", activeFlags: [] }, }); await h.store.setBackendState(thread.id, { sessionId: "cancelled-ask-owner", sessionPath: sessionFile, resume: { sessionId: "cancelled-ask-owner", sessionPath: sessionFile }, }); const started = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "continue after cancellation" }], }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params?.turn?.id === started.turn.id ); assert.equal(completed.params.turn.status, "completed"); })); test("interrupt aborts an active backend turn", () => withHarness({ chunkDelayMs: 15 }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "/slow" }] }); await h.waitFor((message) => message.method === "item/agentMessage/delta"); await h.request("turn/interrupt", { threadId: thread.id, turnId: turn.id }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(completed.params.turn.status, "interrupted"); })); test("server shutdown persists deferred active-turn output", () => withHarness({ chunkDelayMs: 25 }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "/slow" }], }); await h.waitFor((message) => message.method === "item/agentMessage/delta"); await h.server.close(); const persisted = await new StateStore(h.store.file).getThread(thread.id); assert.equal(persisted.status.type, "idle"); const persistedTurn = persisted.turns.find((candidate) => candidate.id === turn.id); assert.equal(persistedTurn.status, "interrupted"); assert.ok(persistedTurn.items.find((item) => item.type === "agentMessage")?.text); })); test("interrupted rich tool items complete with a valid failed status", () => { let disposeCount = 0; const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { const aborted = new Promise((resolve) => context.signal.addEventListener("abort", resolve, { once: true })); await context.emit({ type: "toolStart", itemType: "dynamicToolCall", toolCallId: "dynamic-1", toolName: "read", args: { path: "file.txt" } }); await aborted; return { status: "interrupted" }; }, async interrupt() {}, async dispose() { disposeCount += 1; }, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "inspect" }] }); await h.waitFor((message) => message.method === "item/started" && message.params.item.type === "dynamicToolCall"); await h.request("turn/interrupt", { threadId: thread.id, turnId: turn.id }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(completed.params.turn.items.find((item) => item.type === "dynamicToolCall").status, "failed"); assert.equal(disposeCount, 1); }); }); test("a delayed interrupt cannot cancel a newer turn", () => withHarness({ chunkDelayMs: 15 }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn: firstTurn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "first" }] }); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === firstTurn.id); const { turn: secondTurn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "/slow" }] }); await h.waitFor((message) => message.method === "item/agentMessage/delta" && message.params.turnId === secondTurn.id); await assert.rejects( h.request("turn/interrupt", { threadId: thread.id, turnId: firstTurn.id }), /Turn not found/ ); assert.equal(h.messages.some((message) => message.method === "turn/completed" && message.params.turn.id === secondTurn.id), false); await h.request("turn/interrupt", { threadId: thread.id, turnId: secondTurn.id }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === secondTurn.id); assert.equal(completed.params.turn.status, "interrupted"); })); test("ephemeral title sessions dispose their backend exactly once", () => { let disposeCount = 0; const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(prompt, context) { const interrupted = String(prompt).includes("interrupt"); const aborted = interrupted ? new Promise((resolve) => context.signal.addEventListener("abort", resolve, { once: true })) : null; await context.emit({ type: "textDelta", delta: "short title" }); if (aborted) { await aborted; return { status: "interrupted" }; } return { status: "completed" }; }, async interrupt() {}, async dispose() { disposeCount += 1; }, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp, ephemeral: true }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "title" }] }); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(disposeCount, 1); const { thread: interruptedThread } = await h.request("thread/start", { cwd: h.temp, ephemeral: true }); const { turn: interruptedTurn } = await h.request("turn/start", { threadId: interruptedThread.id, input: [{ type: "text", text: "interrupt" }] }); await h.waitFor((message) => message.method === "item/agentMessage/delta" && message.params.turnId === interruptedTurn.id); await h.request("turn/interrupt", { threadId: interruptedThread.id, turnId: interruptedTurn.id }); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === interruptedTurn.id); assert.equal(disposeCount, 2); }); }); test("command approval is a server-initiated request", () => withHarness({ approval: "accept" }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "/approval" }] }); const approval = await h.waitFor((message) => message.method === "item/commandExecution/requestApproval"); assert.equal(approval.params.threadId, thread.id); assert.equal(approval.params.turnId, turn.id); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const command = completed.params.turn.items.find((item) => item.type === "commandExecution"); assert.equal(command.status, "completed"); assert.match(command.aggregatedOutput, /bridge tool output/); })); test("file changes use native fileChange items and diff notifications", () => withHarness({ approval: "accept" }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "/file" }] }); await h.waitFor((message) => message.method === "item/fileChange/requestApproval"); const diff = await h.waitFor((message) => message.method === "turn/diff/updated"); assert.match(diff.params.diff, /hello from OMP/); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(completed.params.turn.items.find((item) => item.type === "fileChange").status, "completed"); })); test("thread goals persist, notify Desktop, account usage, and clear", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const set = await h.request("thread/goal/set", { threadId: thread.id, objective: "finish rich protocol support", status: "active", tokenBudget: 500, }); assert.equal(set.goal.threadId, thread.id); assert.equal(set.goal.objective, "finish rich protocol support"); assert.equal(set.goal.status, "active"); assert.equal(set.goal.tokenBudget, 500); assert.ok(set.goal.createdAt < 10_000_000_000); assert.ok(set.goal.updatedAt < 10_000_000_000); const messagesBeforeResume = h.messages.length; await h.request("thread/resume", { threadId: thread.id }); await new Promise((resolve) => setTimeout(resolve, 10)); assert.ok(h.messages.slice(messagesBeforeResume).some((message) => message.method === "thread/goal/updated" && message.params.goal.objective === set.goal.objective )); assert.ok(h.messages.some((message) => message.method === "thread/goal/updated" && message.params.goal.objective === set.goal.objective)); const read = await h.request("thread/goal/get", { threadId: thread.id }); assert.deepEqual(read.goal, set.goal); const forked = await h.request("thread/fork", { threadId: thread.id, cwd: h.temp }); assert.equal((await h.request("thread/goal/get", { threadId: forked.thread.id })).goal, null); const deferredFork = await h.request("thread/fork", { threadId: thread.id, cwd: h.temp, deferGoalContinuation: true, }); const forkedGoal = await h.request("thread/goal/get", { threadId: deferredFork.thread.id }); assert.equal(forkedGoal.goal.threadId, deferredFork.thread.id); assert.equal(forkedGoal.goal.objective, set.goal.objective); await assert.rejects( h.request("thread/fork", { threadId: thread.id, deferGoalContinuation: true, ephemeral: true }), /cannot be combined/ ); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "continue" }], }); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const accounted = await h.request("thread/goal/get", { threadId: thread.id }); assert.ok(accounted.goal.tokensUsed > 0); assert.ok(accounted.goal.updatedAt >= set.goal.updatedAt); const budgetLimited = await h.request("thread/goal/set", { threadId: thread.id, objective: null, status: "active", tokenBudget: accounted.goal.tokensUsed, }); assert.equal(budgetLimited.goal.objective, set.goal.objective); assert.equal(budgetLimited.goal.status, "budgetLimited"); const cleared = await h.request("thread/goal/clear", { threadId: thread.id }); assert.equal(cleared.cleared, true); assert.equal((await h.request("thread/goal/get", { threadId: thread.id })).goal, null); assert.ok(h.messages.some((message) => message.method === "thread/goal/cleared" && message.params.threadId === thread.id)); })); test("goal accounting includes every model call in a tool-using turn", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "tokenUsage", usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 } }); await context.emit({ type: "tokenUsage", contextUsage: { tokens: 900_000, contextWindow: 1_000_000 } }); await context.emit({ type: "tokenUsage", usage: { inputTokens: 30, outputTokens: 10, totalTokens: 40 } }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); await h.request("thread/goal/set", { threadId: thread.id, objective: "account all calls" }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "continue" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(completed.params.turn.tokenUsage.totalTokens, 70); assert.equal((await h.request("thread/goal/get", { threadId: thread.id })).goal.tokensUsed, 70); }); }); test("rich OMP tools use native Desktop item types and plan UI", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "toolStart", itemType: "dynamicToolCall", toolCallId: "read-1", toolName: "read", namespace: "oh-my-pi", args: { path: "src/app-server.mjs" } }); await context.emit({ type: "toolEnd", toolCallId: "read-1", status: "completed", outputText: "file contents", output: { content: [{ type: "text", text: "file contents" }, { type: "image", data: "aGVsbG8=", mimeType: "image/png" }] }, durationMs: 4, }); await context.emit({ type: "toolStart", itemType: "webSearch", toolCallId: "web-1", toolName: "web_search", args: { query: "Codex protocol" } }); await context.emit({ type: "toolEnd", toolCallId: "web-1", status: "completed", output: { results: [{ title: "Protocol" }] } }); await context.emit({ type: "toolStart", itemType: "mcpToolCall", toolCallId: "mcp-1", toolName: "lookup", server: "docs", args: { id: 1 } }); await context.emit({ type: "toolDelta", toolCallId: "mcp-1", delta: "working" }); await context.emit({ type: "toolEnd", toolCallId: "mcp-1", status: "completed", outputText: "done", output: { content: [{ type: "text", text: "rendered" }], details: { rawContent: [{ type: "text", text: "done" }, { type: "resource", resource: { uri: "docs://one", text: "resource" } }], mcpMeta: { trace: "abc" }, }, }, durationMs: 3, }); await context.emit({ type: "toolStart", itemType: "imageView", toolCallId: "image-1", toolName: "inspect_image", args: { path: "image.png" } }); await context.emit({ type: "toolEnd", toolCallId: "image-1", status: "completed" }); await context.emit({ type: "planUpdate", explanation: "native plan", plan: [{ step: "verify UI", status: "inProgress" }] }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "use tools" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const types = completed.params.turn.items.map((item) => item.type); assert.ok(types.includes("dynamicToolCall")); assert.ok(types.includes("webSearch")); assert.ok(types.includes("mcpToolCall")); assert.ok(types.includes("imageView")); const dynamic = completed.params.turn.items.find((item) => item.type === "dynamicToolCall"); assert.deepEqual(dynamic.contentItems, [ { type: "inputText", text: "file contents" }, { type: "inputImage", imageUrl: "data:image/png;base64,aGVsbG8=" }, ]); const mcp = completed.params.turn.items.find((item) => item.type === "mcpToolCall"); assert.equal(mcp.result.content[1].type, "resource"); assert.deepEqual(mcp.result._meta, { trace: "abc" }); assert.equal(completed.params.turn.items.find((item) => item.type === "imageView").status, undefined); assert.ok(h.messages.some((message) => message.method === "item/mcpToolCall/progress")); assert.deepEqual(h.messages.find((message) => message.method === "turn/plan/updated").params.plan, [ { step: "verify UI", status: "inProgress" }, ]); }); }); test("thread metadata persists and paginates", () => withHarness({}, async (h) => { await h.initialize(); const one = await h.request("thread/start", { cwd: h.temp }); const two = await h.request("thread/start", { cwd: h.temp }); await h.request("thread/name/set", { threadId: one.thread.id, name: "persisted name" }); const page1 = await h.request("thread/list", { limit: 1 }); assert.equal(page1.data.length, 1); assert.ok(page1.nextCursor); const page2 = await h.request("thread/list", { limit: 1, cursor: page1.nextCursor }); assert.equal(page2.data.length, 1); assert.notEqual(page1.data[0].id, page2.data[0].id); const read = await h.request("thread/read", { threadId: one.thread.id }); assert.equal(read.thread.name, "persisted name"); assert.equal(page1.data[0].id, one.thread.id); })); test("unknown methods fail explicitly", () => withHarness({}, async (h) => { await h.initialize(); const id = 999; await h.server.handle(h.peer, { id, method: "unknown/method", params: {} }); const response = h.messages.find((message) => message.id === id); assert.equal(response.error.code, -32601); })); test("thread/compact/start triggers contextCompaction turn and returns immediately", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const compactResponse = await h.request("thread/compact/start", { threadId: thread.id }); assert.deepEqual(compactResponse, {}); const completed = await h.waitFor((message) => message.method === "turn/completed"); const compactionItem = completed.params.turn.items.find((item) => item.type === "contextCompaction"); assert.ok(compactionItem); const compactedNotif = h.messages.find((message) => message.method === "thread/compacted"); assert.ok(compactedNotif); assert.equal(compactedNotif.params.threadId, thread.id); })); test("thread/fork creates a child thread and copies turns", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const turn1 = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "turn 1" }] }); await h.waitFor((m) => m.method === "turn/completed" && m.params.turn.id === turn1.turn.id); const turn2 = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "turn 2" }] }); await h.waitFor((m) => m.method === "turn/completed" && m.params.turn.id === turn2.turn.id); const forked = await h.request("thread/fork", { threadId: thread.id, lastTurnId: turn1.turn.id }); assert.equal(forked.thread.forkedFromId, thread.id); assert.equal(forked.thread.parentThreadId, thread.id); assert.equal(forked.thread.turns.length, 1); assert.equal(forked.thread.turns[0].id, turn1.turn.id); const sideFork = await h.request("thread/fork", { threadId: thread.id, cwd: h.temp, excludeTurns: true, ephemeral: true, }); assert.equal(sideFork.thread.turns.length, 0); const boundary = { type: "message", role: "user", content: [{ type: "input_text", text: "Side conversation boundary." }], }; assert.deepEqual(await h.request("thread/inject_items", { threadId: sideFork.thread.id, items: [boundary], }), {}); assert.deepEqual(h.server.sessions.get(sideFork.thread.id).injectedItems, [boundary]); })); test("thread resume returns the requested recent page and paginates older turns", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const turnIds = []; for (let index = 1; index <= 3; index += 1) { const started = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: `turn ${index}` }], }); turnIds.push(started.turn.id); await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === started.turn.id); } const resumed = await h.request("thread/resume", { threadId: thread.id, excludeTurns: true, initialTurnsPage: { limit: 2, itemsView: "full", sortDirection: "desc" }, }); assert.deepEqual(resumed.thread.turns, []); assert.deepEqual(resumed.initialTurnsPage.data.map((turn) => turn.id), [turnIds[2], turnIds[1]]); assert.ok(resumed.initialTurnsPage.data.every((turn) => turn.itemsView === "full" && turn.items.length > 0)); assert.ok(resumed.initialTurnsPage.nextCursor); const older = await h.request("thread/turns/list", { threadId: thread.id, cursor: resumed.initialTurnsPage.nextCursor, limit: 2, itemsView: "notLoaded", sortDirection: "desc", }); assert.deepEqual(older.data.map((turn) => turn.id), [turnIds[0]]); assert.deepEqual(older.data[0].items, []); assert.equal(older.data[0].itemsView, "notLoaded"); assert.equal(older.nextCursor, null); assert.ok(older.backwardsCursor); const reversed = await h.request("thread/turns/list", { threadId: thread.id, cursor: older.backwardsCursor, limit: 2, itemsView: "notLoaded", sortDirection: "asc", }); assert.deepEqual(reversed.data.map((turn) => turn.id), [turnIds[0], turnIds[1]]); const summary = await h.request("thread/turns/list", { threadId: thread.id, limit: 1, sortDirection: "desc", }); assert.equal(summary.data[0].itemsView, "summary"); assert.deepEqual(summary.data[0].items.map((item) => item.type), ["userMessage", "agentMessage"]); const items = await h.request("thread/items/list", { threadId: thread.id, turnId: turnIds[0], limit: 100, }); assert.ok(items.data.some((entry) => entry.turnId === turnIds[0] && entry.item.type === "userMessage")); assert.equal(items.nextCursor, null); assert.ok(items.backwardsCursor); const allItems = await h.request("thread/items/list", { threadId: thread.id, limit: 2, sortDirection: "desc", }); assert.equal(allItems.data.length, 2); assert.ok(allItems.data.every((entry) => entry.turnId && entry.item?.id)); assert.ok(allItems.nextCursor); await assert.rejects( h.request("thread/turns/list", { threadId: thread.id, cursor: "invalid", limit: 1 }), /Invalid thread-turn cursor/ ); })); test("thread/rollback drops turns and returns updated thread", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const turn1 = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "turn 1" }] }); await h.waitFor((m) => m.method === "turn/completed" && m.params.turn.id === turn1.turn.id); const turn2 = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "turn 2" }] }); await h.waitFor((m) => m.method === "turn/completed" && m.params.turn.id === turn2.turn.id); const rolledBack = await h.request("thread/rollback", { threadId: thread.id, numTurns: 1 }); assert.equal(rolledBack.thread.turns.length, 1); assert.equal(rolledBack.thread.turns[0].id, turn1.turn.id); })); test("account/login/start and account/logout flow cleanly", () => withHarness({}, async (h) => { await h.initialize(); const loginRes = await h.request("account/login/start", { type: "chatgptAuthTokens" }); assert.equal(loginRes.type, "chatgptAuthTokens"); const completed = await h.waitFor((m) => m.method === "account/login/completed"); assert.equal(completed.params.success, true); const updated = await h.waitFor((m) => m.method === "account/updated"); assert.equal(updated.params.authMode, "apikey"); const logoutRes = await h.request("account/logout", {}); assert.deepEqual(logoutRes, {}); })); test("config/read returns a schema-complete typed config for partial user TOML", () => withHarness({}, async (h) => { const codexHome = path.join(h.temp, "codex-home"); await fs.mkdir(codexHome, { recursive: true }); await fs.writeFile(path.join(codexHome, "config.toml"), [ "[features]", "js_repl = true", "", "[memories]", "use_memories = false", "", "[shell_environment_policy.set]", "EXAMPLE = \"1\"", "", "[tui]", "notifications = false", "", "[tui.model_availability_nux]", "\"gpt-5.6-sol\" = 1", "", "[hooks.state.\"/tmp/hooks.json:pre_compact:0:0\"]", "trusted_hash = \"sha256:abc\"", "", "[marketplaces.openai-bundled]", "source_type = \"local\"", "source = \"/tmp/bundled\"", "", "[mcp_servers.node_repl]", "command = \"/usr/bin/true\"", "", "[model_providers.empty-provider]", "", ].join("\n")); const previousHome = process.env.CODEX_HOME; process.env.CODEX_HOME = codexHome; try { await h.initialize(); const { config, ...envelope } = await h.request("config/read", { cwd: h.temp }); assert.deepEqual(Object.keys(envelope), ["origins"]); for (const bridgeOnly of ["backend", "stateFile", "configFile", "cwd", "modelAliases", "defaultModel"]) { assert.equal(Object.hasOwn(config, bridgeOnly), false, `${bridgeOnly} must not leak into the typed config`); } assert.deepEqual(Object.keys(config.features).sort(), [ "auth_elicitation", "js_repl", "memories", "mentions_v2", "network_proxy", "remote_control", "remote_plugin", "tool_suggest", ]); assert.equal(config.features.js_repl, true); assert.equal(config.memories.use_memories, false); assert.equal(Object.keys(config.memories).length, 12); assert.equal(Object.hasOwn(config.memories, "consolidation_model"), true); assert.deepEqual(Object.keys(config.shell_environment_policy).sort(), [ "exclude", "experimental_use_profile", "ignore_default_excludes", "include_only", "inherit", "set", ]); assert.deepEqual(config.shell_environment_policy.set, { EXAMPLE: "1" }); assert.equal(config.tui.notifications, false); assert.equal(config.tui.pet_anchor, "composer"); assert.deepEqual(config.tui.model_availability_nux, { "gpt-5.6-sol": 1 }); assert.equal(Object.hasOwn(config.tui, "terminal_resize_reflow_max_rows"), true); assert.equal(Object.hasOwn(config.tui.keymap.global, "open_transcript"), true); assert.equal(Object.hasOwn(config.tui.keymap.vim_text_object, "backtick"), true); assert.deepEqual(config.hooks.PreToolUse, []); assert.deepEqual(config.hooks.Stop, []); assert.deepEqual(config.hooks.state["/tmp/hooks.json:pre_compact:0:0"], { trusted_hash: "sha256:abc" }); assert.deepEqual(config.marketplaces["openai-bundled"], { last_updated: null, last_revision: null, source_type: "local", source: "/tmp/bundled", ref: null, sparse_paths: null, }); assert.deepEqual(config.mcp_servers.node_repl, { command: "/usr/bin/true", args: [], environment_id: "local", enabled: true, tool_timeout_sec: null, }); assert.equal(config.model_provider, null, "an incomplete provider table must not be advertised"); const withLayers = await h.request("config/read", { cwd: h.temp, includeLayers: true }); assert.equal(Array.isArray(withLayers.layers), true); } finally { if (previousHome === undefined) delete process.env.CODEX_HOME; else process.env.CODEX_HOME = previousHome; } })); test("a compaction turn is stored as completed, not left running forever", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); await h.request("thread/compact/start", { threadId: thread.id }); await h.waitFor((message) => message.method === "thread/compacted"); await h.waitFor((message) => message.method === "turn/completed"); const stored = await h.store.getThread(thread.id); const compaction = stored.turns.find((turn) => (turn.items || []).some((item) => item.type === "contextCompaction")); assert.equal(compaction.status, "completed"); assert.ok(compaction.completedAt); })); test("resuming a thread settles turns nobody is running and reports idle", () => withHarness({}, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); await h.store.addTurn(thread.id, { id: "turn_abandoned", status: "inProgress", startedAt: 1, completedAt: null, items: [ { id: "user-1", type: "userMessage", content: [{ type: "text", text: "hi" }], status: "completed" }, { id: "cmd-1", type: "commandExecution", command: "read a.ts", status: "inProgress", exitCode: null, durationMs: null }, ], }); await h.store.updateThread(thread.id, (stored) => { stored.status = { type: "active", activeFlags: [] }; }); const resumed = await h.request("thread/resume", { threadId: thread.id }); assert.deepEqual(resumed.thread.status, { type: "idle" }); const settled = (await h.store.getThread(thread.id)).turns.find((turn) => turn.id === "turn_abandoned"); assert.equal(settled.status, "interrupted"); assert.equal(settled.items.find((item) => item.id === "cmd-1").status, "failed"); assert.equal(settled.items.find((item) => item.id === "cmd-1").exitCode, 1); const announced = await h.waitFor((message) => message.method === "turn/completed" && message.params?.turn?.id === "turn_abandoned"); assert.equal(announced.params.turn.status, "interrupted"); })); test("a live turn is never settled out from under its client", () => withHarness({ chunkDelayMs: 40 }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "work" }] }); await h.request("thread/resume", { threadId: thread.id }); const live = (await h.store.getThread(thread.id)).turns.find((candidate) => candidate.id === turn.id); assert.equal(live.status, "inProgress"); await h.waitFor((message) => message.method === "turn/completed" && message.params?.turn?.id === turn.id); })); test("an answer that arrives whole instead of as deltas still reaches the client", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "reasoningDelta", delta: "**Weighing it up**" }); await context.emit({ type: "assistantThinking", thinking: "**Weighing it up**\nmore detail" }); await context.emit({ type: "assistantText", text: "yep, tantivy does the heavy lifting" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "ask" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const items = completed.params.turn.items; const agent = items.filter((item) => item.type === "agentMessage"); assert.equal(agent.length, 1, `expected exactly one answer, got ${JSON.stringify(items.map((i) => i.type))}`); assert.equal(agent[0].text, "yep, tantivy does the heavy lifting"); const reasoning = items.find((item) => item.type === "reasoning"); assert.deepEqual(reasoning.summary, ["**Weighing it up**\nmore detail"], "the closing block must extend, not duplicate, the streamed thinking"); assert.deepEqual(reasoning.content, [], "omp thinking is a summary, not raw chain-of-thought"); const delta = h.messages.find((message) => message.method === "item/agentMessage/delta"); assert.equal(delta.params.delta, "yep, tantivy does the heavy lifting", "clients must see the answer stream in"); }); }); test("a streamed answer confirmed by its closing block is not duplicated", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "textDelta", delta: "half " }); await context.emit({ type: "textDelta", delta: "an answer" }); await context.emit({ type: "assistantText", text: "half an answer" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "ask" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const agent = completed.params.turn.items.filter((item) => item.type === "agentMessage"); assert.equal(agent.length, 1); assert.equal(agent[0].text, "half an answer"); }); }); test("cumulative answer snapshots append only their unseen tails", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "assistantText", text: "nothing," }); await context.emit({ type: "assistantText", text: "nothing, just a typo." }); await context.emit({ type: "assistantText", text: "nothing, just a typo. i meant “moved.”" }); await context.emit({ type: "assistantMessageEnd" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "ask" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const agent = completed.params.turn.items.filter((item) => item.type === "agentMessage"); assert.equal(agent.length, 1); assert.equal(agent[0].text, "nothing, just a typo. i meant “moved.”"); }); }); test("terminal backend errors use the native error UI and fail the turn", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "turnError", message: "provider rejected the API key with 401 unauthorized", additionalDetails: "credential 2 of 2", willRetry: false, terminal: true, }); throw new Error("OMP RPC process exited after reporting the provider error"); }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "ask" }] }); const error = await h.waitFor((message) => message.method === "error" && message.params.turnId === turn.id); assert.equal(error.params.willRetry, false); assert.equal(error.params.error.codexErrorInfo, "unauthorized"); assert.equal(error.params.error.additionalDetails, "credential 2 of 2"); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); assert.equal(completed.params.turn.status, "failed"); assert.deepEqual(completed.params.turn.error, error.params.error); assert.equal(h.messages.filter((message) => message.method === "error" && message.params?.turnId === turn.id).length, 1); }); }); test("thinking from several messages in one turn is neither doubled nor glued", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "reasoningDelta", delta: "**Verifying rustfmt availability**" }); await context.emit({ type: "assistantThinking", thinking: "**Verifying rustfmt availability**" }); await context.emit({ type: "assistantMessageEnd" }); await context.emit({ type: "toolStart", itemType: "commandExecution", toolCallId: "cmd-1", toolName: "bash", command: "which rustfmt" }); await context.emit({ type: "toolEnd", toolCallId: "cmd-1", status: "completed", outputText: "" }); await context.emit({ type: "reasoningDelta", delta: "**Running cargo build**" }); await context.emit({ type: "assistantThinking", thinking: "**Running cargo build**" }); await context.emit({ type: "assistantText", text: "built it" }); await context.emit({ type: "assistantMessageEnd" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "format it" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const reasoning = completed.params.turn.items.filter((item) => item.type === "reasoning"); assert.equal(reasoning.length, 1, "one turn must keep one stable thinking card across tools"); assert.deepEqual(reasoning[0].summary, [ "**Verifying rustfmt availability**", "**Running cargo build**", ]); assert.deepEqual(reasoning[0].content, []); assert.equal(h.messages.some((message) => message.method === "item/reasoning/textDelta"), false); const summaryDeltas = h.messages.filter((message) => message.method === "item/reasoning/summaryTextDelta"); assert.deepEqual(summaryDeltas.map((message) => message.params.summaryIndex), [0, 1]); assert.equal(new Set(summaryDeltas.map((message) => message.params.itemId)).size, 1); const partAdded = h.messages.find((message) => message.method === "item/reasoning/summaryPartAdded"); assert.equal(partAdded.params.summaryIndex, 1); assert.equal(completed.params.turn.items.filter((item) => item.type === "agentMessage").length, 1); }); }); test("a closing block that arrives after its item closed does not reopen it", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "reasoningDelta", delta: "**Checking API visibility**" }); await context.emit({ type: "toolStart", itemType: "commandExecution", toolCallId: "cmd-1", toolName: "bash", command: "jj status" }); await context.emit({ type: "toolEnd", toolCallId: "cmd-1", status: "completed", outputText: "" }); // late closing blocks must not seed the next reasoning card await context.emit({ type: "assistantThinking", thinking: "**Checking API visibility**" }); await context.emit({ type: "assistantMessageEnd" }); await context.emit({ type: "reasoningDelta", delta: "**Planning the rewrite**" }); await context.emit({ type: "assistantMessageEnd" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "go" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const reasoning = completed.params.turn.items.filter((item) => item.type === "reasoning"); assert.equal(reasoning.length, 1); assert.deepEqual(reasoning[0].summary, [ "**Checking API visibility**", "**Planning the rewrite**", ]); assert.deepEqual(reasoning[0].content, []); }); }); test("a closing thinking snapshot after message end is not projected twice", () => { const backendFactory = { async listModels() { return [{ id: "mock/omp", model: "mock/omp" }]; }, async openSession() { return { async runTurn(_prompt, context) { await context.emit({ type: "reasoningDelta", delta: "**Checking the protocol**" }); await context.emit({ type: "assistantMessageEnd" }); await context.emit({ type: "assistantThinking", thinking: "**Checking the protocol**" }); return { status: "completed" }; }, async interrupt() {}, async dispose() {}, }; }, }; return withHarness({ backendFactory }, async (h) => { await h.initialize(); const { thread } = await h.request("thread/start", { cwd: h.temp }); const { turn } = await h.request("turn/start", { threadId: thread.id, input: [{ type: "text", text: "check" }] }); const completed = await h.waitFor((message) => message.method === "turn/completed" && message.params.turn.id === turn.id); const reasoning = completed.params.turn.items.filter((item) => item.type === "reasoning"); assert.equal(reasoning.length, 1); assert.deepEqual(reasoning[0].summary, ["**Checking the protocol**"]); assert.equal(h.messages.filter((message) => message.method === "item/reasoning/summaryTextDelta").length, 1); }); });