Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 68 additions & 33 deletions apps/server/src/textGeneration/ClaudeTextGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isHostWindows } from "@t3tools/shared/hostProcess";
import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
Expand All @@ -23,47 +24,80 @@ function makeFakeClaudeBinary(dir: string) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const isWindows = yield* isHostWindows;
const binDir = path.join(dir, "bin");
const claudePath = path.join(binDir, "claude");
const stubPath = path.join(binDir, "claude-stub.mjs");
yield* fs.makeDirectory(binDir, { recursive: true });

// The stub behaviour lives in Node rather than a `#!/bin/sh` script so the
// same implementation is usable on Windows, where a shebang file is not
// executable and would fall through to the real Claude CLI on PATH.
yield* fs.writeFileString(
claudePath,
stubPath,
[
"#!/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",
'const args = process.argv.slice(2).join(" ");',
"",
"function fail(message, code) {",
' process.stderr.write(message + "\\n");',
" process.exit(code);",
"}",
"",
'let stdinContent = "";',
"if (!process.stdin.isTTY) {",
" const chunks = [];",
" for await (const chunk of process.stdin) {",
" chunks.push(chunk);",
" }",
"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}"',
' stdinContent = Buffer.concat(chunks).toString("utf8");',
"}",
"",
"const argsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;",
"if (argsMustContain && !args.includes(argsMustContain)) {",
' fail("args missing expected content", 2);',
"}",
"",
"const argsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;",
"if (argsMustNotContain && args.includes(argsMustNotContain)) {",
' fail("args contained forbidden content", 3);',
"}",
"",
"const stdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;",
"if (stdinMustContain && !stdinContent.includes(stdinMustContain)) {",
' fail("stdin missing expected content", 4);',
"}",
"",
"const configDirMustBe = process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;",
"if (configDirMustBe && process.env.CLAUDE_CONFIG_DIR !== configDirMustBe) {",
' fail("CLAUDE_CONFIG_DIR was " + (process.env.CLAUDE_CONFIG_DIR ?? ""), 5);',
"}",
"",
"const stderrText = process.env.T3_FAKE_CLAUDE_STDERR;",
"if (stderrText) {",
' process.stderr.write(stderrText + "\\n");',
"}",
"",
'process.stdout.write(process.env.T3_FAKE_CLAUDE_OUTPUT ?? "");',
"process.exit(Number(process.env.T3_FAKE_CLAUDE_EXIT_CODE ?? 0));",
"",
].join("\n"),
);
yield* fs.chmod(claudePath, 0o755);

if (isWindows) {
// Windows resolves executables through PATHEXT, so the entry point has to
// carry a real extension. `resolveSpawnCommand` spawns `.cmd` via a shell.
yield* fs.writeFileString(
path.join(binDir, "claude.cmd"),
["@echo off", 'node "%~dp0claude-stub.mjs" %*', "exit /b %ERRORLEVEL%", ""].join("\r\n"),
);
} else {
const claudePath = path.join(binDir, "claude");
yield* fs.writeFileString(
claudePath,
["#!/bin/sh", 'exec node "$(dirname "$0")/claude-stub.mjs" "$@"', ""].join("\n"),
);
yield* fs.chmod(claudePath, 0o755);
}

return binDir;
});
}
Expand All @@ -85,6 +119,7 @@ function withFakeClaudeEnv<A, E, R>(
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-claude-text-" });
const binDir = yield* makeFakeClaudeBinary(tempDir);
const pathDelimiter = (yield* isHostWindows) ? ";" : ":";
const previousPath = process.env.PATH;
const previousOutput = process.env.T3_FAKE_CLAUDE_OUTPUT;
const previousExitCode = process.env.T3_FAKE_CLAUDE_EXIT_CODE;
Expand All @@ -96,7 +131,7 @@ function withFakeClaudeEnv<A, E, R>(

yield* Effect.acquireRelease(
Effect.sync(() => {
process.env.PATH = `${binDir}:${previousPath ?? ""}`;
process.env.PATH = `${binDir}${pathDelimiter}${previousPath ?? ""}`;
process.env.T3_FAKE_CLAUDE_OUTPUT = input.output;

if (input.exitCode !== undefined) {
Expand Down
Loading