540 lines · 18.6 KB · TypeScript
RawDownload
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 { ServerConfig } from "../../config.ts";import { CodexTextGenerationLive } from "./CodexTextGeneration.ts";import { TextGenerationError } from "../Errors.ts";import { TextGeneration } from "../Services/TextGeneration.ts"; const makeCodexTextGenerationTestLayer = (stateDir: string) =>  CodexTextGenerationLive.pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), stateDir)),    Layer.provideMerge(NodeServices.layer),  ); 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 launcherPath = path.join(binDir, process.platform === "win32" ? "codex.cmd" : "codex");    const scriptPath = path.join(binDir, "codex.cjs");    yield* fs.makeDirectory(binDir, { recursive: true });     yield* fs.writeFileString(      scriptPath,      [        'const fs = require("node:fs");',        "",        "async function readStdin() {",        "  const chunks = [];",        "  for await (const chunk of process.stdin) {",        "    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));",        "  }",        '  return Buffer.concat(chunks).toString("utf8");',        "}",        "",        "(async () => {",        '  let outputPath = "";',        "  let seenImage = false;",        "  for (let index = 2; index < process.argv.length; index += 1) {",        "    const arg = process.argv[index];",        '    if (arg === "--image") {',        "      const imagePath = process.argv[index + 1];",        "      if (imagePath) {",        "        seenImage = true;",        "        index += 1;",        "      }",        "      continue;",        "    }",        '    if (arg === "--output-last-message") {',        '      outputPath = process.argv[index + 1] ?? "";',        "      index += 1;",        "    }",        "  }",        "  const stdinContent = await readStdin();",        '  if (process.env.T3_FAKE_CODEX_REQUIRE_IMAGE === "1" && !seenImage) {',        '    process.stderr.write("missing --image input\\n");',        "    process.exit(2);",        "  }",        "  const mustContain = process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;",        '  if (mustContain && !stdinContent.includes(mustContain)) {',        '    process.stderr.write("stdin missing expected content\\n");',        "    process.exit(3);",        "  }",        "  const mustNotContain = process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;",        '  if (mustNotContain && stdinContent.includes(mustNotContain)) {',        '    process.stderr.write("stdin contained forbidden content\\n");',        "    process.exit(4);",        "  }",        "  if (process.env.T3_FAKE_CODEX_STDERR) {",        '    process.stderr.write(`${process.env.T3_FAKE_CODEX_STDERR}\\n`);',        "  }",        "  if (outputPath) {",        '    fs.writeFileSync(outputPath, Buffer.from(process.env.T3_FAKE_CODEX_OUTPUT_B64 ?? "e30=", "base64"));',        "  }",        '  process.exit(Number(process.env.T3_FAKE_CODEX_EXIT_CODE ?? "0"));',        "})().catch((error) => {",        '  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);',        "  process.exit(1);",        "});",        "",      ].join("\n"),    );     yield* fs.writeFileString(      launcherPath,      process.platform === "win32"        ? `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`        : `#!/bin/sh\nexec "${process.execPath}" "${scriptPath}" "$@"\n`,    );    yield* fs.chmod(launcherPath, 0o755);    return binDir;  });} function withFakeCodexEnv<A, E, R>(  input: {    output: string;    exitCode?: number;    stderr?: string;    requireImage?: boolean;    stdinMustContain?: string;    stdinMustNotContain?: string;  },  effect: Effect.Effect<A, E, R>,) {  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;      const previousRequireImage = process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;      const previousStdinMustContain = process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;      const previousStdinMustNotContain = process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;      const pathDelimiter = process.platform === "win32" ? ";" : ":";       yield* Effect.sync(() => {        process.env.PATH =          previousPath && previousPath.length > 0 ? `${binDir}${pathDelimiter}${previousPath}` : binDir;        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;        }         if (input.requireImage) {          process.env.T3_FAKE_CODEX_REQUIRE_IMAGE = "1";        } else {          delete process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;        }         if (input.stdinMustContain !== undefined) {          process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN = input.stdinMustContain;        } else {          delete process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;        }         if (input.stdinMustNotContain !== undefined) {          process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN = input.stdinMustNotContain;        } else {          delete process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;        }      });       return {        previousPath,        previousOutput,        previousExitCode,        previousStderr,        previousRequireImage,        previousStdinMustContain,        previousStdinMustNotContain,      };    }),    () => 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;        }         if (previous.previousRequireImage === undefined) {          delete process.env.T3_FAKE_CODEX_REQUIRE_IMAGE;        } else {          process.env.T3_FAKE_CODEX_REQUIRE_IMAGE = previous.previousRequireImage;        }         if (previous.previousStdinMustContain === undefined) {          delete process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN;        } else {          process.env.T3_FAKE_CODEX_STDIN_MUST_CONTAIN = previous.previousStdinMustContain;        }         if (previous.previousStdinMustNotContain === undefined) {          delete process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN;        } else {          process.env.T3_FAKE_CODEX_STDIN_MUST_NOT_CONTAIN = previous.previousStdinMustNotContain;        }      }),  );} const CodexTextGenerationTestLayer = makeCodexTextGenerationTestLayer(process.cwd()); it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (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",      },      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");        expect(generated.branch).toBeUndefined();      }),    ),  );   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",      },      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",          includeBranch: true,        });         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",        }),      },      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("generates branch names and normalizes branch fragments", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "  Feat/Session  ",        }),        stdinMustNotContain: "Image attachments supplied to the model",      },      Effect.gen(function* () {        const textGeneration = yield* TextGeneration;         const generated = yield* textGeneration.generateBranchName({          cwd: process.cwd(),          message: "Please update session handling.",        });         expect(generated.branch).toBe("feat/session");      }),    ),  );   it.effect("omits attachment metadata section when no attachments are provided", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "fix/session-timeout",        }),        stdinMustNotContain: "Attachment metadata:",      },      Effect.gen(function* () {        const textGeneration = yield* TextGeneration;         const generated = yield* textGeneration.generateBranchName({          cwd: process.cwd(),          message: "Fix timeout behavior.",        });         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:",      },      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const attachmentId = `thread-branch-image-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;        const attachmentPath = path.join(process.cwd(), "attachments", `${attachmentId}.png`);        yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true });        yield* fs.writeFile(attachmentPath, Buffer.from("hello"));         const textGeneration = yield* TextGeneration;        const generated = yield* textGeneration          .generateBranchName({            cwd: process.cwd(),            message: "Fix layout bug from screenshot.",            attachments: [              {                type: "image",                id: attachmentId,                name: "bug.png",                mimeType: "image/png",                sizeBytes: 5,              },            ],          })          .pipe(Effect.ensuring(fs.remove(attachmentPath).pipe(Effect.catch(() => Effect.void))));         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,      },      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const attachmentId =          `thread-1-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;        const imagePath = path.join(process.cwd(), "attachments", `${attachmentId}.png`);        yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true });        yield* fs.writeFile(imagePath, Buffer.from("hello"));         const textGeneration = yield* TextGeneration;        const generated = yield* textGeneration          .generateBranchName({            cwd: process.cwd(),            message: "Fix layout bug from screenshot.",            attachments: [              {                type: "image",                id: attachmentId,                name: "bug.png",                mimeType: "image/png",                sizeBytes: 5,              },            ],          })          .pipe(            Effect.tap(() =>              fs.stat(imagePath).pipe(                Effect.map((fileInfo) => {                  expect(fileInfo.type).toBe("File");                }),              ),            ),            Effect.ensuring(              fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)),            ),          );         expect(generated.branch).toBe("fix/ui-regression");      }),    ),  );   it.effect("ignores missing attachment ids for codex image inputs", () =>    withFakeCodexEnv(      {        output: JSON.stringify({          branch: "fix/ui-regression",        }),        requireImage: true,      },      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const missingAttachmentId = `thread-missing-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;        const missingPath = path.join(process.cwd(), "attachments", `${missingAttachmentId}.png`);        yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));         const textGeneration = yield* TextGeneration;        const result = yield* textGeneration          .generateBranchName({            cwd: process.cwd(),            message: "Fix layout bug from screenshot.",            attachments: [              {                type: "image",                id: missingAttachmentId,                name: "outside.png",                mimeType: "image/png",                sizeBytes: 5,              },            ],          })          .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("missing --image input");        }      }),    ),  );   it.effect(    "fails with typed TextGenerationError when codex returns wrong branch payload shape",    () =>      withFakeCodexEnv(        {          output: JSON.stringify({            title: "This is not a branch payload",          }),        },        Effect.gen(function* () {          const textGeneration = yield* TextGeneration;           const result = yield* textGeneration            .generateBranchName({              cwd: process.cwd(),              message: "Fix websocket reconnect flake",            })            .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") {

Lines 1–500 of 540.