import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";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 * as Scope from "effect/Scope";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 branch names even when the ambient scope is already closed", () => withFakeCodexEnv( { output: JSON.stringify({ branch: "feat/background-generation", }), }, (textGeneration) => Effect.gen(function* () { // Background fibers (e.g. the worktree branch rename fork) can run // after their launching request's scope has closed; temp files must // not be tied to that ambient scope or they are reaped on creation. const closedScope = yield* Scope.make(); yield* Scope.close(closedScope, Exit.void); const generated = yield* textGeneration .generateBranchName({ cwd: process.cwd(), message: "Please update session handling.", modelSelection: DEFAULT_TEST_MODEL_SELECTION, }) .pipe(Effect.provideService(Scope.Scope, closedScope)); expect(generated.branch).toBe("feat/background-generation"); }), ), ); 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({Lines 1–500 of 646. Load more
import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; 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 * as Scope from "effect/Scope"; 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 branch names even when the ambient scope is already closed", () => withFakeCodexEnv( { output: JSON.stringify({ branch: "feat/background-generation", }), }, (textGeneration) => Effect.gen(function* () { // Background fibers (e.g. the worktree branch rename fork) can run // after their launching request's scope has closed; temp files must // not be tied to that ambient scope or they are reaped on creation. const closedScope = yield* Scope.make(); yield* Scope.close(closedScope, Exit.void); const generated = yield* textGeneration .generateBranchName({ cwd: process.cwd(), message: "Please update session handling.", modelSelection: DEFAULT_TEST_MODEL_SELECTION, }) .pipe(Effect.provideService(Scope.Scope, closedScope)); expect(generated.branch).toBe("feat/background-generation"); }), ), ); 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({