import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { Effect, FileSystem, Layer, Path } from "effect"; import { expect } from "vitest"; import { CodexTextGenerationLive } from "./CodexTextGeneration.ts"; import { TextGenerationError } from "../Errors.ts"; import { TextGeneration } from "../Services/TextGeneration.ts"; function makeFakeCodexBinary(dir: string) { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const binDir = path.join(dir, "bin"); const codexPath = path.join(binDir, "codex"); yield* fs.makeDirectory(binDir, { recursive: true }); yield* fs.writeFileString( codexPath, [ "#!/bin/sh", 'output_path=""', "while [ $# -gt 0 ]; do", ' if [ "$1" = "--output-last-message" ]; then', " shift", ' output_path="$1"', " fi", " shift", "done", "cat >/dev/null", 'if [ -n "$T3_FAKE_CODEX_STDERR" ]; then', ' printf "%s\\n" "$T3_FAKE_CODEX_STDERR" >&2', "fi", 'if [ -n "$output_path" ]; then', ' node -e \'const fs=require("node:fs"); const value=process.argv[2] ?? ""; fs.writeFileSync(process.argv[1], Buffer.from(value, "base64"));\' "$output_path" "${T3_FAKE_CODEX_OUTPUT_B64:-e30=}"', "fi", 'exit "${T3_FAKE_CODEX_EXIT_CODE:-0}"', "", ].join("\n"), ); yield* fs.chmod(codexPath, 0o755); return binDir; }); } function withFakeCodexEnv( input: { output: string; exitCode?: number; stderr?: string; }, effect: Effect.Effect, ) { return Effect.acquireUseRelease( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); const binDir = yield* makeFakeCodexBinary(tempDir); const previousPath = process.env.PATH; const previousOutput = process.env.T3_FAKE_CODEX_OUTPUT_B64; const previousExitCode = process.env.T3_FAKE_CODEX_EXIT_CODE; const previousStderr = process.env.T3_FAKE_CODEX_STDERR; yield* Effect.sync(() => { process.env.PATH = `${binDir}:${previousPath ?? ""}`; process.env.T3_FAKE_CODEX_OUTPUT_B64 = Buffer.from(input.output, "utf8").toString("base64"); if (input.exitCode !== undefined) { process.env.T3_FAKE_CODEX_EXIT_CODE = String(input.exitCode); } else { delete process.env.T3_FAKE_CODEX_EXIT_CODE; } if (input.stderr !== undefined) { process.env.T3_FAKE_CODEX_STDERR = input.stderr; } else { delete process.env.T3_FAKE_CODEX_STDERR; } }); return { previousPath, previousOutput, previousExitCode, previousStderr, }; }), () => effect, (previous) => Effect.sync(() => { process.env.PATH = previous.previousPath; if (previous.previousOutput === undefined) { delete process.env.T3_FAKE_CODEX_OUTPUT_B64; } else { process.env.T3_FAKE_CODEX_OUTPUT_B64 = previous.previousOutput; } if (previous.previousExitCode === undefined) { delete process.env.T3_FAKE_CODEX_EXIT_CODE; } else { process.env.T3_FAKE_CODEX_EXIT_CODE = previous.previousExitCode; } if (previous.previousStderr === undefined) { delete process.env.T3_FAKE_CODEX_STDERR; } else { process.env.T3_FAKE_CODEX_STDERR = previous.previousStderr; } }), ); } const CodexTextGenerationTestLayer = Layer.provideMerge( CodexTextGenerationLive, NodeServices.layer, ); it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { it.effect("generates and sanitizes commit messages", () => 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", }), }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; 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", }); expect(generated.subject.length).toBeLessThanOrEqual(72); expect(generated.subject.endsWith(".")).toBe(false); expect(generated.body).toBe("- added migration\n- updated tests"); }), ), ); 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", }), }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; 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", }); expect(generated.title).toBe("Improve orchestration flow"); expect(generated.body.startsWith("## Summary")).toBe(true); expect(generated.body.endsWith("\n\n")).toBe(false); }), ), ); it.effect("returns typed TextGenerationError when codex exits non-zero", () => withFakeCodexEnv( { output: JSON.stringify({ subject: "ignored", body: "" }), exitCode: 1, stderr: "codex execution failed", }, Effect.gen(function* () { const textGeneration = yield* TextGeneration; const result = yield* textGeneration .generateCommitMessage({ cwd: process.cwd(), branch: "feature/codex-error", stagedSummary: "M README.md", stagedPatch: "diff --git a/README.md b/README.md", }) .pipe( Effect.match({ onFailure: (error) => ({ _tag: "Left" as const, left: error }), onSuccess: (value) => ({ _tag: "Right" as const, right: value }), }), ); expect(result._tag).toBe("Left"); if (result._tag === "Left") { expect(result.left).toBeInstanceOf(TextGenerationError); expect(result.left.message).toContain("Codex CLI command failed: codex execution failed"); } }), ), ); });