typescript-virtual-container 1.2.7 → 1.2.9
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/README.md +457 -42
- package/dist/SSHMimic/executor.js +3 -5
- package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
- package/dist/VirtualFileSystem/binaryPack.js +193 -0
- package/dist/VirtualFileSystem/index.d.ts +7 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +20 -9
- package/dist/VirtualPackageManager/index.d.ts +202 -0
- package/dist/VirtualPackageManager/index.d.ts.map +1 -0
- package/dist/VirtualPackageManager/index.js +676 -0
- package/dist/VirtualShell/index.d.ts +87 -12
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +83 -12
- package/dist/VirtualUserManager/index.d.ts +52 -20
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +54 -20
- package/dist/commands/alias.d.ts +4 -0
- package/dist/commands/alias.d.ts.map +1 -0
- package/dist/commands/alias.js +58 -0
- package/dist/commands/apt.d.ts +4 -0
- package/dist/commands/apt.d.ts.map +1 -0
- package/dist/commands/apt.js +182 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +27 -8
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +52 -3
- package/dist/commands/command-helpers.d.ts +78 -4
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +78 -4
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +81 -29
- package/dist/commands/dpkg.d.ts +4 -0
- package/dist/commands/dpkg.d.ts.map +1 -0
- package/dist/commands/dpkg.js +144 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +24 -12
- package/dist/commands/free.d.ts +3 -0
- package/dist/commands/free.d.ts.map +1 -0
- package/dist/commands/free.js +38 -0
- package/dist/commands/helpers.d.ts +3 -0
- package/dist/commands/helpers.d.ts.map +1 -1
- package/dist/commands/helpers.js +3 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +21 -0
- package/dist/commands/index.d.ts +8 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +120 -11
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +4 -3
- package/dist/commands/lsb-release.d.ts +3 -0
- package/dist/commands/lsb-release.d.ts.map +1 -0
- package/dist/commands/lsb-release.js +50 -0
- package/dist/commands/man.d.ts +3 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +155 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -2
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +27 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +29 -11
- package/dist/commands/source.d.ts +3 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +31 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +92 -0
- package/dist/commands/type.d.ts +3 -0
- package/dist/commands/type.d.ts.map +1 -0
- package/dist/commands/type.js +34 -0
- package/dist/commands/uptime.d.ts +3 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +40 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +71 -100
- package/dist/commands/which.d.ts +3 -0
- package/dist/commands/which.d.ts.map +1 -0
- package/dist/commands/which.js +32 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/modules/linuxRootfs.d.ts +24 -0
- package/dist/modules/linuxRootfs.d.ts.map +1 -0
- package/dist/modules/linuxRootfs.js +297 -0
- package/dist/modules/neofetch.d.ts.map +1 -1
- package/dist/modules/neofetch.js +1 -0
- package/dist/standalone.js +4 -1
- package/package.json +2 -1
- package/src/SSHMimic/executor.ts +3 -5
- package/src/VirtualFileSystem/binaryPack.ts +219 -0
- package/src/VirtualFileSystem/index.ts +21 -11
- package/src/VirtualPackageManager/index.ts +820 -0
- package/src/VirtualShell/index.ts +104 -13
- package/src/VirtualUserManager/index.ts +55 -20
- package/src/commands/alias.ts +60 -0
- package/src/commands/apt.ts +198 -0
- package/src/commands/cat.ts +32 -8
- package/src/commands/chmod.ts +48 -3
- package/src/commands/command-helpers.ts +78 -4
- package/src/commands/curl.ts +78 -37
- package/src/commands/dpkg.ts +158 -0
- package/src/commands/echo.ts +30 -14
- package/src/commands/free.ts +40 -0
- package/src/commands/helpers.ts +8 -0
- package/src/commands/history.ts +29 -0
- package/src/commands/index.ts +116 -11
- package/src/commands/ls.ts +5 -4
- package/src/commands/lsb-release.ts +52 -0
- package/src/commands/man.ts +166 -0
- package/src/commands/neofetch.ts +5 -0
- package/src/commands/ping.ts +5 -2
- package/src/commands/ps.ts +28 -6
- package/src/commands/sh.ts +33 -11
- package/src/commands/source.ts +35 -0
- package/src/commands/test.ts +100 -0
- package/src/commands/type.ts +40 -0
- package/src/commands/uptime.ts +46 -0
- package/src/commands/wget.ts +70 -123
- package/src/commands/which.ts +34 -0
- package/src/index.ts +10 -0
- package/src/modules/linuxRootfs.ts +439 -0
- package/src/modules/neofetch.ts +1 -0
- package/src/standalone.ts +4 -1
- package/standalone.js +418 -103
- package/standalone.js.map +4 -4
- package/tests/new-features.test.ts +626 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { VirtualShell } from "../src";
|
|
3
|
+
import { SshClient } from "../src/SSHClient";
|
|
4
|
+
|
|
5
|
+
// ─── shared shell ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
let shell: VirtualShell;
|
|
8
|
+
let client: InstanceType<typeof SshClient>;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
shell = new VirtualShell("test-vm");
|
|
12
|
+
await shell.ensureInitialized();
|
|
13
|
+
client = new SshClient(shell, "root");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ─── Phase 1: Linux rootfs ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("Linux rootfs", () => {
|
|
19
|
+
test("/etc/os-release exists and has correct distro", async () => {
|
|
20
|
+
const r = await client.cat("/etc/os-release");
|
|
21
|
+
expect(r.exitCode).toBe(0);
|
|
22
|
+
expect(r.stdout).toContain("Fortune GNU/Linux");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("/etc/hostname exists", async () => {
|
|
26
|
+
const r = await client.cat("/etc/hostname");
|
|
27
|
+
expect(r.exitCode).toBe(0);
|
|
28
|
+
expect(r.stdout?.trim()).toBe("test-vm");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("/etc/hosts has localhost", async () => {
|
|
32
|
+
const r = await client.cat("/etc/hosts");
|
|
33
|
+
expect(r.stdout).toContain("127.0.0.1");
|
|
34
|
+
expect(r.stdout).toContain("localhost");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("/proc/meminfo is populated", async () => {
|
|
38
|
+
const r = await client.cat("/proc/meminfo");
|
|
39
|
+
expect(r.exitCode).toBe(0);
|
|
40
|
+
expect(r.stdout).toContain("MemTotal:");
|
|
41
|
+
expect(r.stdout).toContain("MemFree:");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("/proc/cpuinfo is populated", async () => {
|
|
45
|
+
const r = await client.cat("/proc/cpuinfo");
|
|
46
|
+
expect(r.exitCode).toBe(0);
|
|
47
|
+
expect(r.stdout).toContain("processor");
|
|
48
|
+
expect(r.stdout).toContain("model name");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("/proc/version is populated", async () => {
|
|
52
|
+
const r = await client.cat("/proc/version");
|
|
53
|
+
expect(r.exitCode).toBe(0);
|
|
54
|
+
expect(r.stdout).toContain("Linux version");
|
|
55
|
+
expect(r.stdout).toContain("1.0.0+itsrealfortune");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("/sys/devices/virtual/dmi/id/sys_vendor exists", async () => {
|
|
59
|
+
const r = await client.cat("/sys/devices/virtual/dmi/id/sys_vendor");
|
|
60
|
+
expect(r.exitCode).toBe(0);
|
|
61
|
+
expect(r.stdout?.trim()).toBe("Fortune Systems");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("/var/lib/dpkg/status is created", () => {
|
|
65
|
+
expect(shell.vfs.exists("/var/lib/dpkg/status")).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("/bin is symlink to /usr/bin", () => {
|
|
69
|
+
expect(shell.vfs.isSymlink("/bin")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("/tmp has sticky bit", () => {
|
|
73
|
+
const stat = shell.vfs.stat("/tmp");
|
|
74
|
+
expect(stat.type).toBe("directory");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("/etc/passwd contains root", async () => {
|
|
78
|
+
const r = await client.cat("/etc/passwd");
|
|
79
|
+
expect(r.stdout).toContain("root:x:0:0");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("/usr/bin stubs for builtins exist", () => {
|
|
83
|
+
expect(shell.vfs.exists("/usr/bin/ls")).toBe(true);
|
|
84
|
+
expect(shell.vfs.exists("/usr/bin/grep")).toBe(true);
|
|
85
|
+
expect(shell.vfs.exists("/usr/bin/curl")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("lsb_release -a returns Fortune distro info", async () => {
|
|
89
|
+
const r = await client.exec("lsb_release -a");
|
|
90
|
+
expect(r.exitCode).toBe(0);
|
|
91
|
+
expect(r.stdout).toContain("Fortune");
|
|
92
|
+
expect(r.stdout).toContain("Distributor ID");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("uname -a shows kernel from properties", async () => {
|
|
96
|
+
const r = await client.exec("uname -a");
|
|
97
|
+
expect(r.exitCode).toBe(0);
|
|
98
|
+
expect(r.stdout).toContain("1.0.0+itsrealfortune+1-amd64");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─── Phase 2: apt / dpkg ──────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe("Package manager (apt/dpkg)", () => {
|
|
105
|
+
test("apt list shows available packages", async () => {
|
|
106
|
+
const r = await client.exec("apt list");
|
|
107
|
+
expect(r.exitCode).toBe(0);
|
|
108
|
+
expect(r.stdout).toContain("vim");
|
|
109
|
+
expect(r.stdout).toContain("git");
|
|
110
|
+
expect(r.stdout).toContain("python3");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("apt install vim installs package and writes files", async () => {
|
|
114
|
+
const r = await client.exec("apt install vim");
|
|
115
|
+
expect(r.exitCode).toBe(0);
|
|
116
|
+
expect(r.stdout).toContain("Setting up vim");
|
|
117
|
+
expect(shell.vfs.exists("/usr/bin/vim")).toBe(true);
|
|
118
|
+
expect(shell.vfs.exists("/usr/bin/vi")).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("apt list --installed shows vim after install", async () => {
|
|
122
|
+
const r = await client.exec("apt list --installed");
|
|
123
|
+
expect(r.stdout).toContain("vim");
|
|
124
|
+
expect(r.stdout).toContain("[installed]");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("dpkg -l shows installed packages", async () => {
|
|
128
|
+
const r = await client.exec("dpkg -l");
|
|
129
|
+
expect(r.exitCode).toBe(0);
|
|
130
|
+
expect(r.stdout).toContain("vim");
|
|
131
|
+
expect(r.stdout).toContain("ii");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("dpkg -s vim shows package status", async () => {
|
|
135
|
+
await client.exec("apt install vim"); // ensure installed
|
|
136
|
+
const r = await client.exec("dpkg -s vim");
|
|
137
|
+
expect(r.exitCode).toBe(0);
|
|
138
|
+
expect(r.stdout).toContain("Package: vim");
|
|
139
|
+
expect(r.stdout).toContain("Status: install ok installed");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("dpkg -L vim lists installed files", async () => {
|
|
143
|
+
await client.exec("apt install vim"); // ensure installed
|
|
144
|
+
const r = await client.exec("dpkg -L vim");
|
|
145
|
+
expect(r.exitCode).toBe(0);
|
|
146
|
+
expect(r.stdout).toContain("/usr/bin/vim");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("apt install resolves dependencies", async () => {
|
|
150
|
+
// npm depends on nodejs
|
|
151
|
+
const r = await client.exec("apt install npm");
|
|
152
|
+
expect(r.exitCode).toBe(0);
|
|
153
|
+
// nodejs should be auto-installed
|
|
154
|
+
expect(shell.vfs.exists("/usr/bin/node")).toBe(true);
|
|
155
|
+
expect(shell.vfs.exists("/usr/bin/npm")).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("apt install non-existent package fails", async () => {
|
|
159
|
+
const r = await client.exec("apt install fakepackage999");
|
|
160
|
+
expect(r.exitCode).not.toBe(0);
|
|
161
|
+
expect(r.stdout).toContain("Unable to locate package");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("apt remove vim removes package", async () => {
|
|
165
|
+
await client.exec("apt install vim");
|
|
166
|
+
const r = await client.exec("apt remove vim");
|
|
167
|
+
expect(r.exitCode).toBe(0);
|
|
168
|
+
expect(r.stdout).toContain("Removing vim");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("apt search finds packages by term", async () => {
|
|
172
|
+
const r = await client.exec("apt search editor");
|
|
173
|
+
expect(r.exitCode).toBe(0);
|
|
174
|
+
expect(r.stdout).toContain("vim");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("apt show displays package metadata", async () => {
|
|
178
|
+
const r = await client.exec("apt show git");
|
|
179
|
+
expect(r.exitCode).toBe(0);
|
|
180
|
+
expect(r.stdout).toContain("Package: git");
|
|
181
|
+
expect(r.stdout).toContain("Version:");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("apt-cache search works", async () => {
|
|
185
|
+
const r = await client.exec("apt-cache search python");
|
|
186
|
+
expect(r.exitCode).toBe(0);
|
|
187
|
+
expect(r.stdout).toContain("python3");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("apt-cache show works", async () => {
|
|
191
|
+
const r = await client.exec("apt-cache show curl");
|
|
192
|
+
expect(r.exitCode).toBe(0);
|
|
193
|
+
expect(r.stdout).toContain("Package: curl");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("apt-cache policy works", async () => {
|
|
197
|
+
const r = await client.exec("apt-cache policy nodejs");
|
|
198
|
+
expect(r.exitCode).toBe(0);
|
|
199
|
+
expect(r.stdout).toContain("Candidate:");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("dpkg-query -W lists packages", async () => {
|
|
203
|
+
await client.exec("apt install git"); // ensure git installed
|
|
204
|
+
// dpkg-query -W shows tab-separated name\tversion for all installed
|
|
205
|
+
const r = await client.exec("dpkg-query -W git");
|
|
206
|
+
expect(r.exitCode).toBe(0);
|
|
207
|
+
expect(r.stdout).toContain("git");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("apt update simulates package index refresh", async () => {
|
|
211
|
+
const r = await client.exec("apt update");
|
|
212
|
+
expect(r.exitCode).toBe(0);
|
|
213
|
+
expect(r.stdout).toContain("Reading package lists");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("non-root apt install is blocked", async () => {
|
|
217
|
+
// adduser alice
|
|
218
|
+
await shell.users.addUser("alice", "pass");
|
|
219
|
+
const aliceClient = new SshClient(shell, "alice");
|
|
220
|
+
const r = await aliceClient.exec("apt install vim");
|
|
221
|
+
expect(r.exitCode).not.toBe(0);
|
|
222
|
+
expect(r.stderr).toContain("Permission denied");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("neofetch shows package count after installs", async () => {
|
|
226
|
+
await client.exec("apt install curl wget htop");
|
|
227
|
+
const r = await client.exec("neofetch");
|
|
228
|
+
expect(r.exitCode).toBe(0);
|
|
229
|
+
expect(r.stdout).toContain("(dpkg)");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ─── Phase 3: pure fetch curl/wget ───────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
describe("curl / wget (pure fetch)", () => {
|
|
236
|
+
test("curl --help works without host binary", async () => {
|
|
237
|
+
const r = await client.exec("curl --help");
|
|
238
|
+
expect(r.exitCode).toBe(0);
|
|
239
|
+
expect(r.stdout).toContain("Usage: curl");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("wget --help works without host binary", async () => {
|
|
243
|
+
const r = await client.exec("wget --help");
|
|
244
|
+
expect(r.exitCode).toBe(0);
|
|
245
|
+
expect(r.stdout).toContain("Usage: wget");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("wget --version works", async () => {
|
|
249
|
+
const r = await client.exec("wget --version");
|
|
250
|
+
expect(r.exitCode).toBe(0);
|
|
251
|
+
expect(r.stdout).toContain("GNU Wget");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("curl fetches real URL and returns body", async () => {
|
|
255
|
+
const r = await client.exec("curl https://httpbin.org/get");
|
|
256
|
+
// In sandboxed env network may be blocked — accept 0 (ok), 6 (dns), 22 (http err), or 1 (fetch error)
|
|
257
|
+
expect([0, 1, 6, 22]).toContain(r.exitCode);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("curl -o saves to VFS", async () => {
|
|
261
|
+
try {
|
|
262
|
+
await client.exec("curl -o /tmp/test-curl.txt https://httpbin.org/get");
|
|
263
|
+
} catch {}
|
|
264
|
+
// Just check no ENOENT crash; file may or may not exist depending on network
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ─── Phase 4: /proc VFS ───────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe("/proc filesystem", () => {
|
|
271
|
+
test("cat /proc/uptime returns numeric uptime", async () => {
|
|
272
|
+
const r = await client.cat("/proc/uptime");
|
|
273
|
+
expect(r.exitCode).toBe(0);
|
|
274
|
+
const parts = r.stdout?.trim().split(" ");
|
|
275
|
+
expect(parts?.length).toBeGreaterThanOrEqual(1);
|
|
276
|
+
expect(Number(parts?.[0])).toBeGreaterThanOrEqual(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("cat /proc/loadavg returns values", async () => {
|
|
280
|
+
const r = await client.cat("/proc/loadavg");
|
|
281
|
+
expect(r.exitCode).toBe(0);
|
|
282
|
+
expect(r.stdout).toMatch(/\d+\.\d+/);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("cat /proc/net/dev has eth0", async () => {
|
|
286
|
+
const r = await client.cat("/proc/net/dev");
|
|
287
|
+
expect(r.exitCode).toBe(0);
|
|
288
|
+
expect(r.stdout).toContain("eth0");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("refreshProcFs updates /proc/uptime", async () => {
|
|
292
|
+
// const before = shell.vfs.readFile("/proc/uptime");
|
|
293
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
294
|
+
shell.refreshProcFs();
|
|
295
|
+
const after = shell.vfs.readFile("/proc/uptime");
|
|
296
|
+
// Uptime value is based on Date.now(), content is same format
|
|
297
|
+
expect(after).toContain(".00");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ─── Extras: which, type, man, uptime, free, alias, $() ──────────────────────
|
|
302
|
+
|
|
303
|
+
describe("Extra commands", () => {
|
|
304
|
+
test("which ls finds /usr/bin/ls", async () => {
|
|
305
|
+
const r = await client.exec("which ls");
|
|
306
|
+
expect(r.exitCode).toBe(0);
|
|
307
|
+
expect(r.stdout?.trim()).toBe("/usr/bin/ls");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("which nonexistent returns exit 1", async () => {
|
|
311
|
+
const r = await client.exec("which thisdoesnotexist");
|
|
312
|
+
expect(r.exitCode).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("type ls reports builtin", async () => {
|
|
316
|
+
const r = await client.exec("type ls");
|
|
317
|
+
expect(r.exitCode).toBe(0);
|
|
318
|
+
expect(r.stdout).toContain("ls");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("man ls shows manual page", async () => {
|
|
322
|
+
const r = await client.exec("man ls");
|
|
323
|
+
expect(r.exitCode).toBe(0);
|
|
324
|
+
expect(r.stdout).toContain("NAME");
|
|
325
|
+
expect(r.stdout).toContain("list directory");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("man nonexistent returns error", async () => {
|
|
329
|
+
const r = await client.exec("man fakecmd999");
|
|
330
|
+
expect(r.exitCode).not.toBe(0);
|
|
331
|
+
expect(r.stderr).toContain("No manual entry");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("uptime returns formatted string", async () => {
|
|
335
|
+
const r = await client.exec("uptime");
|
|
336
|
+
expect(r.exitCode).toBe(0);
|
|
337
|
+
expect(r.stdout).toMatch(/up/);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("uptime -p returns pretty format", async () => {
|
|
341
|
+
const r = await client.exec("uptime -p");
|
|
342
|
+
expect(r.exitCode).toBe(0);
|
|
343
|
+
expect(r.stdout).toContain("up");
|
|
344
|
+
expect(r.stdout).toMatch(/minute|hour|day/);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("free shows memory table", async () => {
|
|
348
|
+
const r = await client.exec("free");
|
|
349
|
+
expect(r.exitCode).toBe(0);
|
|
350
|
+
expect(r.stdout).toContain("Mem:");
|
|
351
|
+
expect(r.stdout).toContain("Swap:");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("free -h shows human readable", async () => {
|
|
355
|
+
const r = await client.exec("free -h");
|
|
356
|
+
expect(r.exitCode).toBe(0);
|
|
357
|
+
expect(r.stdout).toMatch(/[0-9.]+[GMK]/);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("alias sets and retrieves alias", async () => {
|
|
361
|
+
// alias env is per-session; use sh -c to test in one exec
|
|
362
|
+
const r = await client.exec("alias ll='ls -la'; alias ll");
|
|
363
|
+
expect(r.exitCode).toBe(0);
|
|
364
|
+
// alias output may be in stdout
|
|
365
|
+
const out = (r.stdout ?? "") + (r.stderr ?? "");
|
|
366
|
+
expect(out).toContain("ll");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("alias -a lists all aliases", async () => {
|
|
370
|
+
await client.exec("alias hello='echo hello'");
|
|
371
|
+
const r = await client.exec("alias");
|
|
372
|
+
expect(r.exitCode).toBe(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("unalias removes an alias", async () => {
|
|
376
|
+
await client.exec("alias myfoo='echo foo'");
|
|
377
|
+
const r = await client.exec("unalias myfoo");
|
|
378
|
+
expect(r.exitCode).toBe(0);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("lsb_release -a returns distro info", async () => {
|
|
382
|
+
const r = await client.exec("lsb_release -a");
|
|
383
|
+
expect(r.stdout).toContain("Fortune");
|
|
384
|
+
expect(r.stdout).toContain("aurora");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("lsb_release -d returns description", async () => {
|
|
388
|
+
const r = await client.exec("lsb_release -d");
|
|
389
|
+
expect(r.stdout).toContain("Description:");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ─── $(cmd) substitution ──────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
describe("Command substitution $(cmd)", () => {
|
|
396
|
+
test("echo $(echo hello) works", async () => {
|
|
397
|
+
const r = await client.exec("echo $(echo hello)");
|
|
398
|
+
expect(r.exitCode).toBe(0);
|
|
399
|
+
expect(r.stdout?.trim()).toBe("hello");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("echo $(hostname) returns hostname", async () => {
|
|
403
|
+
const r = await client.exec("echo $(hostname)");
|
|
404
|
+
expect(r.exitCode).toBe(0);
|
|
405
|
+
expect(r.stdout?.trim()).toBe("test-vm");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("echo $(whoami) returns user", async () => {
|
|
409
|
+
const r = await client.exec("echo $(whoami)");
|
|
410
|
+
expect(r.exitCode).toBe(0);
|
|
411
|
+
expect(r.stdout?.trim()).toBe("root");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("nested substitution in variable context", async () => {
|
|
415
|
+
const r = await client.exec("echo user=$(whoami)");
|
|
416
|
+
expect(r.stdout?.trim()).toBe("user=root");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ─── syncPasswd ───────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
describe("syncPasswd", () => {
|
|
423
|
+
test("syncPasswd after addUser updates /etc/passwd", async () => {
|
|
424
|
+
await shell.users.addUser("bob", "pass123");
|
|
425
|
+
shell.syncPasswd();
|
|
426
|
+
const r = await client.cat("/etc/passwd");
|
|
427
|
+
expect(r.stdout).toContain("bob");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("/etc/group has sudo group with sudoers", async () => {
|
|
431
|
+
const r = await client.cat("/etc/group");
|
|
432
|
+
expect(r.stdout).toContain("sudo:");
|
|
433
|
+
expect(r.stdout).toContain("root");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("/etc/shadow exists with restricted permissions", () => {
|
|
437
|
+
expect(shell.vfs.exists("/etc/shadow")).toBe(true);
|
|
438
|
+
const stat = shell.vfs.stat("/etc/shadow");
|
|
439
|
+
expect(stat.type).toBe("file");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ─── Bug fixes ────────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
describe("Bug fixes", () => {
|
|
446
|
+
let shell2: VirtualShell;
|
|
447
|
+
let c: InstanceType<typeof SshClient>;
|
|
448
|
+
|
|
449
|
+
beforeAll(async () => {
|
|
450
|
+
shell2 = new VirtualShell("fix-vm");
|
|
451
|
+
await shell2.ensureInitialized();
|
|
452
|
+
c = new SshClient(shell2, "root");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// echo
|
|
456
|
+
test("echo adds newline — >> append works", async () => {
|
|
457
|
+
await c.exec("echo line1 > /tmp/append.txt");
|
|
458
|
+
await c.exec("echo line2 >> /tmp/append.txt");
|
|
459
|
+
const content = shell2.vfs.readFile("/tmp/append.txt");
|
|
460
|
+
expect(content).toBe("line1\nline2\n");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("echo -e interprets escape sequences", async () => {
|
|
464
|
+
const r = await c.exec("echo -e 'a\\tb'");
|
|
465
|
+
expect(r.stdout?.trim()).toBe("a\tb");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("echo -n suppresses trailing newline", async () => {
|
|
469
|
+
const r = await c.exec("echo -n hello");
|
|
470
|
+
expect(r.stdout).toBe("hello");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("echo uses session env.vars for $VAR", async () => {
|
|
474
|
+
const r = await c.exec("export MYVAR=world && echo $MYVAR");
|
|
475
|
+
expect(r.stdout?.trim()).toBe("world");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ls -a
|
|
479
|
+
test("ls -a shows dotfiles", async () => {
|
|
480
|
+
await c.exec("touch /tmp/.hidden && touch /tmp/visible");
|
|
481
|
+
const normal = await c.exec("ls /tmp");
|
|
482
|
+
const all = await c.exec("ls -a /tmp");
|
|
483
|
+
expect(normal.stdout).not.toContain(".hidden");
|
|
484
|
+
expect(all.stdout).toContain(".hidden");
|
|
485
|
+
expect(all.stdout).toContain("visible");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// chmod symbolic
|
|
489
|
+
test("chmod +x adds execute bit", async () => {
|
|
490
|
+
await c.exec("touch /tmp/script.sh");
|
|
491
|
+
const before = shell2.vfs.stat("/tmp/script.sh").mode;
|
|
492
|
+
await c.exec("chmod +x /tmp/script.sh");
|
|
493
|
+
const after = shell2.vfs.stat("/tmp/script.sh").mode;
|
|
494
|
+
expect(after & 0o111).toBeGreaterThan(0);
|
|
495
|
+
expect(before & 0o111).toBe(0);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("chmod u+x adds only user execute bit", async () => {
|
|
499
|
+
await c.exec("touch /tmp/u.sh && chmod 644 /tmp/u.sh");
|
|
500
|
+
await c.exec("chmod u+x /tmp/u.sh");
|
|
501
|
+
const mode = shell2.vfs.stat("/tmp/u.sh").mode;
|
|
502
|
+
expect(mode & 0o100).toBe(0o100); // user x set
|
|
503
|
+
expect(mode & 0o010).toBe(0); // group x not set
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("chmod go-r removes group+other read", async () => {
|
|
507
|
+
await c.exec("touch /tmp/priv.sh && chmod 755 /tmp/priv.sh");
|
|
508
|
+
await c.exec("chmod go-r /tmp/priv.sh");
|
|
509
|
+
const mode = shell2.vfs.stat("/tmp/priv.sh").mode;
|
|
510
|
+
expect(mode & 0o044).toBe(0);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// cat -n
|
|
514
|
+
test("cat -n numbers lines", async () => {
|
|
515
|
+
await c.exec("echo -e 'foo\\nbar' > /tmp/cattest.txt");
|
|
516
|
+
const r = await c.exec("cat -n /tmp/cattest.txt");
|
|
517
|
+
expect(r.stdout).toContain("1\t");
|
|
518
|
+
expect(r.stdout).toContain("2\t");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// ping -c
|
|
522
|
+
test("ping -c 2 sends exactly 2 packets", async () => {
|
|
523
|
+
const r = await c.exec("ping -c 2 localhost");
|
|
524
|
+
const dataLines = r.stdout?.split("\n").filter((l) => l.includes("icmp_seq="));
|
|
525
|
+
expect(dataLines?.length).toBe(2);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// test / [ command
|
|
529
|
+
test("[ -f path ] returns 0 for existing file", async () => {
|
|
530
|
+
await c.exec("touch /tmp/testfile");
|
|
531
|
+
const r = await c.exec("[ -f /tmp/testfile ] && echo yes || echo no");
|
|
532
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("[ -d path ] returns 0 for existing directory", async () => {
|
|
536
|
+
const r = await c.exec("[ -d /etc ] && echo yes || echo no");
|
|
537
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("[ -f path ] returns 1 for non-existent file", async () => {
|
|
541
|
+
const r = await c.exec("[ -f /tmp/doesnotexist999 ] && echo yes || echo no");
|
|
542
|
+
expect(r.stdout?.trim()).toBe("no");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("[ -e path ] returns 0 for existing path", async () => {
|
|
546
|
+
const r = await c.exec("[ -e /etc/hostname ] && echo yes || echo no");
|
|
547
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("test string comparison = works", async () => {
|
|
551
|
+
const r = await c.exec("[ hello = hello ] && echo yes || echo no");
|
|
552
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("test numeric -eq works", async () => {
|
|
556
|
+
const r = await c.exec("[ 5 -eq 5 ] && echo yes || echo no");
|
|
557
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("test numeric -lt works", async () => {
|
|
561
|
+
const r = await c.exec("[ 3 -lt 5 ] && echo yes || echo no");
|
|
562
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("test -z empty string", async () => {
|
|
566
|
+
const r = await c.exec("[ -z '' ] && echo yes || echo no");
|
|
567
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("test -n non-empty string", async () => {
|
|
571
|
+
const r = await c.exec("[ -n hello ] && echo yes || echo no");
|
|
572
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// source / .
|
|
576
|
+
test("source executes file in current env", async () => {
|
|
577
|
+
shell2.vfs.writeFile("/tmp/setup.sh", "export SOURCED=yes\n");
|
|
578
|
+
const r = await c.exec("source /tmp/setup.sh && echo $SOURCED");
|
|
579
|
+
expect(r.stdout?.trim()).toBe("yes");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test(". (dot) is alias for source", async () => {
|
|
583
|
+
shell2.vfs.writeFile("/tmp/dot.sh", "export DOTTED=ok\n");
|
|
584
|
+
const r = await c.exec(". /tmp/dot.sh && echo $DOTTED");
|
|
585
|
+
expect(r.stdout?.trim()).toBe("ok");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// sh -c with $(cmd)
|
|
589
|
+
test("sh -c handles $(cmd) substitution", async () => {
|
|
590
|
+
const r = await c.exec("sh -c 'echo user=$(whoami)'");
|
|
591
|
+
expect(r.stdout?.trim()).toBe("user=root");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("sh -c for loop with $(cmd)", async () => {
|
|
595
|
+
const r = await c.exec("sh -c 'for x in a b; do echo $(echo $x); done'");
|
|
596
|
+
expect(r.stdout?.trim()).toBe("a\nb");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// history
|
|
600
|
+
test("history command returns command list", async () => {
|
|
601
|
+
// history reads from VFS .bash_history (written by interactive shell)
|
|
602
|
+
// in non-interactive context it may be empty — just check it doesn't crash
|
|
603
|
+
const r = await c.exec("history");
|
|
604
|
+
expect(r.exitCode).toBe(0);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// ps -u
|
|
608
|
+
test("ps -u shows USER column", async () => {
|
|
609
|
+
const r = await c.exec("ps -u");
|
|
610
|
+
expect(r.stdout).toContain("USER");
|
|
611
|
+
expect(r.stdout).toContain("PID");
|
|
612
|
+
expect(r.stdout).toContain("%CPU");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("ps aux shows extended format", async () => {
|
|
616
|
+
const r = await c.exec("ps aux");
|
|
617
|
+
expect(r.stdout).toContain("USER");
|
|
618
|
+
expect(r.exitCode).toBe(0);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// wc -l with pipe (after echo -e fix)
|
|
622
|
+
test("wc -l counts newlines correctly via pipe", async () => {
|
|
623
|
+
const r = await c.exec("echo -e 'a\\nb\\nc' | wc -l");
|
|
624
|
+
expect(r.stdout?.trim()).toBe("3");
|
|
625
|
+
});
|
|
626
|
+
});
|