471 lines · 13.9 KB · TypeScript
RawDownload
import * as NodeAssert from "node:assert/strict"; import { it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import { describe } from "vite-plus/test";import { DEFAULT_MODEL, ThreadId } from "@t3tools/contracts";import * as CodexErrors from "effect-codex-app-server/errors";import * as CodexRpc from "effect-codex-app-server/rpc"; import {  buildCodexDeveloperInstructions,  CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,  CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,} from "../CodexDeveloperInstructions.ts";import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";import {  buildTurnStartParams,  hasConfiguredMcpServer,  isRecoverableThreadResumeError,  openCodexThread,} from "./CodexSessionRuntime.ts";const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError); describe("CodexSessionRuntimeIdentifierGenerationError", () => {  it("retains identifier purpose and the random source failure", () => {    const cause = new Error("random source unavailable");    const error = new CodexErrors.CodexAppServerIdentifierGenerationError({      purpose: "provider-event",      cause,    });     NodeAssert.equal(error.purpose, "provider-event");    NodeAssert.strictEqual(error.cause, cause);    NodeAssert.equal(      error.message,      "Failed to generate Codex App Server identifier for provider-event.",    );  });}); function makeThreadOpenResponse(  threadId: string,): CodexRpc.ClientRequestResponsesByMethod["thread/start"] {  return {    cwd: "/tmp/project",    model: "gpt-5.3-codex",    modelProvider: "openai",    approvalPolicy: "never",    approvalsReviewer: "user",    sandbox: { type: "danger-full-access" },    thread: {      id: threadId,      createdAt: "2026-04-18T00:00:00.000Z",      source: { session: "cli" },      turns: [],      status: {        state: "idle",        activeFlags: [],      },    },  } as unknown as CodexRpc.ClientRequestResponsesByMethod["thread/start"];} describe("buildTurnStartParams", () => {  it("keeps invalid turn values only in the schema cause", () => {    const secret = "codex-turn-input-secret-sentinel";    const error = Effect.runSync(      buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "full-access",        attachments: [          {            type: "image",            url: { secret } as unknown as string,          },        ],      }).pipe(Effect.flip),    );    const { cause, ...directDiagnostics } = error;     NodeAssert.equal(error.operation, "decode-request-payload");    NodeAssert.equal(error.method, "turn/start");    NodeAssert.ok((error.issueCount ?? 0) > 0);    NodeAssert.ok(error.issueKinds?.includes("Pointer"));    NodeAssert.ok((error.maximumPathDepth ?? 0) > 0);    NodeAssert.ok(Schema.isSchemaError(cause));    NodeAssert.doesNotMatch(error.message, new RegExp(secret));    NodeAssert.doesNotMatch(JSON.stringify(directDiagnostics), new RegExp(secret));  });   it("includes plan collaboration mode when requested", () => {    const params = Effect.runSync(      buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "full-access",        prompt: "Make a plan",        model: "gpt-5.3-codex",        effort: "medium",        interactionMode: "plan",      }),    );     NodeAssert.deepStrictEqual(params, {      threadId: "provider-thread-1",      approvalPolicy: "never",      approvalsReviewer: "user",      sandboxPolicy: {        type: "dangerFullAccess",      },      input: [        {          type: "text",          text: "Make a plan",        },      ],      model: "gpt-5.3-codex",      effort: "medium",      collaborationMode: {        mode: "plan",        settings: {          model: "gpt-5.3-codex",          reasoning_effort: "medium",          developer_instructions: buildCodexDeveloperInstructions("plan", {            model: "gpt-5.3-codex",            reasoningEffort: "medium",          }),        },      },    });  });   it("includes default collaboration mode and image attachments", () => {    const params = Effect.runSync(      buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "auto-accept-edits",        prompt: "Implement it",        model: "gpt-5.3-codex",        interactionMode: "default",        attachments: [          {            type: "image",            url: "data:image/png;base64,abc",          },        ],      }),    );     NodeAssert.deepStrictEqual(params, {      threadId: "provider-thread-1",      approvalPolicy: "on-request",      approvalsReviewer: "user",      sandboxPolicy: {        type: "workspaceWrite",      },      input: [        {          type: "text",          text: "Implement it",        },        {          type: "image",          url: "data:image/png;base64,abc",        },      ],      model: "gpt-5.3-codex",      collaborationMode: {        mode: "default",        settings: {          model: "gpt-5.3-codex",          reasoning_effort: "medium",          developer_instructions: buildCodexDeveloperInstructions("default", {            model: "gpt-5.3-codex",            reasoningEffort: "medium",          }),        },      },    });  });   it("reports the same fallback model and effort in settings and instructions", () => {    const params = Effect.runSync(      buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "full-access",        prompt: "Go",        interactionMode: "default",      }),    );     const settings = params.collaborationMode?.settings;    NodeAssert.equal(settings?.model, DEFAULT_MODEL);    NodeAssert.equal(settings?.reasoning_effort, "medium");    NodeAssert.ok(settings?.developer_instructions?.includes(`as ${DEFAULT_MODEL} with medium`));  });   it.effect("routes approvals to the auto reviewer in auto mode", () =>    Effect.gen(function* () {      const params = yield* buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "auto",        prompt: "Ship it",      });       NodeAssert.deepStrictEqual(params, {        threadId: "provider-thread-1",        approvalPolicy: "on-request",        approvalsReviewer: "auto_review",        sandboxPolicy: {          type: "workspaceWrite",        },        input: [          {            type: "text",            text: "Ship it",          },        ],      });    }),  );   it("omits collaboration mode when interaction mode is absent", () => {    const params = Effect.runSync(      buildTurnStartParams({        threadId: "provider-thread-1",        runtimeMode: "approval-required",        prompt: "Review",      }),    );     NodeAssert.deepStrictEqual(params, {      threadId: "provider-thread-1",      approvalPolicy: "untrusted",      approvalsReviewer: "user",      sandboxPolicy: {        type: "readOnly",      },      input: [        {          type: "text",          text: "Review",        },      ],    });  });}); describe("buildCodexDeveloperInstructions", () => {  it("appends runtime info after the mode instructions", () => {    const instructions = buildCodexDeveloperInstructions("default", {      model: "gpt-5.3-codex",      reasoningEffort: "high",    });     NodeAssert.ok(instructions.startsWith(CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS));    NodeAssert.match(instructions, /T3 Code/);    NodeAssert.match(instructions, /Codex harness/);    NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/);  });   it("includes runtime info alongside plan mode instructions", () => {    const instructions = buildCodexDeveloperInstructions("plan", {      model: "gpt-5.3-codex",      reasoningEffort: "medium",    });     NodeAssert.ok(instructions.startsWith(CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS));    NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/);  });   it("varies with the model and effort of each turn", () => {    const first = buildCodexDeveloperInstructions("default", {      model: "gpt-5.3-codex",      reasoningEffort: "medium",    });    const second = buildCodexDeveloperInstructions("default", {      model: "gpt-5.4",      reasoningEffort: "high",    });     NodeAssert.notEqual(first, second);  });   it("flattens multiline metadata into single-line runtime info", () => {    const instructions = buildCodexDeveloperInstructions("default", {      model: "gpt\n5.3\ncodex",      reasoningEffort: " high\neffort ",    });     NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/);    NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/);  });}); describe("T3 browser developer instructions", () => {  it("prefers the product-native preview tools in both collaboration modes", () => {    for (const instructions of [      CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,      CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,    ]) {      NodeAssert.match(instructions, /t3-code/);      NodeAssert.match(instructions, /preview_status/);      NodeAssert.match(instructions, /preview_open/);      NodeAssert.match(instructions, /Do not switch to global browser skills/);    }  });}); describe("hasConfiguredMcpServer", () => {  it("detects inline Codex MCP configuration arguments", () => {    NodeAssert.equal(hasConfiguredMcpServer(undefined), false);    NodeAssert.equal(hasConfiguredMcpServer(["--model", "gpt-5.4"]), false);    NodeAssert.equal(      hasConfiguredMcpServer(["-c", 'mcp_servers.t3-code.url="http://127.0.0.1/mcp"']),      true,    );  });}); describe("codexSessionAppServerArgs", () => {  it("keeps the app-server subcommand when explicit args are provided", () => {    NodeAssert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [      "app-server",      "-c",      "model=gpt-5",    ]);  });   it("keeps launch args when explicit app-server args are provided", () => {    NodeAssert.deepStrictEqual(      codexSessionAppServerArgs(        ["-c", "mcp_servers.t3-code.url=http://127.0.0.1/mcp"],        "--strict-config --enable foo",      ),      [        "app-server",        "--strict-config",        "--enable",        "foo",        "-c",        "mcp_servers.t3-code.url=http://127.0.0.1/mcp",      ],    );  });}); describe("isRecoverableThreadResumeError", () => {  it("matches missing thread errors", () => {    NodeAssert.equal(      isRecoverableThreadResumeError(        new CodexErrors.CodexAppServerRequestError({          code: -32603,          errorMessage: "Thread does not exist",        }),      ),      true,    );  });   it("ignores non-recoverable resume errors", () => {    NodeAssert.equal(      isRecoverableThreadResumeError(        new CodexErrors.CodexAppServerRequestError({          code: -32603,          errorMessage: "Permission denied",        }),      ),      false,    );  });   it("ignores unrelated missing-resource errors that do not mention threads", () => {    NodeAssert.equal(      isRecoverableThreadResumeError(        new CodexErrors.CodexAppServerRequestError({          code: -32603,          errorMessage: "Config file not found",        }),      ),      false,    );    NodeAssert.equal(      isRecoverableThreadResumeError(        new CodexErrors.CodexAppServerRequestError({          code: -32603,          errorMessage: "Model does not exist",        }),      ),      false,    );  });}); describe("openCodexThread", () => {  it.effect("falls back to thread/start when resume fails recoverably", () =>    Effect.gen(function* () {      const calls: Array<{ method: "thread/start" | "thread/resume"; payload: unknown }> = [];      const started = makeThreadOpenResponse("fresh-thread");      const client = {        request: <M extends "thread/start" | "thread/resume">(          method: M,          payload: CodexRpc.ClientRequestParamsByMethod[M],        ) => {          calls.push({ method, payload });          if (method === "thread/resume") {            return Effect.fail(              new CodexErrors.CodexAppServerRequestError({                code: -32603,                errorMessage: "thread not found",              }),            );          }          return Effect.succeed(started as CodexRpc.ClientRequestResponsesByMethod[M]);        },      };       const opened = yield* openCodexThread({        client,        threadId: ThreadId.make("thread-1"),        runtimeMode: "full-access",        cwd: "/tmp/project",        requestedModel: "gpt-5.3-codex",        serviceTier: undefined,        resumeThreadId: "stale-thread",      });       NodeAssert.equal(opened.thread.id, "fresh-thread");      NodeAssert.deepStrictEqual(        calls.map((call) => call.method),        ["thread/resume", "thread/start"],      );    }),  );   it.effect("propagates non-recoverable resume failures", () =>    Effect.gen(function* () {      const client = {        request: <M extends "thread/start" | "thread/resume">(          method: M,          _payload: CodexRpc.ClientRequestParamsByMethod[M],        ) => {          if (method === "thread/resume") {            return Effect.fail(              new CodexErrors.CodexAppServerRequestError({                code: -32603,                errorMessage: "timed out waiting for server",              }),            );          }          return Effect.succeed(            makeThreadOpenResponse("fresh-thread") as CodexRpc.ClientRequestResponsesByMethod[M],          );        },      };       const error = yield* openCodexThread({        client,        threadId: ThreadId.make("thread-1"),        runtimeMode: "full-access",        cwd: "/tmp/project",        requestedModel: "gpt-5.3-codex",        serviceTier: undefined,        resumeThreadId: "stale-thread",      }).pipe(Effect.flip);       NodeAssert.ok(isCodexAppServerRequestError(error));      NodeAssert.equal(error.errorMessage, "timed out waiting for server");    }),  );});