Skip to content

Commit c94b24a

Browse files
committed
Add failing tests for subprocess API
Test fixtures for 10 subprocess scenarios: - spawn-basic.ts: Basic spawn and exit code - spawn-stdout.ts: Capture stdout output - spawn-stderr.ts: Capture stderr output - spawn-stdin.ts: Write to process stdin - spawn-cwd.ts: Set working directory - spawn-env.ts: Set environment variables - spawn-kill.ts: Kill a running process - spawn-error.ts: Handle errors gracefully - spawn-args.ts: Pass arguments to command - spawn-exit-code.ts: Get non-zero exit codes All tests currently fail (subprocess not implemented yet).
1 parent e0cd9d2 commit c94b24a

11 files changed

Lines changed: 536 additions & 0 deletions

File tree

tests/cli.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,4 +3435,170 @@ export default async () => {
34353435
expect(stdout).toContain('headers-object test complete');
34363436
});
34373437
});
3438+
3439+
// ==================== SUBPROCESS API ====================
3440+
3441+
describe('subprocess', () => {
3442+
/**
3443+
* Subprocess API test suite
3444+
*
3445+
* These tests verify funee's subprocess spawning and management API.
3446+
* The API provides:
3447+
* - spawn() for running commands
3448+
* - Process object for managing running processes
3449+
* - Streaming stdin/stdout/stderr
3450+
* - Signal handling and process control
3451+
*/
3452+
3453+
it('spawns a basic command and gets exit code', async () => {
3454+
/**
3455+
* Tests basic subprocess spawning:
3456+
* - spawn(command, args) runs a command
3457+
* - Returns status with exit code
3458+
* - success is true for exit code 0
3459+
*/
3460+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-basic.ts']);
3461+
3462+
expect(exitCode).toBe(0);
3463+
expect(stdout).toContain('exit code: 0');
3464+
expect(stdout).toContain('success: true');
3465+
expect(stdout).toContain('spawn-basic: pass');
3466+
});
3467+
3468+
it('captures stdout output from subprocess', async () => {
3469+
/**
3470+
* Tests stdout capture:
3471+
* - stdout is available as Uint8Array
3472+
* - stdoutText() returns decoded string
3473+
* - Output matches command output
3474+
*/
3475+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-stdout.ts']);
3476+
3477+
expect(exitCode).toBe(0);
3478+
expect(stdout).toContain('stdout text: "hello world"');
3479+
expect(stdout).toContain('spawn-stdout: pass');
3480+
});
3481+
3482+
it('captures stderr output from subprocess', async () => {
3483+
/**
3484+
* Tests stderr capture:
3485+
* - stderr is captured separately
3486+
* - stderrText() returns decoded string
3487+
* - Errors appear in stderr, not stdout
3488+
*/
3489+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-stderr.ts']);
3490+
3491+
expect(exitCode).toBe(0);
3492+
expect(stdout).toContain('stderr contains error: true');
3493+
expect(stdout).toContain('stdout is empty: true');
3494+
expect(stdout).toContain('success: false');
3495+
expect(stdout).toContain('spawn-stderr: pass');
3496+
});
3497+
3498+
it('writes to subprocess stdin', async () => {
3499+
/**
3500+
* Tests stdin writing:
3501+
* - stdin: "piped" enables writing
3502+
* - writeInput() sends data
3503+
* - Process receives input
3504+
*/
3505+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-stdin.ts']);
3506+
3507+
expect(exitCode).toBe(0);
3508+
expect(stdout).toContain('output: "hello from stdin"');
3509+
expect(stdout).toContain('matches input: true');
3510+
expect(stdout).toContain('spawn-stdin: pass');
3511+
});
3512+
3513+
it('sets working directory for subprocess', async () => {
3514+
/**
3515+
* Tests cwd option:
3516+
* - cwd sets process working directory
3517+
* - Commands run in specified directory
3518+
*/
3519+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-cwd.ts']);
3520+
3521+
expect(exitCode).toBe(0);
3522+
expect(stdout).toContain('is /tmp: true');
3523+
expect(stdout).toContain('spawn-cwd: pass');
3524+
});
3525+
3526+
it('sets environment variables for subprocess', async () => {
3527+
/**
3528+
* Tests env options:
3529+
* - env sets custom environment variables
3530+
* - inheritEnv controls parent env inheritance
3531+
*/
3532+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-env.ts']);
3533+
3534+
expect(exitCode).toBe(0);
3535+
expect(stdout).toContain('custom env var: custom_value');
3536+
expect(stdout).toContain('inheritEnv true, has PATH: true');
3537+
expect(stdout).toContain('spawn-env: pass');
3538+
});
3539+
3540+
it('kills a running subprocess', async () => {
3541+
/**
3542+
* Tests process killing:
3543+
* - kill(signal) sends signal to process
3544+
* - Process terminates
3545+
* - status.signal contains termination signal
3546+
*/
3547+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-kill.ts']);
3548+
3549+
expect(exitCode).toBe(0);
3550+
expect(stdout).toContain('pid is number: true');
3551+
expect(stdout).toContain('success: false');
3552+
expect(stdout).toContain('signal: SIGTERM');
3553+
expect(stdout).toContain('spawn-kill: pass');
3554+
});
3555+
3556+
it('handles subprocess errors gracefully', async () => {
3557+
/**
3558+
* Tests error handling:
3559+
* - Command not found throws error
3560+
* - Invalid cwd throws error
3561+
* - Errors have descriptive messages
3562+
*/
3563+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-error.ts']);
3564+
3565+
expect(exitCode).toBe(0);
3566+
expect(stdout).toContain('command not found error caught: true');
3567+
expect(stdout).toContain('spawn-error: pass');
3568+
});
3569+
3570+
it('passes arguments to subprocess correctly', async () => {
3571+
/**
3572+
* Tests argument handling:
3573+
* - Multiple arguments work
3574+
* - Arguments with spaces handled
3575+
* - cmd array form works
3576+
*/
3577+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-args.ts']);
3578+
3579+
expect(exitCode).toBe(0);
3580+
expect(stdout).toContain('multiple args: "one two three"');
3581+
expect(stdout).toContain('cmd array args: "from cmd array"');
3582+
expect(stdout).toContain('spawn-args: pass');
3583+
});
3584+
3585+
it('captures non-zero exit codes', async () => {
3586+
/**
3587+
* Tests exit code handling:
3588+
* - Non-zero codes captured correctly
3589+
* - success is false for non-zero
3590+
* - Different codes are distinguishable
3591+
*/
3592+
const { stdout, stderr, exitCode } = await runFunee(['process/spawn-exit-code.ts']);
3593+
3594+
expect(exitCode).toBe(0);
3595+
expect(stdout).toContain('exit 1 - code: 1');
3596+
expect(stdout).toContain('exit 1 - success: false');
3597+
expect(stdout).toContain('exit 42 - code: 42');
3598+
expect(stdout).toContain('exit 0 - code: 0');
3599+
expect(stdout).toContain('exit 0 - success: true');
3600+
expect(stdout).toContain('exit 255 - code: 255');
3601+
expect(stdout).toContain('spawn-exit-code: pass');
3602+
});
3603+
});
34383604
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Test: Pass arguments to subprocess
3+
*
4+
* Verifies that:
5+
* 1. Arguments are passed correctly to the command
6+
* 2. Arguments with spaces are handled properly
7+
* 3. Multiple arguments work correctly
8+
*/
9+
import { spawn, log } from "funee";
10+
11+
export default async function main() {
12+
// Test 1: Multiple arguments
13+
const result1 = await spawn("echo", ["one", "two", "three"]);
14+
const text1 = result1.stdoutText().trim();
15+
log(`multiple args: "${text1}"`);
16+
17+
// Test 2: Argument with spaces (should be treated as single arg)
18+
const result2 = await spawn("echo", ["hello world"]);
19+
const text2 = result2.stdoutText().trim();
20+
log(`arg with space: "${text2}"`);
21+
22+
// Test 3: Arguments in cmd array (full options form)
23+
const proc = spawn({
24+
cmd: ["echo", "from", "cmd", "array"],
25+
stdout: "piped",
26+
});
27+
const output = await proc.output();
28+
const text3 = output.stdoutText().trim();
29+
log(`cmd array args: "${text3}"`);
30+
31+
// Test 4: Special characters in arguments
32+
const result4 = await spawn("echo", ["$HOME", "&&", "test"]);
33+
const text4 = result4.stdoutText().trim();
34+
// echo should output the literal strings since not going through shell
35+
log(`special chars: "${text4}"`);
36+
37+
if (text1 === "one two three" && text3 === "from cmd array") {
38+
log("spawn-args: pass");
39+
} else {
40+
log("spawn-args: FAIL");
41+
}
42+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Test: Basic subprocess spawn and exit code
3+
*
4+
* Verifies that:
5+
* 1. spawn() can run a simple command
6+
* 2. The process exits with code 0 for successful commands
7+
* 3. status.success is true for exit code 0
8+
*/
9+
import { spawn, log } from "funee";
10+
11+
export default async function main() {
12+
// Simple command that exits successfully
13+
const result = await spawn("echo", ["hello"]);
14+
15+
// Check exit code
16+
const exitCode = result.status.code;
17+
log(`exit code: ${exitCode}`);
18+
log(`success: ${result.status.success}`);
19+
20+
// Verify success
21+
if (exitCode === 0 && result.status.success) {
22+
log("spawn-basic: pass");
23+
} else {
24+
log("spawn-basic: FAIL");
25+
}
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Test: Set working directory for subprocess
3+
*
4+
* Verifies that:
5+
* 1. cwd option changes the process working directory
6+
* 2. Commands run in the specified directory
7+
*/
8+
import { spawn, log } from "funee";
9+
10+
export default async function main() {
11+
// Run pwd in /tmp directory
12+
const result = await spawn({
13+
cmd: ["pwd"],
14+
cwd: "/tmp",
15+
stdout: "piped",
16+
});
17+
18+
const output = await result.output();
19+
const cwd = output.stdoutText().trim();
20+
21+
log(`cwd: ${cwd}`);
22+
log(`is /tmp: ${cwd === "/tmp" || cwd === "/private/tmp"}`);
23+
24+
// macOS resolves /tmp to /private/tmp
25+
if (cwd === "/tmp" || cwd === "/private/tmp") {
26+
log("spawn-cwd: pass");
27+
} else {
28+
log("spawn-cwd: FAIL");
29+
}
30+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Test: Set environment variables for subprocess
3+
*
4+
* Verifies that:
5+
* 1. env option sets custom environment variables
6+
* 2. Process can access the environment variables
7+
* 3. inheritEnv controls whether parent env is included
8+
*/
9+
import { spawn, log } from "funee";
10+
11+
export default async function main() {
12+
// Test 1: Set custom env var
13+
const result1 = await spawn({
14+
cmd: ["sh", "-c", "echo $MY_VAR"],
15+
env: { MY_VAR: "custom_value" },
16+
stdout: "piped",
17+
});
18+
19+
const output1 = await result1.output();
20+
const value1 = output1.stdoutText().trim();
21+
log(`custom env var: ${value1}`);
22+
23+
// Test 2: inheritEnv: false should not have PATH
24+
const result2 = await spawn({
25+
cmd: ["sh", "-c", "echo PATH=$PATH"],
26+
env: { MY_VAR: "test" },
27+
inheritEnv: false,
28+
stdout: "piped",
29+
});
30+
31+
const output2 = await result2.output();
32+
const value2 = output2.stdoutText().trim();
33+
log(`inheritEnv false, PATH: ${value2}`);
34+
35+
// Test 3: inheritEnv: true (default) should have PATH
36+
const result3 = await spawn({
37+
cmd: ["sh", "-c", "echo PATH_EXISTS=$PATH"],
38+
env: { MY_VAR: "test" },
39+
inheritEnv: true,
40+
stdout: "piped",
41+
});
42+
43+
const output3 = await result3.output();
44+
const value3 = output3.stdoutText().trim();
45+
const hasPath = value3.includes("/usr") || value3.includes("/bin");
46+
log(`inheritEnv true, has PATH: ${hasPath}`);
47+
48+
if (value1 === "custom_value" && hasPath) {
49+
log("spawn-env: pass");
50+
} else {
51+
log("spawn-env: FAIL");
52+
}
53+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Test: Handle subprocess errors gracefully
3+
*
4+
* Verifies that:
5+
* 1. Command not found throws an error
6+
* 2. Error message is descriptive
7+
* 3. Does not crash the runtime
8+
*/
9+
import { spawn, log } from "funee";
10+
11+
export default async function main() {
12+
// Test 1: Non-existent command
13+
let errorCaught = false;
14+
let errorMessage = "";
15+
16+
try {
17+
await spawn("nonexistent_command_xyz123", ["arg"]);
18+
} catch (e: unknown) {
19+
errorCaught = true;
20+
errorMessage = e instanceof Error ? e.message : String(e);
21+
}
22+
23+
log(`command not found error caught: ${errorCaught}`);
24+
log(`error message contains useful info: ${errorMessage.includes("not found") || errorMessage.includes("No such file") || errorMessage.includes("ENOENT")}`);
25+
26+
// Test 2: Invalid cwd
27+
let cwdErrorCaught = false;
28+
29+
try {
30+
const proc = spawn({
31+
cmd: ["echo", "hello"],
32+
cwd: "/nonexistent/directory/xyz123",
33+
});
34+
await proc.status;
35+
} catch (e: unknown) {
36+
cwdErrorCaught = true;
37+
}
38+
39+
log(`invalid cwd error caught: ${cwdErrorCaught}`);
40+
41+
if (errorCaught) {
42+
log("spawn-error: pass");
43+
} else {
44+
log("spawn-error: FAIL");
45+
}
46+
}

0 commit comments

Comments
 (0)