@scelar/nodepod 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bench/integration.bench.d.ts +1 -0
- package/dist/__tests__/bench/memory-volume.bench.d.ts +1 -0
- package/dist/__tests__/bench/polyfills.bench.d.ts +1 -0
- package/dist/__tests__/bench/script-engine.bench.d.ts +1 -0
- package/dist/__tests__/bench/shell.bench.d.ts +1 -0
- package/dist/__tests__/bench/syntax-transforms.bench.d.ts +1 -0
- package/dist/__tests__/bench/version-resolver.bench.d.ts +1 -0
- package/dist/__tests__/buffer.test.d.ts +1 -0
- package/dist/__tests__/byte-encoding.test.d.ts +1 -0
- package/dist/__tests__/digest.test.d.ts +1 -0
- package/dist/__tests__/events.test.d.ts +1 -0
- package/dist/__tests__/memory-volume.test.d.ts +1 -0
- package/dist/__tests__/path.test.d.ts +1 -0
- package/dist/__tests__/process.test.d.ts +1 -0
- package/dist/__tests__/script-engine.test.d.ts +1 -0
- package/dist/__tests__/shell-builtins.test.d.ts +1 -0
- package/dist/__tests__/shell-interpreter.test.d.ts +1 -0
- package/dist/__tests__/shell-parser.test.d.ts +1 -0
- package/dist/__tests__/stream.test.d.ts +1 -0
- package/dist/__tests__/syntax-transforms.test.d.ts +1 -0
- package/dist/__tests__/version-resolver.test.d.ts +1 -0
- package/dist/{child_process-Dopvyd-E.js → child_process-D6oDN2MX.js} +4 -4
- package/dist/{child_process-Dopvyd-E.js.map → child_process-D6oDN2MX.js.map} +1 -1
- package/dist/{child_process-B38qoN6R.cjs → child_process-hmVqFcF7.cjs} +5 -5
- package/dist/{child_process-B38qoN6R.cjs.map → child_process-hmVqFcF7.cjs.map} +1 -1
- package/dist/{index--Qr8LVpQ.js → index-Ale2oba_.js} +240 -136
- package/dist/index-Ale2oba_.js.map +1 -0
- package/dist/{index-cnitc68U.cjs → index-BO1i013L.cjs} +236 -191
- package/dist/index-BO1i013L.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/script-engine.d.ts +2 -0
- package/dist/syntax-transforms.d.ts +1 -0
- package/package.json +97 -95
- package/src/__tests__/bench/integration.bench.ts +117 -0
- package/src/__tests__/bench/memory-volume.bench.ts +115 -0
- package/src/__tests__/bench/polyfills.bench.ts +147 -0
- package/src/__tests__/bench/script-engine.bench.ts +104 -0
- package/src/__tests__/bench/shell.bench.ts +101 -0
- package/src/__tests__/bench/syntax-transforms.bench.ts +82 -0
- package/src/__tests__/bench/version-resolver.bench.ts +95 -0
- package/src/__tests__/buffer.test.ts +273 -0
- package/src/__tests__/byte-encoding.test.ts +98 -0
- package/src/__tests__/digest.test.ts +44 -0
- package/src/__tests__/events.test.ts +245 -0
- package/src/__tests__/memory-volume.test.ts +443 -0
- package/src/__tests__/path.test.ts +181 -0
- package/src/__tests__/process.test.ts +129 -0
- package/src/__tests__/script-engine.test.ts +229 -0
- package/src/__tests__/shell-builtins.test.ts +357 -0
- package/src/__tests__/shell-interpreter.test.ts +157 -0
- package/src/__tests__/shell-parser.test.ts +204 -0
- package/src/__tests__/stream.test.ts +142 -0
- package/src/__tests__/syntax-transforms.test.ts +158 -0
- package/src/__tests__/version-resolver.test.ts +184 -0
- package/src/constants/cdn-urls.ts +18 -18
- package/src/helpers/byte-encoding.ts +51 -39
- package/src/memory-volume.ts +962 -941
- package/src/module-transformer.ts +368 -368
- package/src/packages/installer.ts +396 -396
- package/src/polyfills/buffer.ts +633 -628
- package/src/polyfills/esbuild.ts +854 -854
- package/src/polyfills/events.ts +282 -276
- package/src/polyfills/process.ts +695 -690
- package/src/polyfills/readline.ts +692 -692
- package/src/polyfills/tty.ts +71 -71
- package/src/script-engine.ts +3396 -3375
- package/src/syntax-transforms.ts +543 -561
- package/dist/index--Qr8LVpQ.js.map +0 -1
- package/dist/index-cnitc68U.cjs.map +0 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { builtins } from "../shell/shell-builtins";
|
|
3
|
+
import { MemoryVolume } from "../memory-volume";
|
|
4
|
+
import type { ShellContext } from "../shell/shell-types";
|
|
5
|
+
|
|
6
|
+
function makeCtx(
|
|
7
|
+
files?: Record<string, string>,
|
|
8
|
+
cwd = "/",
|
|
9
|
+
): ShellContext {
|
|
10
|
+
const vol = new MemoryVolume();
|
|
11
|
+
if (files) {
|
|
12
|
+
for (const [path, content] of Object.entries(files)) {
|
|
13
|
+
const dir = path.substring(0, path.lastIndexOf("/")) || "/";
|
|
14
|
+
if (dir !== "/") vol.mkdirSync(dir, { recursive: true });
|
|
15
|
+
vol.writeFileSync(path, content);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
cwd,
|
|
20
|
+
env: { HOME: "/home/user", PATH: "/usr/bin", PWD: cwd },
|
|
21
|
+
volume: vol,
|
|
22
|
+
} as unknown as ShellContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function run(name: string, args: string[], ctx: ShellContext, stdin?: string) {
|
|
26
|
+
const fn = builtins.get(name);
|
|
27
|
+
if (!fn) throw new Error(`builtin "${name}" not found`);
|
|
28
|
+
return fn(args, ctx, stdin);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("shell builtins", () => {
|
|
32
|
+
describe("echo", () => {
|
|
33
|
+
it("outputs arguments joined by space with trailing newline", async () => {
|
|
34
|
+
const ctx = makeCtx();
|
|
35
|
+
const result = await run("echo", ["hello", "world"], ctx);
|
|
36
|
+
expect(result.stdout).toBe("hello world\n");
|
|
37
|
+
expect(result.exitCode).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("-n suppresses trailing newline", async () => {
|
|
41
|
+
const ctx = makeCtx();
|
|
42
|
+
const result = await run("echo", ["-n", "hello"], ctx);
|
|
43
|
+
expect(result.stdout).toBe("hello");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("with no args outputs just newline", async () => {
|
|
47
|
+
const ctx = makeCtx();
|
|
48
|
+
const result = await run("echo", [], ctx);
|
|
49
|
+
expect(result.stdout).toBe("\n");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("cat", () => {
|
|
54
|
+
it("outputs file contents", async () => {
|
|
55
|
+
const ctx = makeCtx({ "/f.txt": "hello world" });
|
|
56
|
+
const result = await run("cat", ["/f.txt"], ctx);
|
|
57
|
+
expect(result.stdout).toBe("hello world");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("reads from stdin when no files given", async () => {
|
|
61
|
+
const ctx = makeCtx();
|
|
62
|
+
const result = await run("cat", [], ctx, "stdin data");
|
|
63
|
+
expect(result.stdout).toBe("stdin data");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reads multiple files concatenated", async () => {
|
|
67
|
+
const ctx = makeCtx({ "/a.txt": "AAA", "/b.txt": "BBB" });
|
|
68
|
+
const result = await run("cat", ["/a.txt", "/b.txt"], ctx);
|
|
69
|
+
expect(result.stdout).toBe("AAABBB");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("ls", () => {
|
|
74
|
+
it("lists files in directory", async () => {
|
|
75
|
+
const ctx = makeCtx({
|
|
76
|
+
"/project/a.txt": "a",
|
|
77
|
+
"/project/b.txt": "b",
|
|
78
|
+
});
|
|
79
|
+
const result = await run("ls", ["/project"], ctx);
|
|
80
|
+
expect(result.stdout).toContain("a.txt");
|
|
81
|
+
expect(result.stdout).toContain("b.txt");
|
|
82
|
+
expect(result.exitCode).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("pwd", () => {
|
|
87
|
+
it("outputs current directory with newline", async () => {
|
|
88
|
+
const ctx = makeCtx({}, "/home/user");
|
|
89
|
+
const result = await run("pwd", [], ctx);
|
|
90
|
+
expect(result.stdout).toBe("/home/user\n");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("mkdir", () => {
|
|
95
|
+
it("creates directory", async () => {
|
|
96
|
+
const ctx = makeCtx();
|
|
97
|
+
await run("mkdir", ["/newdir"], ctx);
|
|
98
|
+
expect(ctx.volume.statSync("/newdir").isDirectory()).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("-p creates nested directories", async () => {
|
|
102
|
+
const ctx = makeCtx();
|
|
103
|
+
await run("mkdir", ["-p", "/a/b/c"], ctx);
|
|
104
|
+
expect(ctx.volume.statSync("/a/b/c").isDirectory()).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("rm", () => {
|
|
109
|
+
it("removes a file", async () => {
|
|
110
|
+
const ctx = makeCtx({ "/f.txt": "data" });
|
|
111
|
+
await run("rm", ["/f.txt"], ctx);
|
|
112
|
+
expect(ctx.volume.existsSync("/f.txt")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("-r removes directory recursively", async () => {
|
|
116
|
+
const ctx = makeCtx({ "/dir/f.txt": "data" });
|
|
117
|
+
await run("rm", ["-r", "/dir"], ctx);
|
|
118
|
+
expect(ctx.volume.existsSync("/dir")).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("-f ignores nonexistent files", async () => {
|
|
122
|
+
const ctx = makeCtx();
|
|
123
|
+
const result = await run("rm", ["-f", "/nonexistent"], ctx);
|
|
124
|
+
expect(result.exitCode).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("cp", () => {
|
|
129
|
+
it("copies file content", async () => {
|
|
130
|
+
const ctx = makeCtx({ "/src.txt": "content" });
|
|
131
|
+
await run("cp", ["/src.txt", "/dst.txt"], ctx);
|
|
132
|
+
expect(ctx.volume.readFileSync("/dst.txt", "utf8")).toBe("content");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("mv", () => {
|
|
137
|
+
it("moves a file", async () => {
|
|
138
|
+
const ctx = makeCtx({ "/old.txt": "data" });
|
|
139
|
+
await run("mv", ["/old.txt", "/new.txt"], ctx);
|
|
140
|
+
expect(ctx.volume.readFileSync("/new.txt", "utf8")).toBe("data");
|
|
141
|
+
expect(ctx.volume.existsSync("/old.txt")).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("grep", () => {
|
|
146
|
+
// strip ansi colors so we can assert on the actual text
|
|
147
|
+
const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
148
|
+
|
|
149
|
+
it("matches lines containing pattern", async () => {
|
|
150
|
+
const ctx = makeCtx({ "/f.txt": "apple\nbanana\napricot\n" });
|
|
151
|
+
const result = await run("grep", ["ap", "/f.txt"], ctx);
|
|
152
|
+
const plain = strip(result.stdout);
|
|
153
|
+
expect(plain).toContain("apple");
|
|
154
|
+
expect(plain).toContain("apricot");
|
|
155
|
+
expect(plain).not.toContain("banana");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("-i case insensitive matching", async () => {
|
|
159
|
+
const ctx = makeCtx({ "/f.txt": "Hello\nworld\n" });
|
|
160
|
+
const result = await run("grep", ["-i", "hello", "/f.txt"], ctx);
|
|
161
|
+
expect(strip(result.stdout)).toContain("Hello");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("-v inverts match", async () => {
|
|
165
|
+
const ctx = makeCtx({ "/f.txt": "apple\nbanana\napricot\n" });
|
|
166
|
+
const result = await run("grep", ["-v", "ap", "/f.txt"], ctx);
|
|
167
|
+
const plain = strip(result.stdout);
|
|
168
|
+
expect(plain).toContain("banana");
|
|
169
|
+
expect(plain).not.toContain("apple");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns exit code 1 on no match", async () => {
|
|
173
|
+
const ctx = makeCtx({ "/f.txt": "hello\n" });
|
|
174
|
+
const result = await run("grep", ["xyz", "/f.txt"], ctx);
|
|
175
|
+
expect(result.exitCode).toBe(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("reads from stdin when no files", async () => {
|
|
179
|
+
const ctx = makeCtx();
|
|
180
|
+
const result = await run("grep", ["hello"], ctx, "hello world\nfoo\n");
|
|
181
|
+
expect(strip(result.stdout)).toContain("hello");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("head", () => {
|
|
186
|
+
it("outputs first 10 lines by default", async () => {
|
|
187
|
+
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n") + "\n";
|
|
188
|
+
const ctx = makeCtx({ "/f.txt": lines });
|
|
189
|
+
const result = await run("head", ["/f.txt"], ctx);
|
|
190
|
+
const outputLines = result.stdout.trim().split("\n");
|
|
191
|
+
expect(outputLines.length).toBe(10);
|
|
192
|
+
expect(outputLines[0]).toBe("line0");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("-n 5 outputs first 5 lines", async () => {
|
|
196
|
+
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n") + "\n";
|
|
197
|
+
const ctx = makeCtx({ "/f.txt": lines });
|
|
198
|
+
const result = await run("head", ["-n", "5", "/f.txt"], ctx);
|
|
199
|
+
const outputLines = result.stdout.trim().split("\n");
|
|
200
|
+
expect(outputLines.length).toBe(5);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("tail", () => {
|
|
205
|
+
it("outputs last 10 lines by default", async () => {
|
|
206
|
+
const lines = Array.from({ length: 20 }, (_, i) => `line${i}`).join("\n") + "\n";
|
|
207
|
+
const ctx = makeCtx({ "/f.txt": lines });
|
|
208
|
+
const result = await run("tail", ["/f.txt"], ctx);
|
|
209
|
+
const outputLines = result.stdout.trim().split("\n");
|
|
210
|
+
expect(outputLines.length).toBe(10);
|
|
211
|
+
expect(outputLines[outputLines.length - 1]).toBe("line19");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("wc", () => {
|
|
216
|
+
it("-l counts lines", async () => {
|
|
217
|
+
const ctx = makeCtx({ "/f.txt": "one\ntwo\nthree\n" });
|
|
218
|
+
const result = await run("wc", ["-l", "/f.txt"], ctx);
|
|
219
|
+
expect(result.stdout).toContain("3");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("reads from stdin", async () => {
|
|
223
|
+
const ctx = makeCtx();
|
|
224
|
+
const result = await run("wc", ["-l"], ctx, "a\nb\nc\n");
|
|
225
|
+
expect(result.stdout).toContain("3");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("sort", () => {
|
|
230
|
+
it("sorts lines alphabetically", async () => {
|
|
231
|
+
const ctx = makeCtx();
|
|
232
|
+
const result = await run("sort", [], ctx, "cherry\napple\nbanana\n");
|
|
233
|
+
expect(result.stdout.trim()).toBe("apple\nbanana\ncherry");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("-r reverses sort", async () => {
|
|
237
|
+
const ctx = makeCtx();
|
|
238
|
+
const result = await run("sort", ["-r"], ctx, "a\nb\nc\n");
|
|
239
|
+
expect(result.stdout.trim()).toBe("c\nb\na");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("uniq", () => {
|
|
244
|
+
it("removes consecutive duplicate lines", async () => {
|
|
245
|
+
const ctx = makeCtx();
|
|
246
|
+
const result = await run("uniq", [], ctx, "a\na\nb\nb\na\n");
|
|
247
|
+
expect(result.stdout.trim()).toBe("a\nb\na");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("touch", () => {
|
|
252
|
+
it("creates empty file if not exists", async () => {
|
|
253
|
+
const ctx = makeCtx();
|
|
254
|
+
await run("touch", ["/new.txt"], ctx);
|
|
255
|
+
expect(ctx.volume.existsSync("/new.txt")).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("does not modify existing file content", async () => {
|
|
259
|
+
const ctx = makeCtx({ "/f.txt": "data" });
|
|
260
|
+
await run("touch", ["/f.txt"], ctx);
|
|
261
|
+
expect(ctx.volume.readFileSync("/f.txt", "utf8")).toBe("data");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("true / false", () => {
|
|
266
|
+
it("true returns exit code 0", async () => {
|
|
267
|
+
const ctx = makeCtx();
|
|
268
|
+
const result = await run("true", [], ctx);
|
|
269
|
+
expect(result.exitCode).toBe(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("false returns exit code 1", async () => {
|
|
273
|
+
const ctx = makeCtx();
|
|
274
|
+
const result = await run("false", [], ctx);
|
|
275
|
+
expect(result.exitCode).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("which", () => {
|
|
280
|
+
it("finds builtin commands", async () => {
|
|
281
|
+
const ctx = makeCtx();
|
|
282
|
+
const result = await run("which", ["echo"], ctx);
|
|
283
|
+
expect(result.stdout).toContain("echo");
|
|
284
|
+
expect(result.exitCode).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("test / [", () => {
|
|
289
|
+
it("-f returns 0 for existing file", async () => {
|
|
290
|
+
const ctx = makeCtx({ "/f.txt": "data" });
|
|
291
|
+
const result = await run("test", ["-f", "/f.txt"], ctx);
|
|
292
|
+
expect(result.exitCode).toBe(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("-d returns 0 for existing directory", async () => {
|
|
296
|
+
const ctx = makeCtx();
|
|
297
|
+
ctx.volume.mkdirSync("/dir");
|
|
298
|
+
const result = await run("test", ["-d", "/dir"], ctx);
|
|
299
|
+
expect(result.exitCode).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("-e returns 0 for existing path", async () => {
|
|
303
|
+
const ctx = makeCtx({ "/f.txt": "" });
|
|
304
|
+
const result = await run("test", ["-e", "/f.txt"], ctx);
|
|
305
|
+
expect(result.exitCode).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("-z returns 0 for empty string", async () => {
|
|
309
|
+
const ctx = makeCtx();
|
|
310
|
+
const result = await run("test", ["-z", ""], ctx);
|
|
311
|
+
expect(result.exitCode).toBe(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("-n returns 0 for non-empty string", async () => {
|
|
315
|
+
const ctx = makeCtx();
|
|
316
|
+
const result = await run("test", ["-n", "hello"], ctx);
|
|
317
|
+
expect(result.exitCode).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("find", () => {
|
|
322
|
+
it("finds files by name pattern", async () => {
|
|
323
|
+
const ctx = makeCtx({
|
|
324
|
+
"/project/a.txt": "",
|
|
325
|
+
"/project/b.js": "",
|
|
326
|
+
"/project/sub/c.txt": "",
|
|
327
|
+
});
|
|
328
|
+
const result = await run("find", ["/project", "-name", "*.txt"], ctx);
|
|
329
|
+
expect(result.stdout).toContain("a.txt");
|
|
330
|
+
expect(result.stdout).toContain("c.txt");
|
|
331
|
+
expect(result.stdout).not.toContain("b.js");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("-type f finds only files", async () => {
|
|
335
|
+
const ctx = makeCtx({ "/project/f.txt": "" });
|
|
336
|
+
ctx.volume.mkdirSync("/project/dir");
|
|
337
|
+
const result = await run("find", ["/project", "-type", "f"], ctx);
|
|
338
|
+
expect(result.stdout).toContain("f.txt");
|
|
339
|
+
expect(result.stdout).not.toContain("dir");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("-type d finds only directories", async () => {
|
|
343
|
+
const ctx = makeCtx({ "/project/f.txt": "" });
|
|
344
|
+
ctx.volume.mkdirSync("/project/dir");
|
|
345
|
+
const result = await run("find", ["/project", "-type", "d"], ctx);
|
|
346
|
+
expect(result.stdout).toContain("dir");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("export", () => {
|
|
351
|
+
it("sets environment variable", async () => {
|
|
352
|
+
const ctx = makeCtx();
|
|
353
|
+
await run("export", ["FOO=bar"], ctx);
|
|
354
|
+
expect(ctx.env.FOO).toBe("bar");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { NodepodShell } from "../shell/shell-interpreter";
|
|
3
|
+
import { MemoryVolume } from "../memory-volume";
|
|
4
|
+
|
|
5
|
+
function createShell(files?: Record<string, string>, cwd = "/") {
|
|
6
|
+
const vol = new MemoryVolume();
|
|
7
|
+
if (files) {
|
|
8
|
+
for (const [path, content] of Object.entries(files)) {
|
|
9
|
+
const dir = path.substring(0, path.lastIndexOf("/")) || "/";
|
|
10
|
+
if (dir !== "/") vol.mkdirSync(dir, { recursive: true });
|
|
11
|
+
vol.writeFileSync(path, content);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const shell = new NodepodShell(vol, {
|
|
15
|
+
cwd,
|
|
16
|
+
env: { HOME: "/home/user", PATH: "/usr/bin", PWD: cwd },
|
|
17
|
+
});
|
|
18
|
+
return { vol, shell };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("NodepodShell", () => {
|
|
22
|
+
describe("simple command execution", () => {
|
|
23
|
+
it("executes echo and captures stdout", async () => {
|
|
24
|
+
const { shell } = createShell();
|
|
25
|
+
const result = await shell.exec("echo hello");
|
|
26
|
+
expect(result.stdout).toBe("hello\n");
|
|
27
|
+
expect(result.exitCode).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns exit code from builtin", async () => {
|
|
31
|
+
const { shell } = createShell();
|
|
32
|
+
const result = await shell.exec("false");
|
|
33
|
+
expect(result.exitCode).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("pipe chains", () => {
|
|
38
|
+
it("pipes stdout of one command to stdin of next", async () => {
|
|
39
|
+
const { shell } = createShell();
|
|
40
|
+
const result = await shell.exec("echo hello world | wc -w");
|
|
41
|
+
expect(result.stdout.trim()).toBe("2");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("multi-stage pipe: echo | cat | cat", async () => {
|
|
45
|
+
const { shell } = createShell();
|
|
46
|
+
const result = await shell.exec("echo test | cat | cat");
|
|
47
|
+
expect(result.stdout).toBe("test\n");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("AND/OR chains", () => {
|
|
52
|
+
it("&& runs second command only on success", async () => {
|
|
53
|
+
const { shell } = createShell();
|
|
54
|
+
const result = await shell.exec("true && echo yes");
|
|
55
|
+
expect(result.stdout).toBe("yes\n");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("&& skips second command on failure", async () => {
|
|
59
|
+
const { shell } = createShell();
|
|
60
|
+
const result = await shell.exec("false && echo no");
|
|
61
|
+
expect(result.stdout).toBe("");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("|| runs second command only on failure", async () => {
|
|
65
|
+
const { shell } = createShell();
|
|
66
|
+
const result = await shell.exec("false || echo fallback");
|
|
67
|
+
expect(result.stdout).toBe("fallback\n");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("|| skips second command on success", async () => {
|
|
71
|
+
const { shell } = createShell();
|
|
72
|
+
const result = await shell.exec("true || echo no");
|
|
73
|
+
expect(result.stdout).toBe("");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("mixed chains work correctly", async () => {
|
|
77
|
+
const { shell } = createShell();
|
|
78
|
+
const result = await shell.exec("false || true && echo ok");
|
|
79
|
+
expect(result.stdout).toBe("ok\n");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("semicolons", () => {
|
|
84
|
+
it("runs both commands regardless of exit code", async () => {
|
|
85
|
+
const { shell } = createShell();
|
|
86
|
+
const result = await shell.exec("echo a; echo b");
|
|
87
|
+
expect(result.stdout).toBe("a\nb\n");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("redirections", () => {
|
|
92
|
+
it("> writes stdout to file", async () => {
|
|
93
|
+
const { vol, shell } = createShell();
|
|
94
|
+
await shell.exec("echo hello > /out.txt");
|
|
95
|
+
expect(vol.readFileSync("/out.txt", "utf8")).toBe("hello\n");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it(">> appends to file", async () => {
|
|
99
|
+
const { vol, shell } = createShell({ "/out.txt": "first\n" });
|
|
100
|
+
await shell.exec("echo second >> /out.txt");
|
|
101
|
+
expect(vol.readFileSync("/out.txt", "utf8")).toBe("first\nsecond\n");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("< reads stdin from file", async () => {
|
|
105
|
+
const { shell } = createShell({ "/in.txt": "file content" });
|
|
106
|
+
const result = await shell.exec("cat < /in.txt");
|
|
107
|
+
expect(result.stdout).toBe("file content");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("environment variables", () => {
|
|
112
|
+
it("export VAR=value persists in env", async () => {
|
|
113
|
+
const { shell } = createShell();
|
|
114
|
+
await shell.exec("export X=42");
|
|
115
|
+
expect(shell.getEnv().X).toBe("42");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("cd integration", () => {
|
|
120
|
+
it("cd changes shell cwd", async () => {
|
|
121
|
+
const { vol, shell } = createShell();
|
|
122
|
+
vol.mkdirSync("/mydir", { recursive: true });
|
|
123
|
+
await shell.exec("cd /mydir");
|
|
124
|
+
expect(shell.getCwd()).toBe("/mydir");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("pwd reflects cd", async () => {
|
|
128
|
+
const { vol, shell } = createShell();
|
|
129
|
+
vol.mkdirSync("/mydir", { recursive: true });
|
|
130
|
+
await shell.exec("cd /mydir");
|
|
131
|
+
const result = await shell.exec("pwd");
|
|
132
|
+
expect(result.stdout).toBe("/mydir\n");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("compound operations", () => {
|
|
137
|
+
it("mkdir -p + echo redirect + cat", async () => {
|
|
138
|
+
const { vol, shell } = createShell();
|
|
139
|
+
await shell.exec("mkdir -p /tmp/test && echo hello > /tmp/test/f.txt");
|
|
140
|
+
expect(vol.readFileSync("/tmp/test/f.txt", "utf8")).toBe("hello\n");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("glob expansion", () => {
|
|
145
|
+
it("* is expanded to matching files", async () => {
|
|
146
|
+
const { shell } = createShell({
|
|
147
|
+
"/a.txt": "",
|
|
148
|
+
"/b.txt": "",
|
|
149
|
+
"/c.js": "",
|
|
150
|
+
});
|
|
151
|
+
const result = await shell.exec("echo *.txt");
|
|
152
|
+
expect(result.stdout).toContain("a.txt");
|
|
153
|
+
expect(result.stdout).toContain("b.txt");
|
|
154
|
+
expect(result.stdout).not.toContain("c.js");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|