typescript-virtual-container 1.5.3 → 1.5.4
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 +43 -23
- package/dist/.tsbuildinfo +1 -0
- package/dist/SSHMimic/executor.js +23 -5
- package/dist/VirtualPackageManager/index.js +10 -0
- package/dist/commands/basename.d.ts +13 -0
- package/dist/commands/basename.js +45 -0
- package/dist/commands/file.d.ts +8 -0
- package/dist/commands/file.js +57 -0
- package/dist/commands/fun.d.ts +32 -0
- package/dist/commands/fun.js +172 -0
- package/dist/commands/ifconfig.d.ts +7 -0
- package/dist/commands/ifconfig.js +52 -0
- package/dist/commands/last.d.ts +13 -0
- package/dist/commands/last.js +68 -0
- package/dist/commands/manuals-bundle.js +598 -6
- package/dist/commands/registry.js +24 -2
- package/dist/commands/runtime.js +22 -2
- package/dist/commands/sh.js +5 -0
- package/dist/commands/tput.d.ts +13 -0
- package/dist/commands/tput.js +76 -0
- package/dist/commands/w.d.ts +7 -0
- package/dist/commands/w.js +38 -0
- package/dist/utils/expand.d.ts +12 -0
- package/dist/utils/expand.js +84 -0
- package/package.json +9 -3
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -31
- package/.github/dependabot.yml +0 -27
- package/.github/pull_request_template.md +0 -21
- package/.github/workflows/create-pull-request.yml +0 -85
- package/.github/workflows/publish.yml +0 -25
- package/.github/workflows/test-battery.yml +0 -102
- package/.vscode/settings.json +0 -20
- package/CODE_OF_CONDUCT.md +0 -39
- package/CONTRIBUTING.md +0 -59
- package/HONEYPOT.md +0 -358
- package/SECURITY.md +0 -33
- package/benchmark-results.txt +0 -40
- package/benchmark-virtualshell.ts +0 -88
- package/biome.json +0 -37
- package/build.js +0 -22
- package/builds/fortune-nyx-v1.5.3-directbash-k6.1.0.mjs +0 -1764
- package/builds/fortune-nyx-v1.5.3-ssh-nosftp.js +0 -1764
- package/builds/fortune-nyx-v1.5.3-ssh.cjs +0 -1765
- package/builds/fortune-nyx-v1.5.3-web.min.js +0 -17036
- package/bun.lock +0 -244
- package/docs/.nojekyll +0 -1
- package/docs/app.js +0 -1751
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -162
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/HoneyPot.html +0 -31
- package/docs/classes/IdleManager.html +0 -162
- package/docs/classes/SshClient.html +0 -66
- package/docs/classes/VirtualFileSystem.html +0 -279
- package/docs/classes/VirtualPackageManager.html +0 -63
- package/docs/classes/VirtualSftpServer.html +0 -169
- package/docs/classes/VirtualShell.html +0 -285
- package/docs/classes/VirtualSshServer.html +0 -182
- package/docs/classes/VirtualUserManager.html +0 -276
- package/docs/demo.html +0 -82
- package/docs/functions/assertDiff.html +0 -6
- package/docs/functions/diffSnapshots.html +0 -7
- package/docs/functions/formatDiff.html +0 -6
- package/docs/functions/getArg.html +0 -13
- package/docs/functions/getFlag.html +0 -15
- package/docs/functions/ifFlag.html +0 -11
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -1869
- package/docs/interfaces/AuditLogEntry.html +0 -6
- package/docs/interfaces/CommandContext.html +0 -22
- package/docs/interfaces/CommandResult.html +0 -26
- package/docs/interfaces/ExecStream.html +0 -11
- package/docs/interfaces/HoneyPotStats.html +0 -16
- package/docs/interfaces/IdleManagerOptions.html +0 -7
- package/docs/interfaces/InstalledPackage.html +0 -20
- package/docs/interfaces/NanoEditorSession.html +0 -8
- package/docs/interfaces/PackageDefinition.html +0 -30
- package/docs/interfaces/PackageFile.html +0 -8
- package/docs/interfaces/PasswordChallenge.html +0 -16
- package/docs/interfaces/RemoveOptions.html +0 -4
- package/docs/interfaces/ShellEnv.html +0 -6
- package/docs/interfaces/ShellModule.html +0 -14
- package/docs/interfaces/ShellProperties.html +0 -14
- package/docs/interfaces/ShellStream.html +0 -11
- package/docs/interfaces/SudoChallenge.html +0 -24
- package/docs/interfaces/VfsBaseNode.html +0 -12
- package/docs/interfaces/VfsDiff.html +0 -10
- package/docs/interfaces/VfsDiffEntry.html +0 -6
- package/docs/interfaces/VfsDiffModified.html +0 -10
- package/docs/interfaces/VfsDirectoryNode.html +0 -15
- package/docs/interfaces/VfsFileNode.html +0 -17
- package/docs/interfaces/VfsOptions.html +0 -26
- package/docs/interfaces/VfsSnapshot.html +0 -3
- package/docs/interfaces/VfsSnapshotBaseNode.html +0 -8
- package/docs/interfaces/VfsSnapshotDirectoryNode.html +0 -10
- package/docs/interfaces/VfsSnapshotFileNode.html +0 -12
- package/docs/interfaces/VirtualActiveSession.html +0 -12
- package/docs/interfaces/VirtualSftpServerOptions.html +0 -7
- package/docs/interfaces/VirtualShellVfsLike.html +0 -15
- package/docs/interfaces/VirtualShellVfsOptions.html +0 -3
- package/docs/interfaces/WriteFileOptions.html +0 -6
- package/docs/media/LICENSE +0 -21
- package/docs/modules.html +0 -1
- package/docs/types/ArgParseOptions.html +0 -4
- package/docs/types/CommandMode.html +0 -2
- package/docs/types/CommandOutcome.html +0 -2
- package/docs/types/IdleState.html +0 -1
- package/docs/types/VfsNodeStats.html +0 -2
- package/docs/types/VfsNodeType.html +0 -2
- package/docs/types/VfsPersistenceMode.html +0 -5
- package/docs/types/VfsSnapshotNode.html +0 -2
- package/examples/README.md +0 -288
- package/examples/app.js +0 -1751
- package/examples/app.ts +0 -299
- package/examples/build.js +0 -27
- package/examples/demo.html +0 -33
- package/examples/honeypot-audit.ts +0 -180
- package/examples/honeypot-export.ts +0 -253
- package/examples/honeypot-quickstart.ts +0 -110
- package/examples/index.html +0 -82
- package/examples/server.js +0 -55
- package/polyfills/buffer.js +0 -117
- package/polyfills/node_child_process/index.js +0 -2
- package/polyfills/node_crypto/index.js +0 -167
- package/polyfills/node_events/index.js +0 -9
- package/polyfills/node_fs/index.js +0 -202
- package/polyfills/node_fs/promises.js +0 -4
- package/polyfills/node_os/index.js +0 -9
- package/polyfills/node_path/index.js +0 -28
- package/polyfills/node_vm/index.js +0 -7
- package/polyfills/node_zlib/index.js +0 -3
- package/polyfills/process.js +0 -14
- package/polyfills/ssh2/index.js +0 -75
- package/scripts/build-all.mjs +0 -226
- package/scripts/build-names.mjs +0 -43
- package/scripts/generate-manuals-bundle.mjs +0 -49
- package/scripts/postinstall.js +0 -42
- package/scripts/publish-package.sh +0 -70
- package/src/Honeypot/index.ts +0 -457
- package/src/SSHClient/index.ts +0 -270
- package/src/SSHMimic/exec.ts +0 -49
- package/src/SSHMimic/executor.ts +0 -251
- package/src/SSHMimic/hostKey.ts +0 -21
- package/src/SSHMimic/index.ts +0 -337
- package/src/SSHMimic/loginBanner.ts +0 -36
- package/src/SSHMimic/loginFormat.ts +0 -10
- package/src/SSHMimic/prompt.ts +0 -14
- package/src/SSHMimic/sftp.ts +0 -883
- package/src/VirtualFileSystem/binaryPack.ts +0 -258
- package/src/VirtualFileSystem/index.ts +0 -1193
- package/src/VirtualFileSystem/internalTypes.ts +0 -43
- package/src/VirtualFileSystem/journal.ts +0 -171
- package/src/VirtualFileSystem/path.ts +0 -74
- package/src/VirtualPackageManager/index.ts +0 -996
- package/src/VirtualShell/idleManager.ts +0 -137
- package/src/VirtualShell/index.ts +0 -475
- package/src/VirtualShell/shell.ts +0 -700
- package/src/VirtualShell/shellParser.ts +0 -285
- package/src/VirtualUserManager/index.ts +0 -758
- package/src/bun.d.ts +0 -1
- package/src/commands/adduser.ts +0 -103
- package/src/commands/alias.ts +0 -69
- package/src/commands/apt.ts +0 -233
- package/src/commands/awk.ts +0 -168
- package/src/commands/base64.ts +0 -29
- package/src/commands/cat.ts +0 -52
- package/src/commands/cd.ts +0 -25
- package/src/commands/chmod.ts +0 -85
- package/src/commands/clear.ts +0 -15
- package/src/commands/command-helpers.ts +0 -286
- package/src/commands/cp.ts +0 -83
- package/src/commands/curl.ts +0 -147
- package/src/commands/cut.ts +0 -36
- package/src/commands/date.ts +0 -30
- package/src/commands/declare.ts +0 -49
- package/src/commands/deluser.ts +0 -98
- package/src/commands/df.ts +0 -23
- package/src/commands/diff.ts +0 -43
- package/src/commands/dpkg.ts +0 -180
- package/src/commands/du.ts +0 -56
- package/src/commands/echo.ts +0 -58
- package/src/commands/env.ts +0 -23
- package/src/commands/exit.ts +0 -18
- package/src/commands/export.ts +0 -34
- package/src/commands/find.ts +0 -68
- package/src/commands/free.ts +0 -47
- package/src/commands/grep.ts +0 -116
- package/src/commands/groups.ts +0 -19
- package/src/commands/gzip.ts +0 -88
- package/src/commands/head.ts +0 -52
- package/src/commands/help.ts +0 -152
- package/src/commands/helpers.ts +0 -234
- package/src/commands/history.ts +0 -34
- package/src/commands/hostname.ts +0 -14
- package/src/commands/htop.ts +0 -20
- package/src/commands/id.ts +0 -19
- package/src/commands/index.ts +0 -9
- package/src/commands/kill.ts +0 -19
- package/src/commands/ln.ts +0 -71
- package/src/commands/ls.ts +0 -243
- package/src/commands/lsb-release.ts +0 -63
- package/src/commands/man.ts +0 -31
- package/src/commands/manuals/adduser.txt +0 -11
- package/src/commands/manuals/apt-cache.txt +0 -12
- package/src/commands/manuals/apt.txt +0 -20
- package/src/commands/manuals/awk.txt +0 -13
- package/src/commands/manuals/cat.txt +0 -14
- package/src/commands/manuals/cd.txt +0 -16
- package/src/commands/manuals/chmod.txt +0 -16
- package/src/commands/manuals/clear.txt +0 -10
- package/src/commands/manuals/cp.txt +0 -10
- package/src/commands/manuals/curl.txt +0 -20
- package/src/commands/manuals/date.txt +0 -14
- package/src/commands/manuals/declare.txt +0 -12
- package/src/commands/manuals/deluser.txt +0 -10
- package/src/commands/manuals/df.txt +0 -10
- package/src/commands/manuals/dpkg-query.txt +0 -11
- package/src/commands/manuals/dpkg.txt +0 -14
- package/src/commands/manuals/du.txt +0 -11
- package/src/commands/manuals/echo.txt +0 -11
- package/src/commands/manuals/false.txt +0 -10
- package/src/commands/manuals/find.txt +0 -11
- package/src/commands/manuals/free.txt +0 -12
- package/src/commands/manuals/grep.txt +0 -13
- package/src/commands/manuals/groups.txt +0 -10
- package/src/commands/manuals/gzip.txt +0 -11
- package/src/commands/manuals/head.txt +0 -10
- package/src/commands/manuals/help.txt +0 -11
- package/src/commands/manuals/history.txt +0 -11
- package/src/commands/manuals/hostname.txt +0 -10
- package/src/commands/manuals/id.txt +0 -10
- package/src/commands/manuals/kill.txt +0 -13
- package/src/commands/manuals/ls.txt +0 -20
- package/src/commands/manuals/lsb_release.txt +0 -14
- package/src/commands/manuals/mkdir.txt +0 -10
- package/src/commands/manuals/mv.txt +0 -10
- package/src/commands/manuals/nano.txt +0 -11
- package/src/commands/manuals/neofetch.txt +0 -10
- package/src/commands/manuals/node.txt +0 -13
- package/src/commands/manuals/npm.txt +0 -13
- package/src/commands/manuals/npx.txt +0 -13
- package/src/commands/manuals/passwd.txt +0 -11
- package/src/commands/manuals/ping.txt +0 -10
- package/src/commands/manuals/printf.txt +0 -11
- package/src/commands/manuals/ps.txt +0 -10
- package/src/commands/manuals/pwd.txt +0 -10
- package/src/commands/manuals/python3.txt +0 -13
- package/src/commands/manuals/readlink.txt +0 -10
- package/src/commands/manuals/return.txt +0 -10
- package/src/commands/manuals/rm.txt +0 -10
- package/src/commands/manuals/sed.txt +0 -11
- package/src/commands/manuals/set.txt +0 -11
- package/src/commands/manuals/shift.txt +0 -10
- package/src/commands/manuals/sleep.txt +0 -10
- package/src/commands/manuals/sort.txt +0 -12
- package/src/commands/manuals/source.txt +0 -11
- package/src/commands/manuals/ssh.txt +0 -11
- package/src/commands/manuals/stat.txt +0 -10
- package/src/commands/manuals/su.txt +0 -13
- package/src/commands/manuals/sudo.txt +0 -11
- package/src/commands/manuals/tail.txt +0 -10
- package/src/commands/manuals/tar.txt +0 -19
- package/src/commands/manuals/tee.txt +0 -10
- package/src/commands/manuals/test.txt +0 -11
- package/src/commands/manuals/touch.txt +0 -11
- package/src/commands/manuals/tr.txt +0 -10
- package/src/commands/manuals/trap.txt +0 -10
- package/src/commands/manuals/true.txt +0 -10
- package/src/commands/manuals/type.txt +0 -10
- package/src/commands/manuals/uname.txt +0 -12
- package/src/commands/manuals/uniq.txt +0 -12
- package/src/commands/manuals/unset.txt +0 -10
- package/src/commands/manuals/uptime.txt +0 -11
- package/src/commands/manuals/wc.txt +0 -12
- package/src/commands/manuals/wget.txt +0 -12
- package/src/commands/manuals/which.txt +0 -10
- package/src/commands/manuals/whoami.txt +0 -10
- package/src/commands/manuals/xargs.txt +0 -10
- package/src/commands/manuals-bundle.ts +0 -898
- package/src/commands/mkdir.ts +0 -31
- package/src/commands/mv.ts +0 -50
- package/src/commands/nano.ts +0 -38
- package/src/commands/neofetch.ts +0 -53
- package/src/commands/node.ts +0 -341
- package/src/commands/npm.ts +0 -132
- package/src/commands/passwd.ts +0 -50
- package/src/commands/ping.ts +0 -32
- package/src/commands/printf.ts +0 -129
- package/src/commands/ps.ts +0 -58
- package/src/commands/pwd.ts +0 -9
- package/src/commands/python.ts +0 -2229
- package/src/commands/read.ts +0 -46
- package/src/commands/registry.ts +0 -249
- package/src/commands/rm.ts +0 -42
- package/src/commands/runtime.ts +0 -421
- package/src/commands/sed.ts +0 -68
- package/src/commands/seq.ts +0 -43
- package/src/commands/set.ts +0 -29
- package/src/commands/sh.ts +0 -467
- package/src/commands/shift.ts +0 -63
- package/src/commands/sleep.ts +0 -20
- package/src/commands/sort.ts +0 -46
- package/src/commands/source.ts +0 -52
- package/src/commands/stat.ts +0 -61
- package/src/commands/su.ts +0 -72
- package/src/commands/sudo.ts +0 -76
- package/src/commands/tail.ts +0 -53
- package/src/commands/tar.ts +0 -102
- package/src/commands/tee.ts +0 -36
- package/src/commands/test.ts +0 -137
- package/src/commands/touch.ts +0 -28
- package/src/commands/tr.ts +0 -70
- package/src/commands/tree.ts +0 -20
- package/src/commands/true.ts +0 -27
- package/src/commands/type.ts +0 -48
- package/src/commands/uname.ts +0 -29
- package/src/commands/uniq.ts +0 -39
- package/src/commands/unset.ts +0 -17
- package/src/commands/uptime.ts +0 -54
- package/src/commands/wc.ts +0 -55
- package/src/commands/wget.ts +0 -148
- package/src/commands/which.ts +0 -37
- package/src/commands/who.ts +0 -25
- package/src/commands/whoami.ts +0 -14
- package/src/commands/xargs.ts +0 -31
- package/src/index.ts +0 -67
- package/src/modules/linuxRootfs.ts +0 -1961
- package/src/modules/neofetch.ts +0 -358
- package/src/modules/shellInteractive.ts +0 -57
- package/src/modules/shellRuntime.ts +0 -76
- package/src/self-standalone.ts +0 -542
- package/src/standalone-wo-sftp.ts +0 -38
- package/src/standalone.ts +0 -72
- package/src/types/commands.ts +0 -146
- package/src/types/pipeline.ts +0 -52
- package/src/types/streams.ts +0 -32
- package/src/types/tar-stream.d.ts +0 -38
- package/src/types/vfs.ts +0 -98
- package/src/utils/expand.ts +0 -491
- package/src/utils/perfLogger.ts +0 -72
- package/src/utils/tokenize.ts +0 -98
- package/src/utils/vfsDiff.ts +0 -275
- package/tests/command-helpers.test.ts +0 -116
- package/tests/commands-admin-net.test.ts +0 -441
- package/tests/commands-advanced.test.ts +0 -456
- package/tests/commands-core.test.ts +0 -562
- package/tests/commands-missing.test.ts +0 -570
- package/tests/commands-specific-units.test.ts +0 -327
- package/tests/commands-text-sys.test.ts +0 -445
- package/tests/expand.test.ts +0 -170
- package/tests/helpers.test.ts +0 -97
- package/tests/new-features.test.ts +0 -1036
- package/tests/parser-executor.test.ts +0 -37
- package/tests/sftp.test.ts +0 -323
- package/tests/ssh-exec.test.ts +0 -45
- package/tests/test-helper.ts +0 -79
- package/tests/users.test.ts +0 -86
- package/tsconfig.json +0 -49
- package/typedoc.json +0 -47
|
@@ -1,1036 +0,0 @@
|
|
|
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("/ 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 neofetch");
|
|
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 ?? -1);
|
|
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("nyx");
|
|
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
|
|
525
|
-
?.split("\n")
|
|
526
|
-
.filter((l) => l.includes("icmp_seq="));
|
|
527
|
-
expect(dataLines?.length).toBe(2);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// test / [ command
|
|
531
|
-
test("[ -f path ] returns 0 for existing file", async () => {
|
|
532
|
-
await c.exec("touch /tmp/testfile");
|
|
533
|
-
const r = await c.exec("[ -f /tmp/testfile ] && echo yes || echo no");
|
|
534
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test("[ -d path ] returns 0 for existing directory", async () => {
|
|
538
|
-
const r = await c.exec("[ -d /etc ] && echo yes || echo no");
|
|
539
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
test("[ -f path ] returns 1 for non-existent file", async () => {
|
|
543
|
-
const r = await c.exec(
|
|
544
|
-
"[ -f /tmp/doesnotexist999 ] && echo yes || echo no",
|
|
545
|
-
);
|
|
546
|
-
expect(r.stdout?.trim()).toBe("no");
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
test("[ -e path ] returns 0 for existing path", async () => {
|
|
550
|
-
const r = await c.exec("[ -e /etc/hostname ] && echo yes || echo no");
|
|
551
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
test("test string comparison = works", async () => {
|
|
555
|
-
const r = await c.exec("[ hello = hello ] && echo yes || echo no");
|
|
556
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test("test numeric -eq works", async () => {
|
|
560
|
-
const r = await c.exec("[ 5 -eq 5 ] && echo yes || echo no");
|
|
561
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test("test numeric -lt works", async () => {
|
|
565
|
-
const r = await c.exec("[ 3 -lt 5 ] && echo yes || echo no");
|
|
566
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("test -z empty string", async () => {
|
|
570
|
-
const r = await c.exec("[ -z '' ] && echo yes || echo no");
|
|
571
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
test("test -n non-empty string", async () => {
|
|
575
|
-
const r = await c.exec("[ -n hello ] && echo yes || echo no");
|
|
576
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// source / .
|
|
580
|
-
test("source executes file in current env", async () => {
|
|
581
|
-
shell2.vfs.writeFile("/tmp/setup.sh", "export SOURCED=yes\n");
|
|
582
|
-
const r = await c.exec("source /tmp/setup.sh && echo $SOURCED");
|
|
583
|
-
expect(r.stdout?.trim()).toBe("yes");
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test(". (dot) is alias for source", async () => {
|
|
587
|
-
shell2.vfs.writeFile("/tmp/dot.sh", "export DOTTED=ok\n");
|
|
588
|
-
const r = await c.exec(". /tmp/dot.sh && echo $DOTTED");
|
|
589
|
-
expect(r.stdout?.trim()).toBe("ok");
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
// sh -c with $(cmd)
|
|
593
|
-
test("sh -c handles $(cmd) substitution", async () => {
|
|
594
|
-
const r = await c.exec("sh -c 'echo user=$(whoami)'");
|
|
595
|
-
expect(r.stdout?.trim()).toBe("user=root");
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
test("sh -c for loop with $(cmd)", async () => {
|
|
599
|
-
const r = await c.exec("sh -c 'for x in a b; do echo $(echo $x); done'");
|
|
600
|
-
expect(r.stdout?.trim()).toBe("a\nb");
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// history
|
|
604
|
-
test("history command returns command list", async () => {
|
|
605
|
-
// history reads from VFS .bash_history (written by interactive shell)
|
|
606
|
-
// in non-interactive context it may be empty — just check it doesn't crash
|
|
607
|
-
const r = await c.exec("history");
|
|
608
|
-
expect(r.exitCode).toBe(0);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// ps -u
|
|
612
|
-
test("ps -u shows USER column", async () => {
|
|
613
|
-
const r = await c.exec("ps -u");
|
|
614
|
-
expect(r.stdout).toContain("USER");
|
|
615
|
-
expect(r.stdout).toContain("PID");
|
|
616
|
-
expect(r.stdout).toContain("%CPU");
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
test("ps aux shows extended format", async () => {
|
|
620
|
-
const r = await c.exec("ps aux");
|
|
621
|
-
expect(r.stdout).toContain("USER");
|
|
622
|
-
expect(r.exitCode).toBe(0);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
// wc -l with pipe (after echo -e fix)
|
|
626
|
-
test("wc -l counts newlines correctly via pipe", async () => {
|
|
627
|
-
const r = await c.exec("echo -e 'a\\nb\\nc' | wc -l");
|
|
628
|
-
expect(r.stdout?.trim()).toBe("3");
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
// ─── Roadmap features ────────────────────────────────────────────────────────
|
|
633
|
-
|
|
634
|
-
describe("/proc/self and /proc/<pid>", () => {
|
|
635
|
-
let shell4: VirtualShell;
|
|
636
|
-
let c4: InstanceType<typeof SshClient>;
|
|
637
|
-
|
|
638
|
-
beforeAll(async () => {
|
|
639
|
-
shell4 = new VirtualShell("proc-vm");
|
|
640
|
-
await shell4.ensureInitialized();
|
|
641
|
-
c4 = new SshClient(shell4, "root");
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("/proc/self exists and has comm file", async () => {
|
|
645
|
-
expect(shell4.vfs.exists("/proc/self")).toBe(true);
|
|
646
|
-
expect(shell4.vfs.exists("/proc/self/comm")).toBe(true);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
test("/proc/self/status has Name field", async () => {
|
|
650
|
-
const r = await c4.exec("cat /proc/self/status");
|
|
651
|
-
expect(r.exitCode).toBe(0);
|
|
652
|
-
expect(r.stdout).toContain("Name:");
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
test("/proc/self/cmdline is readable", async () => {
|
|
656
|
-
const r = await c4.exec("cat /proc/self/cmdline");
|
|
657
|
-
expect(r.exitCode).toBe(0);
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
test("/proc/1 (init) exists", async () => {
|
|
661
|
-
expect(shell4.vfs.exists("/proc/1")).toBe(true);
|
|
662
|
-
expect(shell4.vfs.exists("/proc/1/status")).toBe(true);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
test("/proc/1/status has correct pid", async () => {
|
|
666
|
-
const r = await c4.exec("cat /proc/1/status");
|
|
667
|
-
expect(r.stdout).toContain("Pid:");
|
|
668
|
-
expect(r.stdout).toContain("1");
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
test("refreshProcFs updates /proc contents", () => {
|
|
672
|
-
shell4.refreshProcFs();
|
|
673
|
-
expect(shell4.vfs.exists("/proc/uptime")).toBe(true);
|
|
674
|
-
expect(shell4.vfs.exists("/proc/self")).toBe(true);
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
import { assertDiff, diffSnapshots, formatDiff } from "../src";
|
|
679
|
-
|
|
680
|
-
describe("VFS snapshot diff tooling", () => {
|
|
681
|
-
let shell5: VirtualShell;
|
|
682
|
-
let c5: InstanceType<typeof SshClient>;
|
|
683
|
-
|
|
684
|
-
beforeAll(async () => {
|
|
685
|
-
shell5 = new VirtualShell("diff-vm");
|
|
686
|
-
await shell5.ensureInitialized();
|
|
687
|
-
c5 = new SshClient(shell5, "root");
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
test("diffSnapshots returns clean diff for identical snapshots", () => {
|
|
691
|
-
const snap = shell5.vfs.toSnapshot();
|
|
692
|
-
const diff = diffSnapshots(snap, snap);
|
|
693
|
-
expect(diff.clean).toBe(true);
|
|
694
|
-
expect(diff.added).toHaveLength(0);
|
|
695
|
-
expect(diff.removed).toHaveLength(0);
|
|
696
|
-
expect(diff.modified).toHaveLength(0);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
test("diffSnapshots detects added file", async () => {
|
|
700
|
-
const before = shell5.vfs.toSnapshot();
|
|
701
|
-
await c5.exec("echo test > /tmp/diff-test.txt");
|
|
702
|
-
const after = shell5.vfs.toSnapshot();
|
|
703
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
704
|
-
const paths = diff.added.map((e) => e.path);
|
|
705
|
-
expect(paths).toContain("/tmp/diff-test.txt");
|
|
706
|
-
expect(diff.clean).toBe(false);
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
test("diffSnapshots detects added directory", async () => {
|
|
710
|
-
const before = shell5.vfs.toSnapshot();
|
|
711
|
-
shell5.vfs.mkdir("/tmp/diff-newdir", 0o755);
|
|
712
|
-
const after = shell5.vfs.toSnapshot();
|
|
713
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
714
|
-
const paths = diff.added.map((e) => e.path);
|
|
715
|
-
expect(paths).toContain("/tmp/diff-newdir");
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
test("diffSnapshots detects modified file", async () => {
|
|
719
|
-
shell5.vfs.writeFile("/tmp/modtest.txt", "before");
|
|
720
|
-
const before = shell5.vfs.toSnapshot();
|
|
721
|
-
shell5.vfs.writeFile("/tmp/modtest.txt", "after");
|
|
722
|
-
const after = shell5.vfs.toSnapshot();
|
|
723
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
724
|
-
const mod = diff.modified.find((e) => e.path === "/tmp/modtest.txt");
|
|
725
|
-
expect(mod).toBeDefined();
|
|
726
|
-
expect(mod?.before).toBe("before");
|
|
727
|
-
expect(mod?.after).toBe("after");
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
test("diffSnapshots detects removed file", async () => {
|
|
731
|
-
shell5.vfs.writeFile("/tmp/toremove.txt", "bye");
|
|
732
|
-
const before = shell5.vfs.toSnapshot();
|
|
733
|
-
shell5.vfs.remove("/tmp/toremove.txt");
|
|
734
|
-
const after = shell5.vfs.toSnapshot();
|
|
735
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
736
|
-
const paths = diff.removed.map((e) => e.path);
|
|
737
|
-
expect(paths).toContain("/tmp/toremove.txt");
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
test("diffSnapshots ignores specified prefixes", async () => {
|
|
741
|
-
shell5.vfs.writeFile("/proc/uptime", "999.00 888.00\n");
|
|
742
|
-
const before = shell5.vfs.toSnapshot();
|
|
743
|
-
shell5.vfs.writeFile("/proc/uptime", "1000.00 900.00\n");
|
|
744
|
-
const after = shell5.vfs.toSnapshot();
|
|
745
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
746
|
-
expect(diff.modified.map((e) => e.path)).not.toContain("/proc/uptime");
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
test("formatDiff returns (no changes) for clean diff", () => {
|
|
750
|
-
const snap = shell5.vfs.toSnapshot();
|
|
751
|
-
const diff = diffSnapshots(snap, snap);
|
|
752
|
-
expect(formatDiff(diff)).toBe("(no changes)");
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
test("formatDiff includes change summary", async () => {
|
|
756
|
-
const before = shell5.vfs.toSnapshot();
|
|
757
|
-
shell5.vfs.writeFile("/tmp/format-test.txt", "x");
|
|
758
|
-
const after = shell5.vfs.toSnapshot();
|
|
759
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
760
|
-
const formatted = formatDiff(diff);
|
|
761
|
-
expect(formatted).toContain("added");
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
test("assertDiff passes when paths match", async () => {
|
|
765
|
-
shell5.vfs.writeFile("/tmp/assert-test.txt", "hello");
|
|
766
|
-
const before = shell5.vfs.toSnapshot();
|
|
767
|
-
shell5.vfs.writeFile("/tmp/assert-new.txt", "new");
|
|
768
|
-
shell5.vfs.remove("/tmp/assert-test.txt");
|
|
769
|
-
const after = shell5.vfs.toSnapshot();
|
|
770
|
-
const diff = diffSnapshots(before, after, { ignore: ["/proc"] });
|
|
771
|
-
expect(() =>
|
|
772
|
-
assertDiff(diff, {
|
|
773
|
-
added: ["/tmp/assert-new.txt"],
|
|
774
|
-
removed: ["/tmp/assert-test.txt"],
|
|
775
|
-
}),
|
|
776
|
-
).not.toThrow();
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
test("assertDiff throws when expected path is missing", () => {
|
|
780
|
-
const snap = shell5.vfs.toSnapshot();
|
|
781
|
-
const diff = diffSnapshots(snap, snap);
|
|
782
|
-
expect(() => assertDiff(diff, { added: ["/nonexistent"] })).toThrow();
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
describe("node and python3 REPL stubs", () => {
|
|
787
|
-
let shell6: VirtualShell;
|
|
788
|
-
let c6: InstanceType<typeof SshClient>;
|
|
789
|
-
|
|
790
|
-
beforeAll(async () => {
|
|
791
|
-
shell6 = new VirtualShell("repl-vm");
|
|
792
|
-
await shell6.ensureInitialized();
|
|
793
|
-
c6 = new SshClient(shell6, "root");
|
|
794
|
-
await c6.exec("apt install nodejs python3");
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
test("node --version returns v18", async () => {
|
|
798
|
-
const r = await c6.exec("node --version");
|
|
799
|
-
expect(r.exitCode).toBe(0);
|
|
800
|
-
expect(r.stdout?.trim()).toBe("v18.19.0");
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
test("node -e evaluates arithmetic", async () => {
|
|
804
|
-
const r = await c6.exec("node -e '2 + 3'");
|
|
805
|
-
expect(r.exitCode).toBe(0);
|
|
806
|
-
expect(r.stdout?.trim()).toBe("5");
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
test("node -e console.log works", async () => {
|
|
810
|
-
const r = await c6.exec("node -e 'console.log(42)'");
|
|
811
|
-
expect(r.stdout?.trim()).toBe("42");
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
test("node executes VFS file", async () => {
|
|
815
|
-
shell6.vfs.writeFile("/tmp/test.js", "console.log(10 + 5);\n");
|
|
816
|
-
const r = await c6.exec("node /tmp/test.js");
|
|
817
|
-
expect(r.stdout?.trim()).toBe("15");
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
test("node missing file returns exit 1", async () => {
|
|
821
|
-
const r = await c6.exec("node /tmp/nonexistent.js");
|
|
822
|
-
expect(r.exitCode).toBe(1);
|
|
823
|
-
expect(r.stderr).toContain("No such file or directory");
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
test("python3 --version returns Python 3.11", async () => {
|
|
827
|
-
const r = await c6.exec("python3 --version");
|
|
828
|
-
expect(r.exitCode).toBe(0);
|
|
829
|
-
expect(r.stdout?.trim()).toContain("Python 3.11");
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
test("python3 -c print arithmetic", async () => {
|
|
833
|
-
const r = await c6.exec("python3 -c 'print(2 + 3)'");
|
|
834
|
-
expect(r.stdout?.trim()).toBe("5");
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
test("python3 -c f-string", async () => {
|
|
838
|
-
const r = await c6.exec("python3 -c 'print(f\"result: {1+1}\")'");
|
|
839
|
-
expect(r.stdout?.trim()).toBe("result: 2");
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
test("python3 executes VFS script", async () => {
|
|
843
|
-
shell6.vfs.writeFile("/tmp/test.py", "print(10 + 5)\n");
|
|
844
|
-
const r = await c6.exec("python3 /tmp/test.py");
|
|
845
|
-
expect(r.stdout?.trim()).toBe("15");
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
test("python alias works", async () => {
|
|
849
|
-
const r = await c6.exec("python --version");
|
|
850
|
-
expect(r.exitCode).toBe(0);
|
|
851
|
-
expect(r.stdout?.trim()).toContain("Python");
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
test("python3 missing file returns exit 2", async () => {
|
|
855
|
-
const r = await c6.exec("python3 /tmp/nonexistent.py");
|
|
856
|
-
expect(r.exitCode).toBe(2);
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
test("node is available as a shell command", async () => {
|
|
860
|
-
const r = await c6.exec("node --version");
|
|
861
|
-
expect(r.exitCode).toBe(0);
|
|
862
|
-
});
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
// ─── Enhanced REPL tests (post-rewrite) ──────────────────────────────────────
|
|
866
|
-
|
|
867
|
-
describe("node enhanced REPL", () => {
|
|
868
|
-
let shell7: VirtualShell;
|
|
869
|
-
let c7: InstanceType<typeof SshClient>;
|
|
870
|
-
|
|
871
|
-
beforeAll(async () => {
|
|
872
|
-
shell7 = new VirtualShell("node-vm");
|
|
873
|
-
await shell7.ensureInitialized();
|
|
874
|
-
c7 = new SshClient(shell7, "root");
|
|
875
|
-
await c7.exec("apt install nodejs");
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
test("node -e string methods", async () => {
|
|
879
|
-
shell7.vfs.writeFile("/tmp/str.js", "'hello'.toUpperCase()");
|
|
880
|
-
const r = await c7.exec("node /tmp/str.js");
|
|
881
|
-
expect(r.stdout?.trim()).toBe("HELLO");
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
test("node -e array methods", async () => {
|
|
885
|
-
shell7.vfs.writeFile("/tmp/arr.js", "[1,2,3].map(x => x*2).join(',')");
|
|
886
|
-
const r = await c7.exec("node /tmp/arr.js");
|
|
887
|
-
expect(r.stdout?.trim()).toBe("2,4,6");
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
test("node process.version is virtual", async () => {
|
|
891
|
-
const r = await c7.exec("node -e 'process.version'");
|
|
892
|
-
expect(r.stdout?.trim()).toBe("v18.19.0");
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
test("node require path works", async () => {
|
|
896
|
-
shell7.vfs.writeFile(
|
|
897
|
-
"/tmp/path.js",
|
|
898
|
-
"const p = require('path'); console.log(p.join('a', 'b'))",
|
|
899
|
-
);
|
|
900
|
-
const r = await c7.exec("node /tmp/path.js");
|
|
901
|
-
expect(r.stdout?.trim()).toBe("a/b");
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
test("node require fs throws gracefully", async () => {
|
|
905
|
-
shell7.vfs.writeFile(
|
|
906
|
-
"/tmp/fs.js",
|
|
907
|
-
"try { require('fs') } catch(e) { console.log('blocked') }",
|
|
908
|
-
);
|
|
909
|
-
const r = await c7.exec("node /tmp/fs.js");
|
|
910
|
-
expect(r.stdout?.trim()).toBe("blocked");
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
test("node console.log output", async () => {
|
|
914
|
-
shell7.vfs.writeFile("/tmp/log.js", "console.log('hello', 'world')");
|
|
915
|
-
const r = await c7.exec("node /tmp/log.js");
|
|
916
|
-
expect(r.stdout?.trim()).toBe("hello world");
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
test("node Math methods", async () => {
|
|
920
|
-
const r = await c7.exec("node -e 'Math.floor(3.9)'");
|
|
921
|
-
expect(r.stdout?.trim()).toBe("3");
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
test("node JSON.stringify", async () => {
|
|
925
|
-
const r = await c7.exec("node -e 'JSON.stringify({x:1})'");
|
|
926
|
-
expect(r.stdout?.trim()).toBe('{"x":1}');
|
|
927
|
-
});
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
describe("python3 enhanced interpreter", () => {
|
|
931
|
-
let shell8: VirtualShell;
|
|
932
|
-
let c8: InstanceType<typeof SshClient>;
|
|
933
|
-
|
|
934
|
-
beforeAll(async () => {
|
|
935
|
-
shell8 = new VirtualShell("python-vm");
|
|
936
|
-
await shell8.ensureInitialized();
|
|
937
|
-
c8 = new SshClient(shell8, "root");
|
|
938
|
-
await c8.exec("apt install python3");
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
const py = async (code: string) => {
|
|
942
|
-
shell8.vfs.writeFile("/tmp/t.py", code);
|
|
943
|
-
return c8.exec("python3 /tmp/t.py");
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
test("str.upper()", async () => {
|
|
947
|
-
const r = await py("print('hello'.upper())");
|
|
948
|
-
expect(r.stdout?.trim()).toBe("HELLO");
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
test("str.split()", async () => {
|
|
952
|
-
const r = await py("print('a,b,c'.split(','))");
|
|
953
|
-
expect(r.stdout?.trim()).toBe("['a', 'b', 'c']");
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
test("str.join()", async () => {
|
|
957
|
-
const r = await py("print(','.join(['a','b','c']))");
|
|
958
|
-
expect(r.stdout?.trim()).toBe("a,b,c");
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
test("list comprehension", async () => {
|
|
962
|
-
const r = await py("print([x**2 for x in range(5)])");
|
|
963
|
-
expect(r.stdout?.trim()).toBe("[0, 1, 4, 9, 16]");
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
test("list.sort() in-place", async () => {
|
|
967
|
-
const r = await py("nums=[3,1,4,1,5]\nnums.sort()\nprint(nums)");
|
|
968
|
-
expect(r.stdout?.trim()).toBe("[1, 1, 3, 4, 5]");
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
test("dict access and methods", async () => {
|
|
972
|
-
const r = await py("d={'x':1,'y':2}\nprint(d['x'])\nprint(list(d.keys()))");
|
|
973
|
-
expect(r.stdout).toContain("1");
|
|
974
|
-
expect(r.stdout).toContain("x");
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
test("class with __init__ and methods", async () => {
|
|
978
|
-
const r = await py(
|
|
979
|
-
"class Dog:\n def __init__(self, name):\n self.name = name\n def bark(self):\n return f'Woof! I am {self.name}'\nd = Dog('Rex')\nprint(d.bark())",
|
|
980
|
-
);
|
|
981
|
-
expect(r.stdout?.trim()).toBe("Woof! I am Rex");
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
test("import math and use functions", async () => {
|
|
985
|
-
const r = await py(
|
|
986
|
-
"import math\nprint(math.floor(3.9))\nprint(round(math.sqrt(16), 0))",
|
|
987
|
-
);
|
|
988
|
-
expect(r.stdout).toContain("3");
|
|
989
|
-
expect(r.stdout).toContain("4");
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
test("import os and getcwd", async () => {
|
|
993
|
-
const r = await py("import os\nprint(os.getcwd())");
|
|
994
|
-
expect(r.exitCode).toBe(0);
|
|
995
|
-
expect(r.stdout?.trim().length).toBeGreaterThan(0);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
test("import sys and version", async () => {
|
|
999
|
-
const r = await py("import sys\nprint(sys.version[:5])");
|
|
1000
|
-
expect(r.stdout?.trim()).toBe("3.11.");
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
test("import json dumps/loads", async () => {
|
|
1004
|
-
const r = await py(
|
|
1005
|
-
"import json\nd={'x':1}\nprint(json.dumps(d))\nprint(json.loads('{\"a\":2}')['a'])",
|
|
1006
|
-
);
|
|
1007
|
-
expect(r.stdout).toContain('"x"');
|
|
1008
|
-
expect(r.stdout).toContain("2");
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
test("try/except handles errors", async () => {
|
|
1012
|
-
const r = await py(
|
|
1013
|
-
"try:\n x = 1/0\nexcept ZeroDivisionError:\n print('caught')",
|
|
1014
|
-
);
|
|
1015
|
-
expect(r.stdout?.trim()).toBe("caught");
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
test("f-string interpolation", async () => {
|
|
1019
|
-
const r = await py("name='world'\nprint(f'hello {name}')");
|
|
1020
|
-
expect(r.stdout?.trim()).toBe("hello world");
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
test("sorted() and reversed()", async () => {
|
|
1024
|
-
const r = await py(
|
|
1025
|
-
"print(sorted([3,1,2]))\nprint(list(reversed([1,2,3])))",
|
|
1026
|
-
);
|
|
1027
|
-
expect(r.stdout).toContain("[1, 2, 3]");
|
|
1028
|
-
expect(r.stdout).toContain("[3, 2, 1]");
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
test("enumerate()", async () => {
|
|
1032
|
-
const r = await py("for i,v in enumerate(['a','b']):\n print(i, v)");
|
|
1033
|
-
expect(r.stdout).toContain("0 a");
|
|
1034
|
-
expect(r.stdout).toContain("1 b");
|
|
1035
|
-
});
|
|
1036
|
-
});
|