Skip to content

Commit 0ae74da

Browse files
committed
tests
1 parent 75ae745 commit 0ae74da

File tree

5 files changed

+393
-24
lines changed

5 files changed

+393
-24
lines changed

README.md

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ Designed for AI agents that need a secure, sandboxed bash environment.
66

77
Supports optional network access via `curl` with secure-by-default URL filtering.
88

9+
## Security model
10+
11+
- The shell only has access to the provided file system.
12+
- Execution is protected against infinite loops or recursion through.
13+
- Binaries or even WASM are inherently unsupported (Use [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) or a similar product if a full VM is needed).
14+
- There is no network access by default.
15+
- Network access can be enabled, but requests are checked against URL prefix allow-lists and HTTP-method allow-lists. See [network access](#network-access) for details
16+
917
## Installation
1018

1119
```bash
@@ -96,30 +104,6 @@ await sandbox.mkDir("/app/logs", { recursive: true });
96104
await sandbox.stop();
97105
```
98106

99-
#### Command Streaming
100-
101-
The `Command` class provides multiple ways to access output:
102-
103-
```typescript
104-
const cmd = await sandbox.runCommand("echo hello; echo world >&2");
105-
106-
// Get stdout/stderr separately
107-
const stdout = await cmd.stdout(); // "hello\n"
108-
const stderr = await cmd.stderr(); // "world\n"
109-
110-
// Get combined output
111-
const output = await cmd.output(); // "hello\nworld\n"
112-
113-
// Stream logs as they arrive
114-
for await (const msg of cmd.logs()) {
115-
console.log(msg.type, msg.data); // "stdout" "hello\n", "stderr" "world\n"
116-
}
117-
118-
// Wait for completion
119-
const finished = await cmd.wait();
120-
console.log(finished.exitCode); // 0
121-
```
122-
123107
### Interactive Shell
124108

125109
```bash

src/commands/help/help.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("help", () => {
5+
describe("list all commands", () => {
6+
it("should list available commands", async () => {
7+
const env = new BashEnv();
8+
const result = await env.exec("help");
9+
expect(result.stdout).toContain("Available commands");
10+
expect(result.exitCode).toBe(0);
11+
});
12+
13+
it("should show file operations category", async () => {
14+
const env = new BashEnv();
15+
const result = await env.exec("help");
16+
expect(result.stdout).toContain("File operations");
17+
expect(result.stdout).toContain("ls");
18+
expect(result.stdout).toContain("cat");
19+
});
20+
21+
it("should show text processing category", async () => {
22+
const env = new BashEnv();
23+
const result = await env.exec("help");
24+
expect(result.stdout).toContain("Text processing");
25+
expect(result.stdout).toContain("grep");
26+
expect(result.stdout).toContain("sed");
27+
});
28+
29+
it("should show usage hint", async () => {
30+
const env = new BashEnv();
31+
const result = await env.exec("help");
32+
expect(result.stdout).toContain("--help");
33+
});
34+
});
35+
36+
describe("help for specific command", () => {
37+
it("should show help for ls", async () => {
38+
const env = new BashEnv();
39+
const result = await env.exec("help ls");
40+
expect(result.stdout).toContain("ls");
41+
expect(result.exitCode).toBe(0);
42+
});
43+
44+
it("should show help for grep", async () => {
45+
const env = new BashEnv();
46+
const result = await env.exec("help grep");
47+
expect(result.stdout).toContain("grep");
48+
expect(result.exitCode).toBe(0);
49+
});
50+
51+
it("should error for unknown command", async () => {
52+
const env = new BashEnv();
53+
const result = await env.exec("help nonexistent");
54+
expect(result.exitCode).toBe(127);
55+
});
56+
});
57+
58+
describe("--help flag", () => {
59+
it("should show help's own help", async () => {
60+
const env = new BashEnv();
61+
const result = await env.exec("help --help");
62+
expect(result.stdout).toContain("help");
63+
expect(result.stdout).toContain("Usage");
64+
expect(result.exitCode).toBe(0);
65+
});
66+
67+
it("should support -h flag", async () => {
68+
const env = new BashEnv();
69+
const result = await env.exec("help -h");
70+
expect(result.stdout).toContain("help");
71+
expect(result.exitCode).toBe(0);
72+
});
73+
});
74+
});

src/commands/printf/printf.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("printf", () => {
5+
describe("basic format specifiers", () => {
6+
it("should format string with %s", async () => {
7+
const env = new BashEnv();
8+
const result = await env.exec('printf "Hello %s" world');
9+
expect(result.stdout).toBe("Hello world");
10+
expect(result.exitCode).toBe(0);
11+
});
12+
13+
it("should format integer with %d", async () => {
14+
const env = new BashEnv();
15+
const result = await env.exec('printf "Number: %d" 42');
16+
expect(result.stdout).toBe("Number: 42");
17+
expect(result.exitCode).toBe(0);
18+
});
19+
20+
it("should format float with %f", async () => {
21+
const env = new BashEnv();
22+
const result = await env.exec('printf "Value: %f" 3.14');
23+
expect(result.stdout).toBe("Value: 3.14");
24+
expect(result.exitCode).toBe(0);
25+
});
26+
27+
it("should format hex with %x", async () => {
28+
const env = new BashEnv();
29+
const result = await env.exec('printf "Hex: %x" 255');
30+
expect(result.stdout).toBe("Hex: ff");
31+
expect(result.exitCode).toBe(0);
32+
});
33+
34+
it("should format octal with %o", async () => {
35+
const env = new BashEnv();
36+
const result = await env.exec('printf "Octal: %o" 8');
37+
expect(result.stdout).toBe("Octal: 10");
38+
expect(result.exitCode).toBe(0);
39+
});
40+
41+
it("should handle literal %% ", async () => {
42+
const env = new BashEnv();
43+
const result = await env.exec('printf "100%%"');
44+
expect(result.stdout).toBe("100%");
45+
expect(result.exitCode).toBe(0);
46+
});
47+
48+
it("should handle multiple arguments", async () => {
49+
const env = new BashEnv();
50+
const result = await env.exec('printf "%s is %d years old" Alice 30');
51+
expect(result.stdout).toBe("Alice is 30 years old");
52+
expect(result.exitCode).toBe(0);
53+
});
54+
});
55+
56+
describe("escape sequences", () => {
57+
it("should handle newline \\n", async () => {
58+
const env = new BashEnv();
59+
const result = await env.exec('printf "line1\\nline2"');
60+
expect(result.stdout).toBe("line1\nline2");
61+
});
62+
63+
it("should handle tab \\t", async () => {
64+
const env = new BashEnv();
65+
const result = await env.exec('printf "col1\\tcol2"');
66+
expect(result.stdout).toBe("col1\tcol2");
67+
});
68+
69+
it("should handle backslash \\\\", async () => {
70+
const env = new BashEnv();
71+
// 8 backslashes in source -> 4 in bash string -> 2 for printf -> 1 literal
72+
const result = await env.exec('printf "x\\\\\\\\y"');
73+
expect(result.stdout).toBe("x\\y");
74+
});
75+
76+
it("should handle carriage return \\r", async () => {
77+
const env = new BashEnv();
78+
const result = await env.exec('printf "hello\\rworld"');
79+
expect(result.stdout).toBe("hello\rworld");
80+
});
81+
82+
it("should handle octal escape sequences", async () => {
83+
const env = new BashEnv();
84+
const result = await env.exec('printf "\\101\\102\\103"');
85+
expect(result.stdout).toBe("ABC");
86+
});
87+
});
88+
89+
describe("width and precision", () => {
90+
it("should handle width specifier", async () => {
91+
const env = new BashEnv();
92+
const result = await env.exec('printf "%10s" "hi"');
93+
expect(result.stdout).toBe(" hi");
94+
});
95+
96+
it("should handle precision for floats", async () => {
97+
const env = new BashEnv();
98+
const result = await env.exec('printf "%.2f" 3.14159');
99+
expect(result.stdout).toBe("3.14");
100+
});
101+
102+
it("should handle zero-padding", async () => {
103+
const env = new BashEnv();
104+
const result = await env.exec('printf "%05d" 42');
105+
expect(result.stdout).toBe("00042");
106+
});
107+
108+
it("should handle left-justify with -", async () => {
109+
const env = new BashEnv();
110+
const result = await env.exec('printf "%-10s|" "hi"');
111+
expect(result.stdout).toBe("hi |");
112+
});
113+
});
114+
115+
describe("error handling", () => {
116+
it("should error with no arguments", async () => {
117+
const env = new BashEnv();
118+
const result = await env.exec("printf");
119+
expect(result.stderr).toContain("usage");
120+
expect(result.exitCode).toBe(1);
121+
});
122+
123+
it("should handle missing arguments gracefully", async () => {
124+
const env = new BashEnv();
125+
const result = await env.exec('printf "%s %s" only');
126+
expect(result.stdout).toBe("only ");
127+
expect(result.exitCode).toBe(0);
128+
});
129+
130+
it("should handle non-numeric for %d", async () => {
131+
const env = new BashEnv();
132+
const result = await env.exec('printf "%d" notanumber');
133+
expect(result.stdout).toBe("0");
134+
expect(result.exitCode).toBe(0);
135+
});
136+
});
137+
138+
describe("--help", () => {
139+
it("should display help", async () => {
140+
const env = new BashEnv();
141+
const result = await env.exec("printf --help");
142+
expect(result.stdout).toContain("printf");
143+
expect(result.stdout).toContain("FORMAT");
144+
expect(result.exitCode).toBe(0);
145+
});
146+
});
147+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, expect, it } from "vitest";
2+
import { BashEnv } from "../../BashEnv.js";
3+
4+
describe("readlink", () => {
5+
describe("basic usage", () => {
6+
it("should read symlink target", async () => {
7+
const env = new BashEnv();
8+
await env.exec("echo content > /tmp/target.txt");
9+
await env.exec("ln -s /tmp/target.txt /tmp/link");
10+
const result = await env.exec("readlink /tmp/link");
11+
expect(result.stdout).toBe("/tmp/target.txt\n");
12+
expect(result.exitCode).toBe(0);
13+
});
14+
15+
it("should read relative symlink target", async () => {
16+
const env = new BashEnv();
17+
await env.exec("echo content > /tmp/target.txt");
18+
await env.exec("ln -s target.txt /tmp/link");
19+
const result = await env.exec("readlink /tmp/link");
20+
expect(result.stdout).toBe("target.txt\n");
21+
expect(result.exitCode).toBe(0);
22+
});
23+
24+
it("should handle multiple files", async () => {
25+
const env = new BashEnv();
26+
await env.exec("echo a > /tmp/a.txt && echo b > /tmp/b.txt");
27+
await env.exec("ln -s /tmp/a.txt /tmp/link1");
28+
await env.exec("ln -s /tmp/b.txt /tmp/link2");
29+
const result = await env.exec("readlink /tmp/link1 /tmp/link2");
30+
expect(result.stdout).toBe("/tmp/a.txt\n/tmp/b.txt\n");
31+
expect(result.exitCode).toBe(0);
32+
});
33+
34+
it("should fail for non-symlink file", async () => {
35+
const env = new BashEnv();
36+
await env.exec("echo content > /tmp/regular.txt");
37+
const result = await env.exec("readlink /tmp/regular.txt");
38+
expect(result.exitCode).toBe(1);
39+
});
40+
41+
it("should fail for non-existent file", async () => {
42+
const env = new BashEnv();
43+
const result = await env.exec("readlink /tmp/nonexistent");
44+
expect(result.exitCode).toBe(1);
45+
});
46+
});
47+
48+
describe("-f (canonicalize)", () => {
49+
it("should canonicalize path through symlinks", async () => {
50+
const env = new BashEnv();
51+
await env.exec("echo content > /tmp/real.txt");
52+
await env.exec("ln -s /tmp/real.txt /tmp/link1");
53+
await env.exec("ln -s /tmp/link1 /tmp/link2");
54+
const result = await env.exec("readlink -f /tmp/link2");
55+
expect(result.stdout).toBe("/tmp/real.txt\n");
56+
expect(result.exitCode).toBe(0);
57+
});
58+
59+
it("should return path for regular file", async () => {
60+
const env = new BashEnv();
61+
await env.exec("echo content > /tmp/file.txt");
62+
const result = await env.exec("readlink -f /tmp/file.txt");
63+
expect(result.stdout).toBe("/tmp/file.txt\n");
64+
expect(result.exitCode).toBe(0);
65+
});
66+
67+
it("should canonicalize with relative symlink components", async () => {
68+
const env = new BashEnv();
69+
await env.exec("mkdir -p /tmp/dir");
70+
await env.exec("echo content > /tmp/dir/target.txt");
71+
await env.exec("ln -s dir/target.txt /tmp/link");
72+
const result = await env.exec("readlink -f /tmp/link");
73+
expect(result.stdout).toBe("/tmp/dir/target.txt\n");
74+
expect(result.exitCode).toBe(0);
75+
});
76+
77+
it("should return resolved path for nonexistent file with -f", async () => {
78+
const env = new BashEnv();
79+
const result = await env.exec("readlink -f /tmp/nonexistent");
80+
expect(result.stdout).toBe("/tmp/nonexistent\n");
81+
expect(result.exitCode).toBe(0);
82+
});
83+
});
84+
85+
describe("error handling", () => {
86+
it("should error on missing operand", async () => {
87+
const env = new BashEnv();
88+
const result = await env.exec("readlink");
89+
expect(result.stderr).toBe("readlink: missing operand\n");
90+
expect(result.exitCode).toBe(1);
91+
});
92+
93+
it("should error on unknown option", async () => {
94+
const env = new BashEnv();
95+
const result = await env.exec("readlink -x /tmp/link");
96+
expect(result.stderr).toContain("invalid option");
97+
expect(result.exitCode).toBe(1);
98+
});
99+
100+
it("should handle -- to end options", async () => {
101+
const env = new BashEnv();
102+
await env.exec("ln -s target /tmp/-f");
103+
const result = await env.exec("readlink -- /tmp/-f");
104+
expect(result.stdout).toBe("target\n");
105+
expect(result.exitCode).toBe(0);
106+
});
107+
});
108+
109+
describe("--help", () => {
110+
it("should display help", async () => {
111+
const env = new BashEnv();
112+
const result = await env.exec("readlink --help");
113+
expect(result.stdout).toContain("readlink");
114+
expect(result.stdout).toContain("-f");
115+
expect(result.exitCode).toBe(0);
116+
});
117+
});
118+
});

0 commit comments

Comments
 (0)