616 lines · 21.6 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as Result from "effect/Result";import * as Schema from "effect/Schema";import { createModelSelection } from "@t3tools/shared/model";import { expect } from "vite-plus/test"; import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; import * as ServerConfig from "../config.ts";import * as TextGeneration from "./TextGeneration.ts";import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";import { writeFakeCli } from "../testUtils/fakeCli.ts";const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(  ProviderInstanceId.make("codex"),  "gpt-5.4-mini",); const CodexTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {  prefix: "t3code-codex-text-generation-test-",}).pipe(Layer.provideMerge(NodeServices.layer)); interface FakeCodexInput {  output: string;  exitCode?: number;  stderr?: string;  requireImage?: boolean;  requireServiceTier?: string;  requireReasoningEffort?: string;  forbidReasoningEffort?: boolean;  requireArg?: string;  forbidArg?: string;  stdinMustContain?: string;  stdinMustNotContain?: string;} // The stub walks argv the way the shell script it replaced did: `--image`,// `--config key=value`, and `--output-last-message <path>` are consumed, the// prompt arrives on stdin, and each check exits with its own code so a// failing test names the assertion that tripped.function makeFakeCodexBinary(dir: string, input: FakeCodexInput) {  const check = JSON.stringify({    requireImage: input.requireImage ?? false,    requireServiceTier: input.requireServiceTier ?? null,    requireReasoningEffort: input.requireReasoningEffort ?? null,    forbidReasoningEffort: input.forbidReasoningEffort ?? false,    requireArg: input.requireArg ?? null,    forbidArg: input.forbidArg ?? null,    stdinMustContain: input.stdinMustContain ?? null,    stdinMustNotContain: input.stdinMustNotContain ?? null,    stderr: input.stderr ?? null,    output: input.output,    exitCode: input.exitCode ?? 0,  });  return Effect.gen(function* () {    const path = yield* Path.Path;    return writeFakeCli({      directory: path.join(dir, "bin"),      name: "codex",      source: [        'import * as NodeFS from "node:fs";',        `const check = ${check};`,        "const args = process.argv.slice(2);",        'const originalArgs = ` ${args.join(" ")} `;',        "let outputPath = null;",        "let seenImage = false;",        'let seenServiceTier = "";',        'let seenReasoningEffort = "";',        "for (let index = 0; index < args.length; index += 1) {",        '  if (args[index] === "--image") {',        "    index += 1;",        "    if (args[index]) seenImage = true;",        '  } else if (args[index] === "--config") {',        "    index += 1;",        '    const value = args[index] ?? "";',        '    if (value.startsWith("service_tier=")) seenServiceTier = value;',        '    if (value.startsWith("model_reasoning_effort=")) seenReasoningEffort = value;',        '  } else if (args[index] === "--output-last-message") {',        "    index += 1;",        "    outputPath = args[index] ?? null;",        "  }",        "}",        "const chunks = [];",        "for await (const chunk of process.stdin) chunks.push(chunk);",        'const stdinContent = Buffer.concat(chunks).toString("utf8");',        "function fail(message, code) {",        '  process.stderr.write(message + "\\n");',        "  process.exit(code);",        "}",        "if (check.requireArg !== null && !originalArgs.includes(` ${check.requireArg} `)) {",        '  fail("missing arg: " + check.requireArg, 8);',        "}",        "if (check.forbidArg !== null && originalArgs.includes(` ${check.forbidArg} `)) {",        '  fail("forbidden arg: " + check.forbidArg, 9);',        "}",        'if (check.requireImage && !seenImage) fail("missing --image input", 2);',        "if (",        "  check.requireServiceTier !== null &&",        '  seenServiceTier !== `service_tier="${check.requireServiceTier}"`',        ") {",        '  fail("unexpected service tier config: " + seenServiceTier, 5);',        "}",        "if (",        "  check.requireReasoningEffort !== null &&",        '  seenReasoningEffort !== `model_reasoning_effort="${check.requireReasoningEffort}"`',        ") {",        '  fail("unexpected reasoning effort config: " + seenReasoningEffort, 6);',        "}",        "if (check.forbidReasoningEffort && seenReasoningEffort.length > 0) {",        '  fail("reasoning effort config should be omitted: " + seenReasoningEffort, 7);',        "}",        "if (check.stdinMustContain !== null && !stdinContent.includes(check.stdinMustContain)) {",        '  fail("stdin missing expected content", 3);',        "}",        "if (check.stdinMustNotContain !== null && stdinContent.includes(check.stdinMustNotContain)) {",        '  fail("stdin contained forbidden content", 4);',        "}",        'if (check.stderr !== null) process.stderr.write(check.stderr + "\\n");',        'if (outputPath !== null) NodeFS.writeFileSync(outputPath, check.output + "\\n");',        "process.exitCode = check.exitCode;",        "",      ].join("\n"),    });  });} function withFakeCodexEnv<A, E, R>(  input: FakeCodexInput & {    launchArgs?: string;    environment?: NodeJS.ProcessEnv;  },  effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,) {  return Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });    const codexPath = yield* makeFakeCodexBinary(tempDir, input);    const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs });    const textGeneration = yield* makeCodexTextGeneration(config, input.environment);    return yield* effectFn(textGeneration);  }).pipe(Effect.scoped);} it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {  it.effect("generates and sanitizes commit messages without branch by default", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          subject:            "  Add important change to the system with too much detail and a trailing period.\nsecondary line",          body: "\n- added migration\n- updated tests\n",        }),        stdinMustNotContain: "branch must be a short semantic git branch fragment",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateCommitMessage({            cwd: process.cwd(),            branch: "feature/codex-effect",            stagedSummary: "M README.md",            stagedPatch: "diff --git a/README.md b/README.md",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.subject.length).toBeLessThanOrEqual(72);          expect(generated.subject.endsWith(".")).toBe(false);          expect(generated.body).toBe("- added migration\n- updated tests");          expect(generated.branch).toBeUndefined();        }),    ),  );   it.effect(    "forwards codex service tier and non-default reasoning effort into codex exec config",    () =>      withFakeCodexEnv(        {          output: JSON.stringify({            subject: "Add important change",            body: "",          }),          requireServiceTier: "priority",          requireReasoningEffort: "xhigh",          stdinMustNotContain: "branch must be a short semantic git branch fragment",        },        (textGeneration) =>          textGeneration.generateCommitMessage({            cwd: process.cwd(),            branch: "feature/codex-effect",            stagedSummary: "M README.md",            stagedPatch: "diff --git a/README.md b/README.md",            modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [              { id: "reasoningEffort", value: "xhigh" },              { id: "serviceTier", value: "priority" },            ]),          }),      ),  );   it.effect("passes exec-safe launch args into codex exec", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          subject: "Add important change",          body: "",        }),        launchArgs: "--strict-config --listen off",        requireArg: "--strict-config",        forbidArg: "--listen",      },      (textGeneration) =>        textGeneration.generateCommitMessage({          cwd: process.cwd(),          branch: "feature/codex-effect",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        }),    ),  );   it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for codex exec over settings", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          subject: "Add important change",          body: "",        }),        launchArgs: "--enable settings-feature",        environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --listen off " },        requireArg: "--strict-config",        forbidArg: "settings-feature",      },      (textGeneration) =>        textGeneration.generateCommitMessage({          cwd: process.cwd(),          branch: "feature/codex-effect",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        }),    ),  );   it.effect("defaults git text generation codex effort to low", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          subject: "Add important change",          body: "",        }),        requireReasoningEffort: "low",      },      (textGeneration) =>        textGeneration.generateCommitMessage({          cwd: process.cwd(),          branch: "feature/codex-effect",          stagedSummary: "M README.md",          stagedPatch: "diff --git a/README.md b/README.md",          modelSelection: DEFAULT_TEST_MODEL_SELECTION,        }),    ),  );   it.effect("generates commit message with branch when includeBranch is true", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          subject: "Add important change",          body: "",          branch: "fix/important-system-change",        }),        stdinMustContain: "branch must be a short semantic git branch fragment",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateCommitMessage({            cwd: process.cwd(),            branch: "feature/codex-effect",            stagedSummary: "M README.md",            stagedPatch: "diff --git a/README.md b/README.md",            includeBranch: true,            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.subject).toBe("Add important change");          expect(generated.branch).toBe("feature/fix/important-system-change");        }),    ),  );   it.effect("generates PR content and trims markdown body", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          title: "  Improve orchestration flow\nwith ignored suffix",          body: "\n## Summary\n- improve flow\n\n## Testing\n- bun test\n\n",        }),      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generatePrContent({            cwd: process.cwd(),            baseBranch: "main",            headBranch: "feature/codex-effect",            commitSummary: "feat: improve orchestration flow",            diffSummary: "2 files changed",            diffPatch: "diff --git a/a.ts b/a.ts",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.title).toBe("Improve orchestration flow");          expect(generated.body.startsWith("## Summary")).toBe(true);          expect(generated.body.endsWith("\n\n")).toBe(false);        }),    ),  );   it.effect("generates branch names and normalizes branch fragments", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "  Feat/Session  ",        }),        stdinMustNotContain: "Image attachments supplied to the model",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateBranchName({            cwd: process.cwd(),            message: "Please update session handling.",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.branch).toBe("feat/session");        }),    ),  );   it.effect("generates thread titles and trims them for sidebar use", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          title:            '  "Investigate websocket reconnect regressions after worktree restore"  \nignored line',        }),      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateThreadTitle({            cwd: process.cwd(),            message: "Please investigate websocket reconnect regressions after a worktree restore.",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.title).toBe("Investigate websocket reconnect regressions aft...");        }),    ),  );   it.effect("falls back when thread title normalization becomes whitespace-only", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          title: '  """   """  ',        }),      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateThreadTitle({            cwd: process.cwd(),            message: "Name this thread.",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.title).toBe("New thread");        }),    ),  );   it.effect("trims whitespace exposed after quote removal in thread titles", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          title: `  "' hello world '"  `,        }),      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateThreadTitle({            cwd: process.cwd(),            message: "Name this thread.",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.title).toBe("hello world");        }),    ),  );   it.effect("omits attachment metadata section when no attachments are provided", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "fix/session-timeout",        }),        stdinMustNotContain: "Attachment metadata:",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateBranchName({            cwd: process.cwd(),            message: "Fix timeout behavior.",            modelSelection: DEFAULT_TEST_MODEL_SELECTION,          });           expect(generated.branch).toBe("fix/session-timeout");        }),    ),  );   it.effect("passes image attachments through as codex image inputs", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "fix/ui-regression",        }),        requireImage: true,        stdinMustContain: "Attachment metadata:",      },      (textGeneration) =>        Effect.gen(function* () {          const fs = yield* FileSystem.FileSystem;          const path = yield* Path.Path;          const { attachmentsDir } = yield* ServerConfig.ServerConfig;          const attachmentId = "thread-branch-image-attachment";          const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`);          yield* fs.makeDirectory(attachmentsDir, { recursive: true });          yield* fs.writeFile(attachmentPath, Buffer.from("hello"));           const generated = yield* textGeneration.generateBranchName({            modelSelection: DEFAULT_TEST_MODEL_SELECTION,            cwd: process.cwd(),            message: "Fix layout bug from screenshot.",            attachments: [              {                type: "image",                id: attachmentId,                name: "bug.png",                mimeType: "image/png",                sizeBytes: 5,              },            ],          });           expect(generated.branch).toBe("fix/ui-regression");        }),    ),  );   it.effect("resolves persisted attachment ids to files for codex image inputs", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "fix/ui-regression",        }),        requireImage: true,      },      (textGeneration) =>        Effect.gen(function* () {          const fs = yield* FileSystem.FileSystem;          const path = yield* Path.Path;          const { attachmentsDir } = yield* ServerConfig.ServerConfig;          const attachmentId = "thread-1-attachment";          const imagePath = path.join(attachmentsDir, `${attachmentId}.png`);          yield* fs.makeDirectory(attachmentsDir, { recursive: true });          yield* fs.writeFile(imagePath, Buffer.from("hello"));           const generated = yield* textGeneration            .generateBranchName({              modelSelection: DEFAULT_TEST_MODEL_SELECTION,              cwd: process.cwd(),              message: "Fix layout bug from screenshot.",              attachments: [                {                  type: "image",                  id: attachmentId,                  name: "bug.png",                  mimeType: "image/png",                  sizeBytes: 5,                },              ],            })            .pipe(

Lines 1–500 of 616.