Skip to content

Commit 339f2c0

Browse files
anandgupta42claude
andauthored
test: add E2E tests for engine stdio piping (#52)
* test: add E2E tests for engine.ts stdio piping Verify that all execFileSync calls in engine.ts use { stdio: "pipe" } to prevent subprocess output (uv, pip install, python --version, etc.) from leaking into the TUI's text input area. Tests use real subprocess calls via spawnSync to confirm the piping behavior end-to-end, plus a source-level check that every call site in engine.ts includes the stdio option. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add stdio: "pipe" to all execFileSync calls in engine.ts Without this option, subprocess output from uv, pip install, tar, and python/uv --version leaks into the TUI's text input area. Piping stdio suppresses this noise while still capturing the return value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 32b8d4f commit 339f2c0

File tree

2 files changed

+135
-6
lines changed

2 files changed

+135
-6
lines changed

packages/altimate-code/src/bridge/engine.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function ensureUv(): Promise<void> {
114114
// Extract: tar.gz on unix, zip on windows
115115
if (asset.endsWith(".tar.gz")) {
116116
// Use tar to extract, the binary is inside a directory named like "uv-aarch64-apple-darwin"
117-
execFileSync("tar", ["-xzf", tmpFile, "-C", path.join(dir, "bin")])
117+
execFileSync("tar", ["-xzf", tmpFile, "-C", path.join(dir, "bin")], { stdio: "pipe" })
118118
// The extracted dir has the same name as the asset minus .tar.gz
119119
const extractedDir = path.join(dir, "bin", asset.replace(".tar.gz", ""))
120120
// Move uv binary from extracted dir to engine/bin/uv
@@ -126,7 +126,7 @@ export async function ensureUv(): Promise<void> {
126126
execFileSync("powershell", [
127127
"-Command",
128128
`Expand-Archive -Path '${tmpFile}' -DestinationPath '${path.join(dir, "bin")}' -Force`,
129-
])
129+
], { stdio: "pipe" })
130130
const extractedDir = path.join(dir, "bin", asset.replace(".zip", ""))
131131
await fs.rename(path.join(extractedDir, "uv.exe"), uv)
132132
await fs.rm(extractedDir, { recursive: true, force: true })
@@ -172,7 +172,7 @@ async function ensureEngineImpl(): Promise<void> {
172172
if (!existsSync(venvDir)) {
173173
UI.println(`${UI.Style.TEXT_DIM}Creating Python environment...${UI.Style.TEXT_NORMAL}`)
174174
try {
175-
execFileSync(uv, ["venv", "--python", "3.12", venvDir])
175+
execFileSync(uv, ["venv", "--python", "3.12", venvDir], { stdio: "pipe" })
176176
} catch (e: any) {
177177
Telemetry.track({
178178
type: "engine_error",
@@ -189,7 +189,7 @@ async function ensureEngineImpl(): Promise<void> {
189189
const pythonPath = enginePythonPath()
190190
UI.println(`${UI.Style.TEXT_DIM}Installing altimate-engine ${ALTIMATE_ENGINE_VERSION}...${UI.Style.TEXT_NORMAL}`)
191191
try {
192-
execFileSync(uv, ["pip", "install", "--python", pythonPath, `altimate-engine==${ALTIMATE_ENGINE_VERSION}`])
192+
execFileSync(uv, ["pip", "install", "--python", pythonPath, `altimate-engine==${ALTIMATE_ENGINE_VERSION}`], { stdio: "pipe" })
193193
} catch (e: any) {
194194
Telemetry.track({
195195
type: "engine_error",
@@ -202,9 +202,9 @@ async function ensureEngineImpl(): Promise<void> {
202202
}
203203

204204
// Get python version
205-
const pyVersion = execFileSync(pythonPath, ["--version"]).toString().trim()
205+
const pyVersion = execFileSync(pythonPath, ["--version"], { stdio: "pipe" }).toString().trim()
206206
// Get uv version
207-
const uvVersion = execFileSync(uv, ["--version"]).toString().trim()
207+
const uvVersion = execFileSync(uv, ["--version"], { stdio: "pipe" }).toString().trim()
208208

209209
await writeManifest({
210210
engine_version: ALTIMATE_ENGINE_VERSION,
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* E2E tests verifying that execFileSync with { stdio: "pipe" } prevents
3+
* subprocess output from leaking to the parent's stdout/stderr.
4+
*
5+
* These are real tests — they spawn actual child processes running real
6+
* commands and verify the captured output is clean.
7+
*/
8+
9+
import { describe, expect, test } from "bun:test"
10+
import { execFileSync, spawnSync } from "child_process"
11+
import path from "path"
12+
import os from "os"
13+
import fsp from "fs/promises"
14+
15+
describe("execFileSync stdio piping behavior", () => {
16+
test("stdio: 'pipe' prevents subprocess stdout from reaching parent", () => {
17+
// Run a child process that calls execFileSync with stdio: "pipe"
18+
// and verify the parent sees nothing on stdout/stderr
19+
const result = spawnSync("bun", ["-e", `
20+
const { execFileSync } = require("child_process");
21+
execFileSync("echo", ["THIS_SHOULD_NOT_LEAK"], { stdio: "pipe" });
22+
execFileSync("echo", ["ALSO_SHOULD_NOT_LEAK"], { stdio: "pipe" });
23+
`], { encoding: "utf-8" })
24+
25+
expect(result.stdout).not.toContain("THIS_SHOULD_NOT_LEAK")
26+
expect(result.stdout).not.toContain("ALSO_SHOULD_NOT_LEAK")
27+
expect(result.stderr).not.toContain("THIS_SHOULD_NOT_LEAK")
28+
expect(result.stderr).not.toContain("ALSO_SHOULD_NOT_LEAK")
29+
})
30+
31+
test("without stdio: 'pipe', subprocess output DOES leak to parent", () => {
32+
// Control test: prove that without stdio: "pipe", output leaks through
33+
const result = spawnSync("bun", ["-e", `
34+
const { execFileSync } = require("child_process");
35+
execFileSync("echo", ["CONTROL_LEAKED"], { stdio: "inherit" });
36+
`], { encoding: "utf-8" })
37+
38+
// With stdio: "inherit", the child's subprocess output goes to the
39+
// child's stdout, which the parent captures
40+
expect(result.stdout).toContain("CONTROL_LEAKED")
41+
})
42+
43+
test("stdio: 'pipe' still captures the return value", () => {
44+
// Verify that piped output is available as the return value
45+
const output = execFileSync("echo", ["captured_value"], { stdio: "pipe" })
46+
expect(output.toString().trim()).toBe("captured_value")
47+
})
48+
})
49+
50+
describe("engine.ts subprocess noise suppression", () => {
51+
test("commands matching engine.ts patterns don't leak output when piped", () => {
52+
// Run a child process that mimics the exact execFileSync patterns in
53+
// engine.ts: version checks, tar, and noisy commands — all with
54+
// stdio: "pipe". Verify no output leaks.
55+
const script = `
56+
const { execFileSync } = require("child_process");
57+
58+
// Mimics: execFileSync(pythonPath, ["--version"], { stdio: "pipe" })
59+
try { execFileSync("python3", ["--version"], { stdio: "pipe" }); } catch {}
60+
61+
// Mimics: execFileSync("tar", ["--version"], { stdio: "pipe" })
62+
try { execFileSync("tar", ["--version"], { stdio: "pipe" }); } catch {}
63+
64+
// Mimics: execFileSync(uv, ["--version"], { stdio: "pipe" })
65+
// Use a command that prints to both stdout and stderr
66+
try { execFileSync("ls", ["--version"], { stdio: "pipe" }); } catch {}
67+
68+
// Simulate noisy pip-like output
69+
try { execFileSync("echo", ["Collecting altimate-engine==0.1.0\\nInstalling collected packages\\nSuccessfully installed"], { stdio: "pipe" }); } catch {}
70+
`
71+
const result = spawnSync("bun", ["-e", script], { encoding: "utf-8" })
72+
73+
// None of the subprocess output should appear in the parent's streams
74+
expect(result.stdout).not.toContain("Python")
75+
expect(result.stdout).not.toContain("tar")
76+
expect(result.stdout).not.toContain("Collecting")
77+
expect(result.stdout).not.toContain("Installing")
78+
expect(result.stderr).not.toContain("Python")
79+
expect(result.stderr).not.toContain("Collecting")
80+
})
81+
82+
test("same commands WITHOUT piping DO leak output (control)", () => {
83+
// Control: verify the same commands actually produce output when not piped
84+
const result = spawnSync("bun", ["-e", `
85+
const { execFileSync } = require("child_process");
86+
try { execFileSync("python3", ["--version"], { stdio: "inherit" }); } catch {}
87+
`], { encoding: "utf-8" })
88+
89+
expect(result.stdout).toContain("Python")
90+
})
91+
92+
test("engine.ts uses stdio: 'pipe' on all execFileSync calls", async () => {
93+
// Read the actual source and verify every execFileSync call site
94+
// includes { stdio: "pipe" } — this ensures the behavior tested above
95+
// is actually applied in the production code
96+
const engineSrc = path.resolve(
97+
__dirname,
98+
"../../src/bridge/engine.ts",
99+
)
100+
const source = await fsp.readFile(engineSrc, "utf-8")
101+
const lines = source.split("\n")
102+
103+
// Find every execFileSync call and extract the full multi-line expression
104+
const callSites: { line: number; text: string }[] = []
105+
for (let i = 0; i < lines.length; i++) {
106+
if (!lines[i].includes("execFileSync(")) continue
107+
108+
let text = ""
109+
let depth = 0
110+
for (let j = i; j < lines.length; j++) {
111+
text += lines[j] + "\n"
112+
for (const ch of lines[j]) {
113+
if (ch === "(") depth++
114+
if (ch === ")") depth--
115+
}
116+
if (depth <= 0) break
117+
}
118+
callSites.push({ line: i + 1, text })
119+
}
120+
121+
// engine.ts has 6 execFileSync calls:
122+
// tar, powershell, uv venv, uv pip install, python --version, uv --version
123+
expect(callSites.length).toBeGreaterThanOrEqual(6)
124+
125+
for (const site of callSites) {
126+
expect(site.text).toContain('stdio: "pipe"')
127+
}
128+
})
129+
})

0 commit comments

Comments
 (0)