authorgravatar for mihneanob@gmail.comMihnea Peteu <mihneanob@gmail.com> 2026-08-15 15:01:29+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:01:29+00:00
log5ffbf3ce4a4e8d14f8f37fae304167ad6c1e5218
tree5fd06d580cc77a78b6c306bcd464d6b2936e5591
parent34a12bc33f5fba02b0b398e7dff35ca041a87dc7
signature Signed by PGP key B5690EEEBB952194

Make ClaudeTextGeneration tests hermetic on Windows (#4508)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 70 insertions(+), 35 deletions(-)

apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+70-35
......@@ -1,12 +1,13 @@
1import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
21import * as NodeServices from "@effect/platform-node/NodeServices";
32import { it } from "@effect/vitest";
3import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
4import { isHostWindows } from "@t3tools/shared/hostProcess";
5import { createModelSelection } from "@t3tools/shared/model";
46import * as Effect from "effect/Effect";
57import * as FileSystem from "effect/FileSystem";
68import * as Layer from "effect/Layer";
79import * as Path from "effect/Path";
810import * as Schema from "effect/Schema";
9import { createModelSelection } from "@t3tools/shared/model";
1011import { expect } from "vite-plus/test";
1112
1213import * as ServerConfig from "../config.ts";
......@@ -23,47 +24,80 @@ function makeFakeClaudeBinary(dir: string) {
2324 return Effect.gen(function* () {
2425 const fs = yield* FileSystem.FileSystem;
2526 const path = yield* Path.Path;
27 const isWindows = yield* isHostWindows;
2628 const binDir = path.join(dir, "bin");
27 const claudePath = path.join(binDir, "claude");
29 const stubPath = path.join(binDir, "claude-stub.mjs");
2830 yield* fs.makeDirectory(binDir, { recursive: true });
2931
32 // The stub behaviour lives in Node rather than a `#!/bin/sh` script so the
33 // same implementation is usable on Windows, where a shebang file is not
34 // executable and would fall through to the real Claude CLI on PATH.
3035 yield* fs.writeFileString(
31 claudePath,
36 stubPath,
3237 [
33 "#!/bin/sh",
34 'args="$*"',
35 'stdin_content="$(cat)"',
36 'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" ]; then',
37 ' printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN" >/dev/null || {',
38 ' printf "%s\\n" "args missing expected content" >&2',
39 " exit 2",
40 " }",
41 "fi",
42 'if [ -n "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" ]; then',
43 ' if printf "%s" "$args" | grep -F -- "$T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN" >/dev/null; then',
44 ' printf "%s\\n" "args contained forbidden content" >&2',
45 " exit 3",
46 " fi",
47 "fi",
48 'if [ -n "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" ]; then',
49 ' printf "%s" "$stdin_content" | grep -F -- "$T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN" >/dev/null || {',
50 ' printf "%s\\n" "stdin missing expected content" >&2',
51 " exit 4",
38 'const args = process.argv.slice(2).join(" ");',
39 "",
40 "function fail(message, code) {",
41 ' process.stderr.write(message + "\\n");',
42 " process.exit(code);",
43 "}",
44 "",
45 'let stdinContent = "";',
46 "if (!process.stdin.isTTY) {",
47 " const chunks = [];",
48 " for await (const chunk of process.stdin) {",
49 " chunks.push(chunk);",
5250 " }",
53 "fi",
54 'if [ -n "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ] && [ "$CLAUDE_CONFIG_DIR" != "$T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE" ]; then',
55 ' printf "%s\\n" "CLAUDE_CONFIG_DIR was $CLAUDE_CONFIG_DIR" >&2',
56 " exit 5",
57 "fi",
58 'if [ -n "$T3_FAKE_CLAUDE_STDERR" ]; then',
59 ' printf "%s\\n" "$T3_FAKE_CLAUDE_STDERR" >&2',
60 "fi",
61 'printf "%s" "$T3_FAKE_CLAUDE_OUTPUT"',
62 'exit "${T3_FAKE_CLAUDE_EXIT_CODE:-0}"',
51 ' stdinContent = Buffer.concat(chunks).toString("utf8");',
52 "}",
53 "",
54 "const argsMustContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_CONTAIN;",
55 "if (argsMustContain && !args.includes(argsMustContain)) {",
56 ' fail("args missing expected content", 2);',
57 "}",
58 "",
59 "const argsMustNotContain = process.env.T3_FAKE_CLAUDE_ARGS_MUST_NOT_CONTAIN;",
60 "if (argsMustNotContain && args.includes(argsMustNotContain)) {",
61 ' fail("args contained forbidden content", 3);',
62 "}",
63 "",
64 "const stdinMustContain = process.env.T3_FAKE_CLAUDE_STDIN_MUST_CONTAIN;",
65 "if (stdinMustContain && !stdinContent.includes(stdinMustContain)) {",
66 ' fail("stdin missing expected content", 4);',
67 "}",
68 "",
69 "const configDirMustBe = process.env.T3_FAKE_CLAUDE_CONFIG_DIR_MUST_BE;",
70 "if (configDirMustBe && process.env.CLAUDE_CONFIG_DIR !== configDirMustBe) {",
71 ' fail("CLAUDE_CONFIG_DIR was " + (process.env.CLAUDE_CONFIG_DIR ?? ""), 5);',
72 "}",
73 "",
74 "const stderrText = process.env.T3_FAKE_CLAUDE_STDERR;",
75 "if (stderrText) {",
76 ' process.stderr.write(stderrText + "\\n");',
77 "}",
78 "",
79 'process.stdout.write(process.env.T3_FAKE_CLAUDE_OUTPUT ?? "");',
80 "process.exitCode = Number(process.env.T3_FAKE_CLAUDE_EXIT_CODE ?? 0);",
6381 "",
6482 ].join("\n"),
6583 );
66 yield* fs.chmod(claudePath, 0o755);
84
85 if (isWindows) {
86 // Windows resolves executables through PATHEXT, so the entry point has to
87 // carry a real extension. `resolveSpawnCommand` spawns `.cmd` via a shell.
88 yield* fs.writeFileString(
89 path.join(binDir, "claude.cmd"),
90 ["@echo off", 'node "%~dp0claude-stub.mjs" %*', "exit /b %ERRORLEVEL%", ""].join("\r\n"),
91 );
92 } else {
93 const claudePath = path.join(binDir, "claude");
94 yield* fs.writeFileString(
95 claudePath,
96 ["#!/bin/sh", 'exec node "$(dirname "$0")/claude-stub.mjs" "$@"', ""].join("\n"),
97 );
98 yield* fs.chmod(claudePath, 0o755);
99 }
100
67101 return binDir;
68102 });
69103}
......@@ -85,6 +119,7 @@ function withFakeClaudeEnv<A, E, R>(
85119 const fs = yield* FileSystem.FileSystem;
86120 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-claude-text-" });
87121 const binDir = yield* makeFakeClaudeBinary(tempDir);
122 const pathDelimiter = (yield* isHostWindows) ? ";" : ":";
88123 const previousPath = process.env.PATH;
89124 const previousOutput = process.env.T3_FAKE_CLAUDE_OUTPUT;
90125 const previousExitCode = process.env.T3_FAKE_CLAUDE_EXIT_CODE;
......@@ -96,7 +131,7 @@ function withFakeClaudeEnv<A, E, R>(
96131
97132 yield* Effect.acquireRelease(
98133 Effect.sync(() => {
99 process.env.PATH = `${binDir}:${previousPath ?? ""}`;
134 process.env.PATH = `${binDir}${pathDelimiter}${previousPath ?? ""}`;
100135 process.env.T3_FAKE_CLAUDE_OUTPUT = input.output;
101136
102137 if (input.exitCode !== undefined) {