836 lines · 24.0 KB · TypeScript
RawDownload
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.makeUnsafe(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,    },  };   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",    },  };   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.makeUnsafe("req-user-input-1"),        {          requestId: ApprovalRequestId.makeUnsafe("req-user-input-1"),          jsonRpcId: 42,          threadId: asThreadId("thread_1"),        },      ],    ]),  };   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 };} 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("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("keeps spark enabled for api key accounts", () => {    expect(      readCodexAccountSnapshot({        type: "apiKey",      }),    ).toEqual({      type: "apiKey",      planType: null,      sparkEnabled: true,    });  });}); 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");  });}); 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",          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",          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();    }  });}); describe("sendTurn", () => {  it("sends text and image user input items to turn/start", async () => {    const { manager, context, requireSession, sendRequest, updateSession } =      createSendTurnHarness();     const result = await manager.sendTurn({      threadId: asThreadId("thread_1"),      input: "Inspect this image",      attachments: [        {          type: "image",          url: "data:image/png;base64,AAAA",        },      ],      model: "gpt-5.3",      serviceTier: "fast",      effort: "high",    });     expect(result).toEqual({      threadId: "thread_1",      turnId: "turn_1",      resumeCursor: { threadId: "thread_1" },    });    expect(requireSession).toHaveBeenCalledWith("thread_1");    expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", {      threadId: "thread_1",      input: [        {          type: "text",          text: "Inspect this image",          text_elements: [],        },        {          type: "image",          url: "data:image/png;base64,AAAA",        },      ],      model: "gpt-5.3-codex",      serviceTier: "fast",      effort: "high",    });    expect(updateSession).toHaveBeenCalledWith(context, {      status: "running",      activeTurnId: "turn_1",      resumeCursor: { threadId: "thread_1" },    });  });   it("supports image-only turns", async () => {    const { manager, context, sendRequest } = createSendTurnHarness();     await manager.sendTurn({      threadId: asThreadId("thread_1"),      attachments: [        {          type: "image",          url: "data:image/png;base64,BBBB",        },      ],    });     expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", {      threadId: "thread_1",      input: [        {          type: "image",          url: "data:image/png;base64,BBBB",        },      ],      model: "gpt-5.3-codex",    });  });   it("passes Codex plan mode as a collaboration preset on turn/start", async () => {    const { manager, context, sendRequest } = createSendTurnHarness();     await manager.sendTurn({      threadId: asThreadId("thread_1"),      input: "Plan the work",      interactionMode: "plan",    });     expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", {      threadId: "thread_1",      input: [        {          type: "text",          text: "Plan the work",          text_elements: [],        },      ],      model: "gpt-5.3-codex",      collaborationMode: {        mode: "plan",        settings: {          model: "gpt-5.3-codex",          reasoning_effort: "medium",          developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,        },      },    });  });   it("passes Codex default mode as a collaboration preset on turn/start", async () => {    const { manager, context, sendRequest } = createSendTurnHarness();     await manager.sendTurn({      threadId: asThreadId("thread_1"),      input: "PLEASE IMPLEMENT THIS PLAN:\n- step 1",      interactionMode: "default",    });     expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", {      threadId: "thread_1",      input: [        {          type: "text",          text: "PLEASE IMPLEMENT THIS PLAN:\n- step 1",          text_elements: [],        },      ],      model: "gpt-5.3-codex",      collaborationMode: {        mode: "default",        settings: {          model: "gpt-5.3-codex",

Lines 1–500 of 836.