345 lines · 12.2 KB · TypeScript
RawDownload
import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";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 Schema from "effect/Schema";import { createModelSelection } from "@t3tools/shared/model";import { expect } from "vite-plus/test"; import * as ServerConfig from "../config.ts";import * as TextGeneration from "./TextGeneration.ts";import { sanitizeThreadTitle } from "./TextGenerationUtils.ts";import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); const ClaudeTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {  prefix: "t3code-claude-text-generation-test-",}).pipe(Layer.provideMerge(NodeServices.layer)); function makeFakeClaudeBinary(dir: string) {  return Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    const path = yield* Path.Path;    const binDir = path.join(dir, "bin");    const claudePath = path.join(binDir, "claude");    yield* fs.makeDirectory(binDir, { recursive: true });     yield* fs.writeFileString(      claudePath,      [        "#!/bin/sh",        'args="$*"',        'stdin_content="$(cat)"',        'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" ]; then',        '  printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" >/dev/null || {',        '    printf "%s\\n" "args missing expected content" >&2',        "    exit 2",        "  }",        "fi",        'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" ]; then',        '  if printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" >/dev/null; then',        '    printf "%s\\n" "args contained forbidden content" >&2',        "    exit 3",        "  fi",        "fi",        'if [ -n "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" ]; then',        '  printf "%s" "$stdin_content" | grep -F -- "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" >/dev/null || {',        '    printf "%s\\n" "stdin missing expected content" >&2',        "    exit 4",        "  }",        "fi",        'if [ -n "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ] && [ "$CLAUDE_CONFIG_DIR" != "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ]; then',        '  printf "%s\\n" "CLAUDE_CONFIG_DIR was $CLAUDE_CONFIG_DIR" >&2',        "  exit 5",        "fi",        'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then',        '  printf "%s\\n" "$T3_FAKE_CLAUDE_STDERR" >&2',        "fi",        'printf "%s" "$T3_FAKE_CLAUDE_OUTPUT"',        'exit "${T3_FAKE_CLAUDE_EXIT_CODE:-0}"',        "",      ].join("\n"),    );    yield* fs.chmod(claudePath, 0o755);    return binDir;  });} function withFakeClaudeEnv<A, E, R>(  input: {    output: string;    exitCode?: number;    stderr?: string;    argsMustContain?: string;    argsMustNotContain?: string;    stdinMustContain?: string;    configDirMustBe?: string;    claudeConfig?: Partial<ClaudeSettings>;  },  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-claude-text-" });    const binDir = yield* makeFakeClaudeBinary(tempDir);    const previousPath = process.env.PATH;    const previousOutput = process.env.T3_FAKE_CLAUDE_OUTPUT;    const previousExitCode = process.env.T3_FAKE_CLAUDE_EXIT_CODE;    const previousStderr = process.env.T3_FAKE_CLAUDE_STDERR;    const previousArgsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;    const previousArgsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;    const previousStdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;    const previousConfigDirMustBe = process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;     yield* Effect.acquireRelease(      Effect.sync(() => {        process.env.PATH = `${binDir}:${previousPath ?? ""}`;        process.env.T3_FAKE_CLAUDE_OUTPUT = input.output;         if (input.exitCode !== undefined) {          process.env.T3_FAKE_CLAUDE_EXIT_CODE = String(input.exitCode);        } else {          delete process.env.T3_FAKE_CLAUDE_EXIT_CODE;        }         if (input.stderr !== undefined) {          process.env.T3_FAKE_CLAUDE_STDERR = input.stderr;        } else {          delete process.env.T3_FAKE_CLAUDE_STDERR;        }         if (input.argsMustContain !== undefined) {          process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN = input.argsMustContain;        } else {          delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;        }         if (input.argsMustNotContain !== undefined) {          process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN = input.argsMustNotContain;        } else {          delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;        }         if (input.stdinMustContain !== undefined) {          process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = input.stdinMustContain;        } else {          delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;        }         if (input.configDirMustBe !== undefined) {          process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = input.configDirMustBe;        } else {          delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;        }      }),      () =>        Effect.sync(() => {          process.env.PATH = previousPath;           if (previousOutput === undefined) {            delete process.env.T3_FAKE_CLAUDE_OUTPUT;          } else {            process.env.T3_FAKE_CLAUDE_OUTPUT = previousOutput;          }           if (previousExitCode === undefined) {            delete process.env.T3_FAKE_CLAUDE_EXIT_CODE;          } else {            process.env.T3_FAKE_CLAUDE_EXIT_CODE = previousExitCode;          }           if (previousStderr === undefined) {            delete process.env.T3_FAKE_CLAUDE_STDERR;          } else {            process.env.T3_FAKE_CLAUDE_STDERR = previousStderr;          }           if (previousArgsMustContain === undefined) {            delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;          } else {            process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN = previousArgsMustContain;          }           if (previousArgsMustNotContain === undefined) {            delete process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;          } else {            process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN = previousArgsMustNotContain;          }           if (previousStdinMustContain === undefined) {            delete process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;          } else {            process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN = previousStdinMustContain;          }           if (previousConfigDirMustBe === undefined) {            delete process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;          } else {            process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE = previousConfigDirMustBe;          }        }),    );     const config = decodeClaudeSettings(input.claudeConfig ?? {});    const textGeneration = yield* makeClaudeTextGeneration(config);    return yield* effectFn(textGeneration);  }).pipe(Effect.scoped);} it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {  it.effect("forwards Claude thinking settings for Haiku without passing effort", () =>    withFakeClaudeEnv(      {        output: JSON.stringify({          structured_output: {            subject: "Add important change",            body: "",          },        }),        argsMustContain: '--settings {"alwaysThinkingEnabled":false}',        argsMustNotContain: "--effort",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateCommitMessage({            cwd: process.cwd(),            branch: "feature/claude-effect",            stagedSummary: "M README.md",            stagedPatch: "diff --git a/README.md b/README.md",            modelSelection: {              ...createModelSelection(ProviderInstanceId.make("claudeAgent"), "claude-haiku-4-5", [                { id: "thinking", value: false },                { id: "effort", value: "high" },              ]),            },          });           expect(generated.subject).toBe("Add important change");        }),    ),  );   it.effect("forwards Claude fast mode and supported effort", () =>    withFakeClaudeEnv(      {        output: JSON.stringify({          structured_output: {            title: "Improve orchestration flow",            body: "Body",          },        }),        argsMustContain: '--effort max --settings {"fastMode":true}',      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generatePrContent({            cwd: process.cwd(),            baseBranch: "main",            headBranch: "feature/claude-effect",            commitSummary: "Improve orchestration",            diffSummary: "1 file changed",            diffPatch: "diff --git a/README.md b/README.md",            modelSelection: {              ...createModelSelection(ProviderInstanceId.make("claudeAgent"), "claude-opus-4-6", [                { id: "effort", value: "max" },                { id: "fastMode", value: true },              ]),            },          });           expect(generated.title).toBe("Improve orchestration flow");        }),    ),  );   it.effect("generates thread titles through the Claude provider", () =>    withFakeClaudeEnv(      {        output: JSON.stringify({          structured_output: {            title:              '  "Reconnect failures after restart because the session state does not recover"  ',          },        }),        stdinMustContain: "You write concise thread titles for coding conversations.",      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateThreadTitle({            cwd: process.cwd(),            message: "Please investigate reconnect failures after restarting the session.",            modelSelection: {              instanceId: ProviderInstanceId.make("claudeAgent"),              model: "claude-sonnet-4-6",            },          });           expect(generated.title).toBe(            sanitizeThreadTitle(              '"Reconnect failures after restart because the session state does not recover"',            ),          );        }),    ),  );   it.effect("runs Claude text generation with the configured CLAUDE_CONFIG_DIR", () =>    Effect.gen(function* () {      const path = yield* Path.Path;      const claudeConfigDir = path.join(process.cwd(), ".claude-work-test");      return yield* withFakeClaudeEnv(        {          // @effect-diagnostics-next-line preferSchemaOverJson:off          output: JSON.stringify({            structured_output: {              title: "Use Claude home",            },          }),          configDirMustBe: claudeConfigDir,          claudeConfig: { homePath: claudeConfigDir },        },        (textGeneration) =>          Effect.gen(function* () {            const generated = yield* textGeneration.generateThreadTitle({              cwd: process.cwd(),              message: "thread title",              modelSelection: {                instanceId: ProviderInstanceId.make("claudeAgent"),                model: "claude-sonnet-4-6",              },            });             expect(generated.title).toBe(sanitizeThreadTitle("Use Claude home"));          }),      );    }),  );   it.effect("falls back when Claude thread title normalization becomes whitespace-only", () =>    withFakeClaudeEnv(      {        output: JSON.stringify({          structured_output: {            title: '  """   """  ',          },        }),      },      (textGeneration) =>        Effect.gen(function* () {          const generated = yield* textGeneration.generateThreadTitle({            cwd: process.cwd(),            message: "Name this thread.",            modelSelection: {              instanceId: ProviderInstanceId.make("claudeAgent"),              model: "claude-sonnet-4-6",            },          });           expect(generated.title).toBe("New thread");        }),    ),  );});