typescript-virtual-container 1.2.9 → 1.3.1

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.
Files changed (281) hide show
  1. package/.vscode/settings.json +0 -1
  2. package/README.md +141 -50
  3. package/biome.json +7 -0
  4. package/dist/SSHMimic/exec.d.ts.map +1 -1
  5. package/dist/SSHMimic/executor.d.ts.map +1 -1
  6. package/dist/SSHMimic/executor.js +32 -16
  7. package/dist/SSHMimic/index.d.ts.map +1 -1
  8. package/dist/SSHMimic/index.js +20 -6
  9. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/binaryPack.js +29 -6
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +36 -13
  13. package/dist/VirtualPackageManager/index.d.ts.map +1 -1
  14. package/dist/VirtualPackageManager/index.js +192 -43
  15. package/dist/VirtualShell/index.d.ts +10 -4
  16. package/dist/VirtualShell/index.d.ts.map +1 -1
  17. package/dist/VirtualShell/index.js +18 -7
  18. package/dist/VirtualShell/shell.d.ts.map +1 -1
  19. package/dist/VirtualShell/shell.js +3 -1
  20. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  22. package/dist/commands/adduser.d.ts +6 -0
  23. package/dist/commands/adduser.d.ts.map +1 -1
  24. package/dist/commands/adduser.js +6 -0
  25. package/dist/commands/alias.d.ts +5 -0
  26. package/dist/commands/alias.d.ts.map +1 -1
  27. package/dist/commands/alias.js +5 -0
  28. package/dist/commands/apt.d.ts +5 -0
  29. package/dist/commands/apt.d.ts.map +1 -1
  30. package/dist/commands/apt.js +32 -9
  31. package/dist/commands/awk.d.ts +11 -0
  32. package/dist/commands/awk.d.ts.map +1 -1
  33. package/dist/commands/awk.js +15 -2
  34. package/dist/commands/base64.d.ts +5 -0
  35. package/dist/commands/base64.d.ts.map +1 -1
  36. package/dist/commands/base64.js +9 -1
  37. package/dist/commands/cat.d.ts +5 -0
  38. package/dist/commands/cat.d.ts.map +1 -1
  39. package/dist/commands/cat.js +10 -2
  40. package/dist/commands/cd.d.ts +5 -0
  41. package/dist/commands/cd.d.ts.map +1 -1
  42. package/dist/commands/cd.js +5 -0
  43. package/dist/commands/chmod.d.ts +5 -0
  44. package/dist/commands/chmod.d.ts.map +1 -1
  45. package/dist/commands/chmod.js +5 -0
  46. package/dist/commands/cp.d.ts +5 -0
  47. package/dist/commands/cp.d.ts.map +1 -1
  48. package/dist/commands/cp.js +5 -0
  49. package/dist/commands/curl.d.ts +5 -0
  50. package/dist/commands/curl.d.ts.map +1 -1
  51. package/dist/commands/curl.js +34 -6
  52. package/dist/commands/cut.d.ts +5 -0
  53. package/dist/commands/cut.d.ts.map +1 -1
  54. package/dist/commands/cut.js +8 -1
  55. package/dist/commands/date.d.ts +5 -0
  56. package/dist/commands/date.d.ts.map +1 -1
  57. package/dist/commands/date.js +7 -1
  58. package/dist/commands/declare.d.ts +3 -0
  59. package/dist/commands/declare.d.ts.map +1 -0
  60. package/dist/commands/declare.js +39 -0
  61. package/dist/commands/diff.d.ts +5 -0
  62. package/dist/commands/diff.d.ts.map +1 -1
  63. package/dist/commands/diff.js +5 -0
  64. package/dist/commands/dpkg.d.ts +5 -0
  65. package/dist/commands/dpkg.d.ts.map +1 -1
  66. package/dist/commands/dpkg.js +24 -7
  67. package/dist/commands/du.d.ts.map +1 -1
  68. package/dist/commands/du.js +8 -2
  69. package/dist/commands/echo.d.ts +5 -0
  70. package/dist/commands/echo.d.ts.map +1 -1
  71. package/dist/commands/echo.js +13 -4
  72. package/dist/commands/env.d.ts +5 -0
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +11 -1
  75. package/dist/commands/exit.d.ts +5 -0
  76. package/dist/commands/exit.d.ts.map +1 -1
  77. package/dist/commands/exit.js +12 -2
  78. package/dist/commands/export.d.ts.map +1 -1
  79. package/dist/commands/export.js +3 -1
  80. package/dist/commands/find.d.ts +5 -0
  81. package/dist/commands/find.d.ts.map +1 -1
  82. package/dist/commands/find.js +5 -0
  83. package/dist/commands/free.d.ts +5 -0
  84. package/dist/commands/free.d.ts.map +1 -1
  85. package/dist/commands/free.js +5 -0
  86. package/dist/commands/grep.d.ts +5 -0
  87. package/dist/commands/grep.d.ts.map +1 -1
  88. package/dist/commands/grep.js +12 -2
  89. package/dist/commands/gzip.d.ts +5 -0
  90. package/dist/commands/gzip.d.ts.map +1 -1
  91. package/dist/commands/gzip.js +18 -2
  92. package/dist/commands/head.d.ts +5 -0
  93. package/dist/commands/head.d.ts.map +1 -1
  94. package/dist/commands/head.js +5 -0
  95. package/dist/commands/help.d.ts.map +1 -1
  96. package/dist/commands/help.js +98 -45
  97. package/dist/commands/history.d.ts +5 -0
  98. package/dist/commands/history.d.ts.map +1 -1
  99. package/dist/commands/history.js +5 -0
  100. package/dist/commands/hostname.d.ts +5 -0
  101. package/dist/commands/hostname.d.ts.map +1 -1
  102. package/dist/commands/hostname.js +5 -0
  103. package/dist/commands/id.d.ts.map +1 -1
  104. package/dist/commands/id.js +4 -1
  105. package/dist/commands/index.d.ts +2 -17
  106. package/dist/commands/index.d.ts.map +1 -1
  107. package/dist/commands/index.js +2 -340
  108. package/dist/commands/ls.d.ts.map +1 -1
  109. package/dist/commands/ls.js +3 -1
  110. package/dist/commands/lsb-release.d.ts.map +1 -1
  111. package/dist/commands/lsb-release.js +8 -2
  112. package/dist/commands/nano.js +1 -1
  113. package/dist/commands/neofetch.js +1 -1
  114. package/dist/commands/node.d.ts +9 -0
  115. package/dist/commands/node.d.ts.map +1 -0
  116. package/dist/commands/node.js +316 -0
  117. package/dist/commands/npm.d.ts +19 -0
  118. package/dist/commands/npm.d.ts.map +1 -0
  119. package/dist/commands/npm.js +109 -0
  120. package/dist/commands/ping.d.ts.map +1 -1
  121. package/dist/commands/ping.js +3 -1
  122. package/dist/commands/printf.d.ts +3 -0
  123. package/dist/commands/printf.d.ts.map +1 -0
  124. package/dist/commands/printf.js +113 -0
  125. package/dist/commands/ps.d.ts.map +1 -1
  126. package/dist/commands/ps.js +4 -1
  127. package/dist/commands/python.d.ts +30 -0
  128. package/dist/commands/python.d.ts.map +1 -0
  129. package/dist/commands/python.js +2058 -0
  130. package/dist/commands/read.d.ts +3 -0
  131. package/dist/commands/read.d.ts.map +1 -0
  132. package/dist/commands/read.js +34 -0
  133. package/dist/commands/registry.d.ts +8 -0
  134. package/dist/commands/registry.d.ts.map +1 -0
  135. package/dist/commands/registry.js +229 -0
  136. package/dist/commands/runtime.d.ts +6 -0
  137. package/dist/commands/runtime.d.ts.map +1 -0
  138. package/dist/commands/runtime.js +280 -0
  139. package/dist/commands/sed.d.ts.map +1 -1
  140. package/dist/commands/sed.js +11 -3
  141. package/dist/commands/set.d.ts.map +1 -1
  142. package/dist/commands/set.js +9 -3
  143. package/dist/commands/sh.d.ts.map +1 -1
  144. package/dist/commands/sh.js +57 -36
  145. package/dist/commands/shift.d.ts +5 -0
  146. package/dist/commands/shift.d.ts.map +1 -0
  147. package/dist/commands/shift.js +52 -0
  148. package/dist/commands/sleep.d.ts.map +1 -1
  149. package/dist/commands/sort.d.ts.map +1 -1
  150. package/dist/commands/sort.js +4 -2
  151. package/dist/commands/source.d.ts.map +1 -1
  152. package/dist/commands/source.js +5 -2
  153. package/dist/commands/sudo.js +1 -1
  154. package/dist/commands/tar.d.ts.map +1 -1
  155. package/dist/commands/tar.js +11 -3
  156. package/dist/commands/tee.d.ts.map +1 -1
  157. package/dist/commands/tee.js +8 -6
  158. package/dist/commands/test.d.ts.map +1 -1
  159. package/dist/commands/test.js +46 -24
  160. package/dist/commands/tr.d.ts.map +1 -1
  161. package/dist/commands/tr.js +3 -1
  162. package/dist/commands/true.d.ts +4 -0
  163. package/dist/commands/true.d.ts.map +1 -0
  164. package/dist/commands/true.js +14 -0
  165. package/dist/commands/type.d.ts.map +1 -1
  166. package/dist/commands/type.js +1 -1
  167. package/dist/commands/uname.d.ts.map +1 -1
  168. package/dist/commands/uname.js +4 -1
  169. package/dist/commands/uniq.d.ts.map +1 -1
  170. package/dist/commands/uptime.d.ts.map +1 -1
  171. package/dist/commands/uptime.js +4 -1
  172. package/dist/commands/wget.d.ts.map +1 -1
  173. package/dist/commands/wget.js +32 -7
  174. package/dist/commands/which.d.ts.map +1 -1
  175. package/dist/commands/xargs.d.ts.map +1 -1
  176. package/dist/commands/xargs.js +1 -1
  177. package/dist/index.d.ts +15 -14
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +9 -9
  180. package/dist/modules/linuxRootfs.d.ts +18 -1
  181. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  182. package/dist/modules/linuxRootfs.js +160 -17
  183. package/dist/standalone-wo-sftp.d.ts +2 -0
  184. package/dist/standalone-wo-sftp.d.ts.map +1 -0
  185. package/dist/standalone-wo-sftp.js +30 -0
  186. package/dist/utils/expand.d.ts +50 -0
  187. package/dist/utils/expand.d.ts.map +1 -0
  188. package/dist/utils/expand.js +183 -0
  189. package/dist/utils/vfsDiff.d.ts +90 -0
  190. package/dist/utils/vfsDiff.d.ts.map +1 -0
  191. package/dist/utils/vfsDiff.js +177 -0
  192. package/package.json +2 -1
  193. package/src/SSHMimic/exec.ts +10 -1
  194. package/src/SSHMimic/executor.ts +104 -18
  195. package/src/SSHMimic/index.ts +49 -15
  196. package/src/VirtualFileSystem/binaryPack.ts +35 -8
  197. package/src/VirtualFileSystem/index.ts +78 -28
  198. package/src/VirtualPackageManager/index.ts +208 -49
  199. package/src/VirtualShell/index.ts +35 -7
  200. package/src/VirtualShell/shell.ts +23 -3
  201. package/src/VirtualShell/shellParser.ts +134 -36
  202. package/src/VirtualUserManager/index.ts +7 -2
  203. package/src/commands/adduser.ts +6 -0
  204. package/src/commands/alias.ts +5 -1
  205. package/src/commands/apt.ts +47 -17
  206. package/src/commands/awk.ts +20 -6
  207. package/src/commands/base64.ts +13 -2
  208. package/src/commands/cat.ts +13 -5
  209. package/src/commands/cd.ts +5 -0
  210. package/src/commands/chmod.ts +5 -0
  211. package/src/commands/cp.ts +5 -0
  212. package/src/commands/curl.ts +56 -12
  213. package/src/commands/cut.ts +8 -1
  214. package/src/commands/date.ts +7 -1
  215. package/src/commands/declare.ts +44 -0
  216. package/src/commands/diff.ts +17 -3
  217. package/src/commands/dpkg.ts +33 -11
  218. package/src/commands/du.ts +17 -5
  219. package/src/commands/echo.ts +22 -9
  220. package/src/commands/env.ts +11 -1
  221. package/src/commands/exit.ts +12 -2
  222. package/src/commands/export.ts +3 -1
  223. package/src/commands/find.ts +5 -0
  224. package/src/commands/free.ts +9 -2
  225. package/src/commands/grep.ts +12 -2
  226. package/src/commands/gzip.ts +28 -4
  227. package/src/commands/head.ts +5 -0
  228. package/src/commands/help.ts +121 -47
  229. package/src/commands/history.ts +7 -2
  230. package/src/commands/hostname.ts +5 -0
  231. package/src/commands/id.ts +4 -1
  232. package/src/commands/index.ts +9 -360
  233. package/src/commands/ls.ts +5 -3
  234. package/src/commands/lsb-release.ts +8 -2
  235. package/src/commands/nano.ts +1 -1
  236. package/src/commands/neofetch.ts +1 -1
  237. package/src/commands/node.ts +341 -0
  238. package/src/commands/npm.ts +132 -0
  239. package/src/commands/ping.ts +6 -2
  240. package/src/commands/printf.ts +112 -0
  241. package/src/commands/ps.ts +21 -9
  242. package/src/commands/python.ts +2229 -0
  243. package/src/commands/read.ts +41 -0
  244. package/src/commands/registry.ts +244 -0
  245. package/src/commands/runtime.ts +353 -0
  246. package/src/commands/sed.ts +27 -9
  247. package/src/commands/set.ts +9 -3
  248. package/src/commands/sh.ts +159 -55
  249. package/src/commands/shift.ts +53 -0
  250. package/src/commands/sleep.ts +2 -1
  251. package/src/commands/sort.ts +10 -6
  252. package/src/commands/source.ts +15 -3
  253. package/src/commands/sudo.ts +1 -1
  254. package/src/commands/tar.ts +28 -7
  255. package/src/commands/tee.ts +7 -1
  256. package/src/commands/test.ts +61 -26
  257. package/src/commands/tr.ts +3 -1
  258. package/src/commands/true.ts +17 -0
  259. package/src/commands/type.ts +6 -3
  260. package/src/commands/uname.ts +5 -1
  261. package/src/commands/uniq.ts +8 -2
  262. package/src/commands/uptime.ts +4 -1
  263. package/src/commands/wget.ts +51 -12
  264. package/src/commands/which.ts +5 -2
  265. package/src/commands/xargs.ts +11 -2
  266. package/src/index.ts +23 -24
  267. package/src/modules/linuxRootfs.ts +233 -30
  268. package/src/standalone-wo-sftp.ts +38 -0
  269. package/src/utils/expand.ts +238 -0
  270. package/src/utils/vfsDiff.ts +275 -0
  271. package/standalone-wo-sftp.js +507 -0
  272. package/standalone-wo-sftp.js.map +7 -0
  273. package/standalone.js +253 -191
  274. package/standalone.js.map +4 -4
  275. package/tests/bun-test-shim.ts +9 -1
  276. package/tests/command-helpers.test.ts +1 -5
  277. package/tests/new-features.test.ts +415 -5
  278. package/tests/parser-executor.test.ts +27 -27
  279. package/tests/sftp.test.ts +122 -42
  280. package/tests/users.test.ts +23 -5
  281. package/CHANGELOG.md +0 -150
@@ -1 +1,9 @@
1
- export { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
1
+ export {
2
+ describe,
3
+ test,
4
+ expect,
5
+ beforeEach,
6
+ afterEach,
7
+ beforeAll,
8
+ afterAll,
9
+ } from "vitest";
@@ -1,9 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import {
3
- getArg,
4
- getFlag,
5
- ifFlag,
6
- } from "../src/commands/command-helpers";
2
+ import { getArg, getFlag, ifFlag } from "../src/commands/command-helpers";
7
3
 
8
4
  describe("command-helpers", () => {
9
5
  test("ifFlag detects plain and inline flag forms", () => {
@@ -254,7 +254,7 @@ describe("curl / wget (pure fetch)", () => {
254
254
  test("curl fetches real URL and returns body", async () => {
255
255
  const r = await client.exec("curl https://httpbin.org/get");
256
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);
257
+ expect([0, 1, 6, 22]).toContain(r.exitCode ?? -1);
258
258
  });
259
259
 
260
260
  test("curl -o saves to VFS", async () => {
@@ -479,7 +479,7 @@ describe("Bug fixes", () => {
479
479
  test("ls -a shows dotfiles", async () => {
480
480
  await c.exec("touch /tmp/.hidden && touch /tmp/visible");
481
481
  const normal = await c.exec("ls /tmp");
482
- const all = await c.exec("ls -a /tmp");
482
+ const all = await c.exec("ls -a /tmp");
483
483
  expect(normal.stdout).not.toContain(".hidden");
484
484
  expect(all.stdout).toContain(".hidden");
485
485
  expect(all.stdout).toContain("visible");
@@ -500,7 +500,7 @@ describe("Bug fixes", () => {
500
500
  await c.exec("chmod u+x /tmp/u.sh");
501
501
  const mode = shell2.vfs.stat("/tmp/u.sh").mode;
502
502
  expect(mode & 0o100).toBe(0o100); // user x set
503
- expect(mode & 0o010).toBe(0); // group x not set
503
+ expect(mode & 0o010).toBe(0); // group x not set
504
504
  });
505
505
 
506
506
  test("chmod go-r removes group+other read", async () => {
@@ -521,7 +521,9 @@ describe("Bug fixes", () => {
521
521
  // ping -c
522
522
  test("ping -c 2 sends exactly 2 packets", async () => {
523
523
  const r = await c.exec("ping -c 2 localhost");
524
- const dataLines = r.stdout?.split("\n").filter((l) => l.includes("icmp_seq="));
524
+ const dataLines = r.stdout
525
+ ?.split("\n")
526
+ .filter((l) => l.includes("icmp_seq="));
525
527
  expect(dataLines?.length).toBe(2);
526
528
  });
527
529
 
@@ -538,7 +540,9 @@ describe("Bug fixes", () => {
538
540
  });
539
541
 
540
542
  test("[ -f path ] returns 1 for non-existent file", async () => {
541
- const r = await c.exec("[ -f /tmp/doesnotexist999 ] && echo yes || echo no");
543
+ const r = await c.exec(
544
+ "[ -f /tmp/doesnotexist999 ] && echo yes || echo no",
545
+ );
542
546
  expect(r.stdout?.trim()).toBe("no");
543
547
  });
544
548
 
@@ -624,3 +628,409 @@ describe("Bug fixes", () => {
624
628
  expect(r.stdout?.trim()).toBe("3");
625
629
  });
626
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 { diffSnapshots, formatDiff, assertDiff } 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
+ });
@@ -4,34 +4,34 @@ import { executePipeline } from "../src/SSHMimic/executor";
4
4
  import { parseShellPipeline } from "../src/VirtualShell/shellParser";
5
5
 
6
6
  describe("Pipeline parser and executor", () => {
7
- test("parses simple pipeline", () => {
8
- const pipeline = parseShellPipeline("echo hello | grep h");
9
- expect(pipeline.isValid).toBe(true);
10
- expect(pipeline.commands).toHaveLength(2);
11
- expect(pipeline.commands[0]?.name).toBe("echo");
12
- expect(pipeline.commands[1]?.name).toBe("grep");
13
- });
7
+ test("parses simple pipeline", () => {
8
+ const pipeline = parseShellPipeline("echo hello | grep h");
9
+ expect(pipeline.isValid).toBe(true);
10
+ expect(pipeline.commands).toHaveLength(2);
11
+ expect(pipeline.commands[0]?.name).toBe("echo");
12
+ expect(pipeline.commands[1]?.name).toBe("grep");
13
+ });
14
14
 
15
- test("handles invalid syntax", () => {
16
- const pipeline = parseShellPipeline("echo hello |");
17
- expect(pipeline.isValid).toBe(false);
18
- expect(pipeline.error).toBeDefined();
19
- });
15
+ test("handles invalid syntax", () => {
16
+ const pipeline = parseShellPipeline("echo hello |");
17
+ expect(pipeline.isValid).toBe(false);
18
+ expect(pipeline.error).toBeDefined();
19
+ });
20
20
 
21
- test("executes simple pipeline", async () => {
22
- const shell = new VirtualShell("localhost");
23
- const pipeline = parseShellPipeline("echo hello | grep h");
21
+ test("executes simple pipeline", async () => {
22
+ const shell = new VirtualShell("localhost");
23
+ const pipeline = parseShellPipeline("echo hello | grep h");
24
24
 
25
- const result = await executePipeline(
26
- pipeline,
27
- "root",
28
- "localhost",
29
- "shell",
30
- "/",
31
- shell
32
- );
25
+ const result = await executePipeline(
26
+ pipeline,
27
+ "root",
28
+ "localhost",
29
+ "shell",
30
+ "/",
31
+ shell,
32
+ );
33
33
 
34
- expect(result.exitCode).toBe(0);
35
- expect(result.stdout).toContain("hello");
36
- });
37
- });
34
+ expect(result.exitCode).toBe(0);
35
+ expect(result.stdout).toContain("hello");
36
+ });
37
+ });