import { describe, expect, it, vi } from "vitest";import { randomUUID } from "node:crypto";import { mkdtempSync, rmSync, writeFileSync } from "node:fs";import os from "node:os";import path from "node:path";import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; import { buildCodexInitializeParams, CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, CodexAppServerManager, classifyCodexStderrLine, isRecoverableThreadResumeError, normalizeCodexModelSlug, readCodexAccountSnapshot, resolveCodexModelForAccount,} from "./codexAppServerManager"; const asThreadId = (value: string): ThreadId => ThreadId.make(value); function createSendTurnHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, account: { type: "unknown", planType: null, sparkEnabled: true, }, collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const sendRequest = vi .spyOn( manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> }, "sendRequest", ) .mockResolvedValue({ turn: { id: "turn_1", }, }); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, requireSession, sendRequest, updateSession };} function createThreadControlHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const sendRequest = vi.spyOn( manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> }, "sendRequest", ); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, requireSession, sendRequest, updateSession };} function createPendingUserInputHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, pendingUserInputs: new Map([ [ ApprovalRequestId.make("req-user-input-1"), { requestId: ApprovalRequestId.make("req-user-input-1"), jsonRpcId: 42, threadId: asThreadId("thread_1"), }, ], ]), collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const writeMessage = vi .spyOn(manager as unknown as { writeMessage: (...args: unknown[]) => void }, "writeMessage") .mockImplementation(() => {}); const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); return { manager, context, requireSession, writeMessage, emitEvent };} function createCollabNotificationHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "running", threadId: asThreadId("thread_1"), runtimeMode: "full-access", model: "gpt-5.3-codex", activeTurnId: "turn_parent", resumeCursor: { threadId: "provider_parent" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, account: { type: "unknown", planType: null, sparkEnabled: true, }, pending: new Map(), pendingApprovals: new Map(), pendingUserInputs: new Map(), collabReceiverTurns: new Map<string, string>(), nextRequestId: 1, stopping: false, }; const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, emitEvent, updateSession };} describe("classifyCodexStderrLine", () => { it("ignores empty lines", () => { expect(classifyCodexStderrLine(" ")).toBeNull(); }); it("ignores non-error structured codex logs", () => { const line = "2026-02-08T04:24:19.241256Z WARN codex_core::features: unknown feature key in config: skills"; expect(classifyCodexStderrLine(line)).toBeNull(); }); it("ignores known benign rollout path errors", () => { const line = "\u001b[2m2026-02-08T04:24:20.085687Z\u001b[0m \u001b[31mERROR\u001b[0m \u001b[2mcodex_core::rollout::list\u001b[0m: state db missing rollout path for thread 019c3b6c-46b8-7b70-ad23-82f824d161fb"; expect(classifyCodexStderrLine(line)).toBeNull(); }); it("keeps unknown structured errors", () => { const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; expect(classifyCodexStderrLine(line)).toEqual({ message: line, }); }); it("keeps plain stderr messages", () => { const line = "fatal: permission denied"; expect(classifyCodexStderrLine(line)).toEqual({ message: line, }); });}); describe("process stderr events", () => { it("emits classified stderr lines as notifications", () => { const manager = new CodexAppServerManager(); const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); ( manager as unknown as { emitNotificationEvent: ( context: { session: { threadId: ThreadId } }, method: string, message: string, ) => void; } ).emitNotificationEvent( { session: { threadId: asThreadId("thread-1"), }, }, "process/stderr", "fatal: permission denied", ); expect(emitEvent).toHaveBeenCalledWith( expect.objectContaining({ kind: "notification", method: "process/stderr", threadId: "thread-1", message: "fatal: permission denied", }), ); });}); describe("normalizeCodexModelSlug", () => { it("maps 5.3 aliases to gpt-5.3-codex", () => { expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex"); expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); }); it("prefers codex id when model differs", () => { expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); }); it("keeps non-aliased models as-is", () => { expect(normalizeCodexModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); expect(normalizeCodexModelSlug("gpt-5.2")).toBe("gpt-5.2"); });}); describe("isRecoverableThreadResumeError", () => { it("matches not-found resume errors", () => { expect( isRecoverableThreadResumeError(new Error("thread/resume failed: thread not found")), ).toBe(true); }); it("ignores non-resume errors", () => { expect( isRecoverableThreadResumeError(new Error("thread/start failed: permission denied")), ).toBe(false); }); it("ignores non-recoverable resume errors", () => { expect( isRecoverableThreadResumeError( new Error("thread/resume failed: timed out waiting for server"), ), ).toBe(false); });}); describe("readCodexAccountSnapshot", () => { it("disables spark for chatgpt plus accounts", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "plus@example.com", planType: "plus", }), ).toEqual({ type: "chatgpt", planType: "plus", sparkEnabled: false, }); }); it("keeps spark enabled for chatgpt pro accounts", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "pro@example.com", planType: "pro", }), ).toEqual({ type: "chatgpt", planType: "pro", sparkEnabled: true, }); }); it("disables spark for api key accounts", () => { expect( readCodexAccountSnapshot({ type: "apiKey", }), ).toEqual({ type: "apiKey", planType: null, sparkEnabled: false, }); }); it("disables spark for unknown chatgpt plans", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "unknown@example.com", }), ).toEqual({ type: "chatgpt", planType: "unknown", sparkEnabled: false, }); });}); describe("resolveCodexModelForAccount", () => { it("falls back from spark to default for unsupported chatgpt plans", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "chatgpt", planType: "plus", sparkEnabled: false, }), ).toBe("gpt-5.3-codex"); }); it("keeps spark for supported plans", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "chatgpt", planType: "pro", sparkEnabled: true, }), ).toBe("gpt-5.3-codex-spark"); }); it("falls back from spark to default for api key auth", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "apiKey", planType: null, sparkEnabled: false, }), ).toBe("gpt-5.3-codex"); });}); describe("startSession", () => { it("enables Codex experimental api capabilities during initialize", () => { expect(buildCodexInitializeParams()).toEqual({ clientInfo: { name: "t3code_desktop", title: "T3 Code Desktop", version: "0.1.0", }, capabilities: { experimentalApi: true, }, }); }); it("emits session/startFailed when resolving cwd throws before process launch", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; manager.on("event", (event) => { events.push({ method: event.method, kind: event.kind, ...(event.message ? { message: event.message } : {}), }); }); const processCwd = vi.spyOn(process, "cwd").mockImplementation(() => { throw new Error("cwd missing"); }); try { await expect( manager.startSession({ threadId: asThreadId("thread-1"), provider: "codex", binaryPath: "codex", runtimeMode: "full-access", }), ).rejects.toThrow("cwd missing"); expect(events).toHaveLength(1); expect(events[0]).toEqual({ method: "session/startFailed", kind: "error", message: "cwd missing", }); } finally { processCwd.mockRestore(); manager.stopAll(); } }); it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; manager.on("event", (event) => { events.push({ method: event.method, kind: event.kind, ...(event.message ? { message: event.message } : {}), }); }); const versionCheck = vi .spyOn( manager as unknown as { assertSupportedCodexCliVersion: (input: { binaryPath: string; cwd: string; homePath?: string; }) => void; }, "assertSupportedCodexCliVersion", ) .mockImplementation(() => { throw new Error( "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", ); }); try { await expect( manager.startSession({ threadId: asThreadId("thread-1"), provider: "codex", binaryPath: "codex", runtimeMode: "full-access", }), ).rejects.toThrow( "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", ); expect(versionCheck).toHaveBeenCalledTimes(1); expect(events).toEqual([ { method: "session/startFailed", kind: "error", message: "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", }, ]); } finally { versionCheck.mockRestore(); manager.stopAll(); } }); it("disposes an existing session before starting a replacement for the same thread", async () => { const manager = new CodexAppServerManager(); const existingContext = { session: { provider: "codex", status: "ready", threadId: asThreadId("thread-1"), runtimeMode: "full-access", createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, }; ( manager as unknown as { sessions: Map<ThreadId, typeof existingContext>; } ).sessions.set(asThreadId("thread-1"), existingContext); const disposeSession = vi .spyOn( manager as unknown as { disposeSession: ( context: typeof existingContext, options?: { readonly emitLifecycleEvent?: boolean }, ) => void; },Lines 1–500 of 1218. Load more
import { describe, expect, it, vi } from "vitest"; import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; import { buildCodexInitializeParams, CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, CodexAppServerManager, classifyCodexStderrLine, isRecoverableThreadResumeError, normalizeCodexModelSlug, readCodexAccountSnapshot, resolveCodexModelForAccount, } from "./codexAppServerManager"; const asThreadId = (value: string): ThreadId => ThreadId.make(value); function createSendTurnHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, account: { type: "unknown", planType: null, sparkEnabled: true, }, collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const sendRequest = vi .spyOn( manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> }, "sendRequest", ) .mockResolvedValue({ turn: { id: "turn_1", }, }); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, requireSession, sendRequest, updateSession }; } function createThreadControlHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const sendRequest = vi.spyOn( manager as unknown as { sendRequest: (...args: unknown[]) => Promise<unknown> }, "sendRequest", ); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, requireSession, sendRequest, updateSession }; } function createPendingUserInputHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "ready", threadId: "thread_1", runtimeMode: "full-access", model: "gpt-5.3-codex", resumeCursor: { threadId: "thread_1" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, pendingUserInputs: new Map([ [ ApprovalRequestId.make("req-user-input-1"), { requestId: ApprovalRequestId.make("req-user-input-1"), jsonRpcId: 42, threadId: asThreadId("thread_1"), }, ], ]), collabReceiverTurns: new Map(), }; const requireSession = vi .spyOn( manager as unknown as { requireSession: (sessionId: string) => unknown }, "requireSession", ) .mockReturnValue(context); const writeMessage = vi .spyOn(manager as unknown as { writeMessage: (...args: unknown[]) => void }, "writeMessage") .mockImplementation(() => {}); const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); return { manager, context, requireSession, writeMessage, emitEvent }; } function createCollabNotificationHarness() { const manager = new CodexAppServerManager(); const context = { session: { provider: "codex", status: "running", threadId: asThreadId("thread_1"), runtimeMode: "full-access", model: "gpt-5.3-codex", activeTurnId: "turn_parent", resumeCursor: { threadId: "provider_parent" }, createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, account: { type: "unknown", planType: null, sparkEnabled: true, }, pending: new Map(), pendingApprovals: new Map(), pendingUserInputs: new Map(), collabReceiverTurns: new Map<string, string>(), nextRequestId: 1, stopping: false, }; const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); const updateSession = vi .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession") .mockImplementation(() => {}); return { manager, context, emitEvent, updateSession }; } describe("classifyCodexStderrLine", () => { it("ignores empty lines", () => { expect(classifyCodexStderrLine(" ")).toBeNull(); }); it("ignores non-error structured codex logs", () => { const line = "2026-02-08T04:24:19.241256Z WARN codex_core::features: unknown feature key in config: skills"; expect(classifyCodexStderrLine(line)).toBeNull(); }); it("ignores known benign rollout path errors", () => { const line = "\u001b[2m2026-02-08T04:24:20.085687Z\u001b[0m \u001b[31mERROR\u001b[0m \u001b[2mcodex_core::rollout::list\u001b[0m: state db missing rollout path for thread 019c3b6c-46b8-7b70-ad23-82f824d161fb"; expect(classifyCodexStderrLine(line)).toBeNull(); }); it("keeps unknown structured errors", () => { const line = "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; expect(classifyCodexStderrLine(line)).toEqual({ message: line, }); }); it("keeps plain stderr messages", () => { const line = "fatal: permission denied"; expect(classifyCodexStderrLine(line)).toEqual({ message: line, }); }); }); describe("process stderr events", () => { it("emits classified stderr lines as notifications", () => { const manager = new CodexAppServerManager(); const emitEvent = vi .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") .mockImplementation(() => {}); ( manager as unknown as { emitNotificationEvent: ( context: { session: { threadId: ThreadId } }, method: string, message: string, ) => void; } ).emitNotificationEvent( { session: { threadId: asThreadId("thread-1"), }, }, "process/stderr", "fatal: permission denied", ); expect(emitEvent).toHaveBeenCalledWith( expect.objectContaining({ kind: "notification", method: "process/stderr", threadId: "thread-1", message: "fatal: permission denied", }), ); }); }); describe("normalizeCodexModelSlug", () => { it("maps 5.3 aliases to gpt-5.3-codex", () => { expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex"); expect(normalizeCodexModelSlug("gpt-5.3")).toBe("gpt-5.3-codex"); }); it("prefers codex id when model differs", () => { expect(normalizeCodexModelSlug("gpt-5.3", "gpt-5.3-codex")).toBe("gpt-5.3-codex"); }); it("keeps non-aliased models as-is", () => { expect(normalizeCodexModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); expect(normalizeCodexModelSlug("gpt-5.2")).toBe("gpt-5.2"); }); }); describe("isRecoverableThreadResumeError", () => { it("matches not-found resume errors", () => { expect( isRecoverableThreadResumeError(new Error("thread/resume failed: thread not found")), ).toBe(true); }); it("ignores non-resume errors", () => { expect( isRecoverableThreadResumeError(new Error("thread/start failed: permission denied")), ).toBe(false); }); it("ignores non-recoverable resume errors", () => { expect( isRecoverableThreadResumeError( new Error("thread/resume failed: timed out waiting for server"), ), ).toBe(false); }); }); describe("readCodexAccountSnapshot", () => { it("disables spark for chatgpt plus accounts", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "plus@example.com", planType: "plus", }), ).toEqual({ type: "chatgpt", planType: "plus", sparkEnabled: false, }); }); it("keeps spark enabled for chatgpt pro accounts", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "pro@example.com", planType: "pro", }), ).toEqual({ type: "chatgpt", planType: "pro", sparkEnabled: true, }); }); it("disables spark for api key accounts", () => { expect( readCodexAccountSnapshot({ type: "apiKey", }), ).toEqual({ type: "apiKey", planType: null, sparkEnabled: false, }); }); it("disables spark for unknown chatgpt plans", () => { expect( readCodexAccountSnapshot({ type: "chatgpt", email: "unknown@example.com", }), ).toEqual({ type: "chatgpt", planType: "unknown", sparkEnabled: false, }); }); }); describe("resolveCodexModelForAccount", () => { it("falls back from spark to default for unsupported chatgpt plans", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "chatgpt", planType: "plus", sparkEnabled: false, }), ).toBe("gpt-5.3-codex"); }); it("keeps spark for supported plans", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "chatgpt", planType: "pro", sparkEnabled: true, }), ).toBe("gpt-5.3-codex-spark"); }); it("falls back from spark to default for api key auth", () => { expect( resolveCodexModelForAccount("gpt-5.3-codex-spark", { type: "apiKey", planType: null, sparkEnabled: false, }), ).toBe("gpt-5.3-codex"); }); }); describe("startSession", () => { it("enables Codex experimental api capabilities during initialize", () => { expect(buildCodexInitializeParams()).toEqual({ clientInfo: { name: "t3code_desktop", title: "T3 Code Desktop", version: "0.1.0", }, capabilities: { experimentalApi: true, }, }); }); it("emits session/startFailed when resolving cwd throws before process launch", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; manager.on("event", (event) => { events.push({ method: event.method, kind: event.kind, ...(event.message ? { message: event.message } : {}), }); }); const processCwd = vi.spyOn(process, "cwd").mockImplementation(() => { throw new Error("cwd missing"); }); try { await expect( manager.startSession({ threadId: asThreadId("thread-1"), provider: "codex", binaryPath: "codex", runtimeMode: "full-access", }), ).rejects.toThrow("cwd missing"); expect(events).toHaveLength(1); expect(events[0]).toEqual({ method: "session/startFailed", kind: "error", message: "cwd missing", }); } finally { processCwd.mockRestore(); manager.stopAll(); } }); it("fails fast with an upgrade message when codex is below the minimum supported version", async () => { const manager = new CodexAppServerManager(); const events: Array<{ method: string; kind: string; message?: string }> = []; manager.on("event", (event) => { events.push({ method: event.method, kind: event.kind, ...(event.message ? { message: event.message } : {}), }); }); const versionCheck = vi .spyOn( manager as unknown as { assertSupportedCodexCliVersion: (input: { binaryPath: string; cwd: string; homePath?: string; }) => void; }, "assertSupportedCodexCliVersion", ) .mockImplementation(() => { throw new Error( "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", ); }); try { await expect( manager.startSession({ threadId: asThreadId("thread-1"), provider: "codex", binaryPath: "codex", runtimeMode: "full-access", }), ).rejects.toThrow( "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", ); expect(versionCheck).toHaveBeenCalledTimes(1); expect(events).toEqual([ { method: "session/startFailed", kind: "error", message: "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", }, ]); } finally { versionCheck.mockRestore(); manager.stopAll(); } }); it("disposes an existing session before starting a replacement for the same thread", async () => { const manager = new CodexAppServerManager(); const existingContext = { session: { provider: "codex", status: "ready", threadId: asThreadId("thread-1"), runtimeMode: "full-access", createdAt: "2026-02-10T00:00:00.000Z", updatedAt: "2026-02-10T00:00:00.000Z", }, }; ( manager as unknown as { sessions: Map<ThreadId, typeof existingContext>; } ).sessions.set(asThreadId("thread-1"), existingContext); const disposeSession = vi .spyOn( manager as unknown as { disposeSession: ( context: typeof existingContext, options?: { readonly emitLifecycleEvent?: boolean }, ) => void; },