typescript-virtual-container 1.0.8 → 1.1.1-b

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 (54) hide show
  1. package/.vscode/settings.json +18 -0
  2. package/README.md +183 -92
  3. package/modules/shellInteractive.ts +45 -0
  4. package/modules/shellRuntime.ts +76 -0
  5. package/package.json +1 -1
  6. package/src/{SSHMimic/client.ts → SSHClient/index.ts} +17 -20
  7. package/src/SSHMimic/exec.ts +6 -17
  8. package/src/SSHMimic/executor.ts +20 -31
  9. package/src/SSHMimic/index.ts +23 -85
  10. package/src/VirtualFileSystem/index.ts +26 -1
  11. package/src/VirtualShell/index.ts +131 -26
  12. package/src/VirtualShell/shell.ts +43 -141
  13. package/src/VirtualShell/shellParser.ts +32 -7
  14. package/src/{SSHMimic/users.ts → VirtualUserManager/index.ts} +155 -3
  15. package/src/{VirtualShell/commands → commands}/adduser.ts +3 -3
  16. package/src/{VirtualShell/commands → commands}/cat.ts +4 -4
  17. package/src/{VirtualShell/commands → commands}/cd.ts +3 -3
  18. package/src/{VirtualShell/commands → commands}/clear.ts +1 -1
  19. package/src/{VirtualShell/commands → commands}/curl.ts +3 -3
  20. package/src/{VirtualShell/commands → commands}/deluser.ts +3 -3
  21. package/src/{VirtualShell/commands → commands}/echo.ts +1 -1
  22. package/src/{VirtualShell/commands → commands}/env.ts +1 -1
  23. package/src/{VirtualShell/commands → commands}/exit.ts +1 -1
  24. package/src/{VirtualShell/commands → commands}/export.ts +1 -1
  25. package/src/{VirtualShell/commands → commands}/grep.ts +3 -3
  26. package/src/{VirtualShell/commands → commands}/help.ts +1 -1
  27. package/src/{VirtualShell/commands → commands}/helpers.ts +1 -1
  28. package/src/{VirtualShell/commands → commands}/hostname.ts +1 -1
  29. package/src/{VirtualShell/commands → commands}/htop.ts +1 -1
  30. package/src/{VirtualShell/commands → commands}/index.ts +19 -110
  31. package/src/{VirtualShell/commands → commands}/ls.ts +7 -5
  32. package/src/{VirtualShell/commands → commands}/mkdir.ts +3 -3
  33. package/src/{VirtualShell/commands → commands}/nano.ts +4 -4
  34. package/src/{VirtualShell/commands → commands}/neofetch.ts +4 -4
  35. package/src/{VirtualShell/commands → commands}/pwd.ts +1 -1
  36. package/src/{VirtualShell/commands → commands}/rm.ts +3 -3
  37. package/src/{VirtualShell/commands → commands}/set.ts +1 -1
  38. package/src/{VirtualShell/commands → commands}/sh.ts +3 -14
  39. package/src/{VirtualShell/commands → commands}/su.ts +3 -2
  40. package/src/{VirtualShell/commands → commands}/sudo.ts +4 -7
  41. package/src/{VirtualShell/commands → commands}/touch.ts +4 -4
  42. package/src/{VirtualShell/commands → commands}/tree.ts +3 -3
  43. package/src/{VirtualShell/commands → commands}/unset.ts +1 -1
  44. package/src/{VirtualShell/commands → commands}/wget.ts +3 -3
  45. package/src/{VirtualShell/commands → commands}/who.ts +4 -4
  46. package/src/{VirtualShell/commands → commands}/whoami.ts +1 -1
  47. package/src/index.ts +6 -6
  48. package/src/standalone.ts +19 -14
  49. package/src/types/commands.ts +3 -11
  50. package/tests/command-helpers.test.ts +1 -1
  51. package/tests/helpers.test.ts +1 -1
  52. package/tests/parser-executor.test.ts +3 -6
  53. package/tests/users.test.ts +61 -1
  54. /package/src/{VirtualShell/commands → commands}/command-helpers.ts +0 -0
@@ -1,11 +1,11 @@
1
1
  import * as path from "node:path";
2
- import type { ShellModule } from "../../types/commands";
2
+ import type { ShellModule } from "../types/commands";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const nanoCommand: ShellModule = {
6
6
  name: "nano",
7
7
  params: ["<file>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  const fileArg = args[0];
10
10
  if (!fileArg) {
11
11
  return { stderr: "nano: missing file operand", exitCode: 1 };
@@ -13,8 +13,8 @@ export const nanoCommand: ShellModule = {
13
13
 
14
14
  const targetPath = resolvePath(cwd, fileArg);
15
15
  assertPathAccess(authUser, targetPath, "nano");
16
- const initialContent = vfs.exists(targetPath)
17
- ? vfs.readFile(targetPath)
16
+ const initialContent = shell.vfs.exists(targetPath)
17
+ ? shell.vfs.readFile(targetPath)
18
18
  : "";
19
19
  const safeName = path.posix.basename(targetPath) || "buffer";
20
20
  const tempPath = `/tmp/sshmimic-nano-${Date.now()}-${safeName}.tmp`;
@@ -1,12 +1,12 @@
1
- import { buildNeofetchOutput } from "../../../modules/neofetch";
2
- import type { ShellModule } from "../../types/commands";
1
+ import { buildNeofetchOutput } from "../../modules/neofetch";
2
+ import type { ShellModule } from "../types/commands";
3
3
  import { ifFlag } from "./command-helpers";
4
4
  import { getAllEnvVars } from "./set";
5
5
 
6
6
  export const neofetchCommand: ShellModule = {
7
7
  name: "neofetch",
8
8
  params: ["[--off]"],
9
- run: ({ args, authUser, hostname, shellProps }) => {
9
+ run: ({ args, authUser, hostname, shell }) => {
10
10
  const env = getAllEnvVars(authUser);
11
11
 
12
12
  if (ifFlag(args, "--help")) {
@@ -28,7 +28,7 @@ export const neofetchCommand: ShellModule = {
28
28
  user: authUser,
29
29
  host: hostname,
30
30
  shell: env.SHELL,
31
- shellProps: shellProps,
31
+ shellProps: shell.properties,
32
32
  terminal: env.TERM,
33
33
  }),
34
34
  exitCode: 0,
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const pwdCommand: ShellModule = {
4
4
  name: "pwd",
@@ -1,11 +1,11 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg, ifFlag } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const rmCommand: ShellModule = {
6
6
  name: "rm",
7
7
  params: ["[-r|-rf] <path>"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  if (args.length === 0) {
10
10
  return { stderr: "rm: missing operand", exitCode: 1 };
11
11
  }
@@ -27,7 +27,7 @@ export const rmCommand: ShellModule = {
27
27
  for (const target of targets) {
28
28
  const resolvedTarget = resolvePath(cwd, target);
29
29
  assertPathAccess(authUser, resolvedTarget, "rm");
30
- vfs.remove(resolvedTarget, { recursive });
30
+ shell.vfs.remove(resolvedTarget, { recursive });
31
31
  }
32
32
 
33
33
  return { exitCode: 0 };
@@ -1,5 +1,5 @@
1
1
  /** biome-ignore-all lint/style/useNamingConvention: env variables */
2
- import type { ShellModule } from "../../types/commands";
2
+ import type { ShellModule } from "../types/commands";
3
3
  import { getArg } from "./command-helpers";
4
4
 
5
5
  // Simple in-memory environment variables store
@@ -1,5 +1,4 @@
1
- import { defaultShellProperties } from "..";
2
- import type { CommandContext, ShellModule } from "../../types/commands";
1
+ import type { CommandContext, ShellModule } from "../types/commands";
3
2
  import { getArg, getFlag } from "./command-helpers";
4
3
  import { runCommand } from "./index";
5
4
 
@@ -9,8 +8,7 @@ export const shCommand: ShellModule = {
9
8
  params: ["-c <script>", "[<file>]"],
10
9
  aliases: ["bash"],
11
10
  run: async (ctx: CommandContext) => {
12
- const { vfs, args, authUser, hostname, users, mode, cwd } = ctx;
13
-
11
+ const { args, authUser, hostname, mode, cwd } = ctx;
14
12
  // Handle -c option: sh -c "command"
15
13
  if (getFlag(args, "-c") && args.length >= 2) {
16
14
  const script = getArg(args, 1) ?? "";
@@ -39,16 +37,7 @@ export const shCommand: ShellModule = {
39
37
 
40
38
  // Execute the command
41
39
  const result = await Promise.resolve(
42
- runCommand(
43
- command,
44
- authUser,
45
- hostname,
46
- users,
47
- mode,
48
- cwd,
49
- defaultShellProperties,
50
- vfs,
51
- ),
40
+ runCommand(command, authUser, hostname, mode, cwd, ctx.shell),
52
41
  );
53
42
 
54
43
  if (result.stdout) {
@@ -1,10 +1,11 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
 
4
4
  export const suCommand: ShellModule = {
5
5
  name: "su",
6
6
  params: ["- <username>"],
7
- run: ({ authUser, users, args }) => {
7
+ run: ({ authUser, shell, args }) => {
8
+ const users = shell.users!;
8
9
  const targetUser = getArg(args, 0, { flags: ["-"] });
9
10
 
10
11
  if (!targetUser) {
@@ -1,5 +1,4 @@
1
- import { defaultShellProperties } from "..";
2
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
3
2
  import { parseArgs } from "./command-helpers";
4
3
  import { runCommand } from "./index";
5
4
 
@@ -23,10 +22,10 @@ function parseSudoArgs(args: string[]): {
23
22
  export const sudoCommand: ShellModule = {
24
23
  name: "sudo",
25
24
  params: ["<command...>"],
26
- run: async ({ authUser, hostname, users, mode, cwd, vfs, args }) => {
25
+ run: async ({ authUser, hostname, mode, cwd, shell, args }) => {
27
26
  const { targetUser, loginShell, commandLine } = parseSudoArgs(args);
28
27
 
29
- if (authUser !== "root" && !users.isSudoer(authUser)) {
28
+ if (authUser !== "root" && !shell.users.isSudoer(authUser)) {
30
29
  return { stderr: "sudo: permission denied", exitCode: 1 };
31
30
  }
32
31
 
@@ -50,11 +49,9 @@ export const sudoCommand: ShellModule = {
50
49
  commandLine,
51
50
  effectiveUser,
52
51
  hostname,
53
- users,
54
52
  mode,
55
53
  loginShell ? `/home/${effectiveUser}` : cwd,
56
- defaultShellProperties,
57
- vfs,
54
+ shell,
58
55
  );
59
56
  }
60
57
 
@@ -1,10 +1,10 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { assertPathAccess, resolvePath } from "./helpers";
3
3
 
4
4
  export const touchCommand: ShellModule = {
5
5
  name: "touch",
6
6
  params: ["<file>"],
7
- run: ({ authUser, vfs, cwd, args }) => {
7
+ run: ({ authUser, shell, cwd, args }) => {
8
8
  if (args.length === 0) {
9
9
  return { stderr: "touch: missing file operand", exitCode: 1 };
10
10
  }
@@ -12,8 +12,8 @@ export const touchCommand: ShellModule = {
12
12
  for (const file of args) {
13
13
  const target = resolvePath(cwd, file);
14
14
  assertPathAccess(authUser, target, "touch");
15
- if (!vfs.exists(target)) {
16
- vfs.writeFile(target, "");
15
+ if (!shell.vfs.exists(target)) {
16
+ shell.writeFileAsUser(authUser, target, "");
17
17
  }
18
18
  }
19
19
  return { exitCode: 0 };
@@ -1,13 +1,13 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { getArg } from "./command-helpers";
3
3
  import { assertPathAccess, resolvePath } from "./helpers";
4
4
 
5
5
  export const treeCommand: ShellModule = {
6
6
  name: "tree",
7
7
  params: ["[path]"],
8
- run: ({ authUser, vfs, cwd, args }) => {
8
+ run: ({ authUser, shell, cwd, args }) => {
9
9
  const target = resolvePath(cwd, getArg(args, 0) ?? cwd);
10
10
  assertPathAccess(authUser, target, "tree");
11
- return { stdout: vfs.tree(target), exitCode: 0 };
11
+ return { stdout: shell.vfs.tree(target), exitCode: 0 };
12
12
  },
13
13
  };
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
  import { setEnvVar } from "./set";
3
3
 
4
4
  export const unsetCommand: ShellModule = {
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { mkdtemp, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import type { ShellModule } from "../../types/commands";
5
+ import type { ShellModule } from "../types/commands";
6
6
  import { ifFlag, parseArgs } from "./command-helpers";
7
7
  import {
8
8
  assertPathAccess,
@@ -83,7 +83,7 @@ function runHostWget(args: string[]): Promise<{
83
83
  export const wgetCommand: ShellModule = {
84
84
  name: "wget",
85
85
  params: ["[url]"],
86
- run: async ({ authUser, vfs, cwd, args }) => {
86
+ run: async ({ authUser, cwd, args, shell }) => {
87
87
  const { flagsWithValues, positionals } = parseArgs(args, {
88
88
  flagsWithValue: ["-o", "-O", "--output", "--output-document"],
89
89
  });
@@ -131,7 +131,7 @@ export const wgetCommand: ShellModule = {
131
131
  const content = await readFile(tempFile, "utf8");
132
132
  const target = resolvePath(cwd, outputPath || stripUrlFilename(url));
133
133
  assertPathAccess(authUser, target, "wget");
134
- vfs.writeFile(target, content);
134
+ shell.writeFileAsUser(authUser, target, content);
135
135
 
136
136
  return {
137
137
  stdout: `saved ${target}`,
@@ -1,11 +1,11 @@
1
- import { formatLoginDate } from "../../SSHMimic/loginFormat";
2
- import type { ShellModule } from "../../types/commands";
1
+ import { formatLoginDate } from "../SSHMimic/loginFormat";
2
+ import type { ShellModule } from "../types/commands";
3
3
 
4
4
  export const whoCommand: ShellModule = {
5
5
  name: "who",
6
6
  params: [],
7
- run: ({ users }) => {
8
- const lines = users.listActiveSessions().map((session) => {
7
+ run: ({ shell }) => {
8
+ const lines = shell.users.listActiveSessions().map((session) => {
9
9
  const loginAt = new Date(session.startedAt);
10
10
  const displayDate = Number.isNaN(loginAt.getTime())
11
11
  ? session.startedAt
@@ -1,4 +1,4 @@
1
- import type { ShellModule } from "../../types/commands";
1
+ import type { ShellModule } from "../types/commands";
2
2
 
3
3
  export const whoamiCommand: ShellModule = {
4
4
  name: "whoami",
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { SshClient } from "./SSHMimic/client";
1
+ import { SshClient } from "./SSHClient";
2
2
  import { SshMimic } from "./SSHMimic/index";
3
- import { VirtualUserManager } from "./SSHMimic/users";
4
3
  import VirtualFileSystem from "./VirtualFileSystem";
5
- import type { VirtualShell } from "./VirtualShell";
4
+ import { VirtualShell } from "./VirtualShell";
5
+ import { VirtualUserManager } from "./VirtualUserManager";
6
6
 
7
7
  export type {
8
8
  CommandContext,
@@ -32,13 +32,13 @@ export type {
32
32
  export {
33
33
  SshClient,
34
34
  VirtualFileSystem,
35
- SshMimic as VirtualMachine,
35
+ VirtualShell,
36
+ SshMimic as VirtualSshServer,
36
37
  VirtualUserManager,
37
- type VirtualShell,
38
38
  };
39
39
 
40
40
  export {
41
41
  getArg,
42
42
  getFlag,
43
43
  ifFlag,
44
- } from "./VirtualShell/commands/command-helpers";
44
+ } from "./commands/command-helpers";
package/src/standalone.ts CHANGED
@@ -1,21 +1,26 @@
1
- import { VirtualMachine } from ".";
1
+ import { VirtualShell, VirtualSshServer } from ".";
2
2
 
3
- const sshHostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
- const sshMimic = new VirtualMachine({ port: 2222, hostname: sshHostname });
3
+ const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
+ const virtualShell = new VirtualShell(hostname);
5
5
 
6
- sshMimic
6
+ virtualShell.addCommand("demo", [], () => {
7
+ return {
8
+ stdout: "This is a demo command. It does nothing useful.",
9
+ exitCode: 0,
10
+ };
11
+ });
12
+
13
+ new VirtualSshServer({
14
+ port: 2222,
15
+ hostname,
16
+ shell: virtualShell,
17
+ })
7
18
  .start()
8
19
  .then((port: number) => {
9
- if (!sshMimic.shell) console.error("Failed to initialize SSH Mimic shell.");
10
- else {
11
- console.log(`SSH Mimic initialized. Listening on port ${port}.`);
12
- sshMimic.shell.addCommand("demo", [], () => {
13
- return {
14
- stdout: "This is a demo command. It does nothing useful.",
15
- exitCode: 0,
16
- };
17
- });
18
- }
20
+ // if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
21
+ // else {
22
+ console.log(`SSH Mimic initialized. Listening on port ${port}.`);
23
+ // }
19
24
  })
20
25
  .catch((error: unknown) => {
21
26
  console.error("Failed to start SSH Mimic:", error);
@@ -1,12 +1,8 @@
1
1
  /** Command invocation mode used by shell runtime. */
2
2
  export type CommandMode = "shell" | "exec";
3
3
 
4
- import type {
5
- VirtualActiveSession,
6
- VirtualUserManager,
7
- } from "../SSHMimic/users";
8
- import type VirtualFileSystem from "../VirtualFileSystem";
9
- import type { ShellProperties } from "../VirtualShell";
4
+ import type { VirtualShell } from "../VirtualShell";
5
+ import type { VirtualActiveSession } from "../VirtualUserManager";
10
6
 
11
7
  /**
12
8
  * Normalized command execution output.
@@ -67,8 +63,6 @@ export interface CommandContext {
67
63
  authUser: string;
68
64
  /** Virtual hostname shown in prompt and banners. */
69
65
  hostname: string;
70
- /** User and session manager instance. */
71
- users: VirtualUserManager;
72
66
  /** Snapshot of currently active user sessions. */
73
67
  activeSessions: VirtualActiveSession[];
74
68
  /** Original unparsed command line input. */
@@ -78,13 +72,11 @@ export interface CommandContext {
78
72
  /** Tokenized arguments excluding command name. */
79
73
  args: string[];
80
74
  /** Virtual shell instance. */
81
- shellProps: ShellProperties;
75
+ shell: VirtualShell;
82
76
  /** Optional stdin payload (used by pipes/redirections). */
83
77
  stdin?: string;
84
78
  /** Current working directory for command execution. */
85
79
  cwd: string;
86
- /** Virtual filesystem instance for IO operations. */
87
- vfs: VirtualFileSystem;
88
80
  }
89
81
 
90
82
  /** Contract implemented by each shell command module. */
@@ -3,7 +3,7 @@ import {
3
3
  getArg,
4
4
  getFlag,
5
5
  ifFlag,
6
- } from "../src/VirtualShell/commands/command-helpers";
6
+ } from "../src/commands/command-helpers";
7
7
 
8
8
  describe("command-helpers", () => {
9
9
  test("ifFlag detects plain and inline flag forms", () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { assertPathAccess } from "../src/VirtualShell/commands/helpers";
2
+ import { assertPathAccess } from "../src/commands/helpers";
3
3
 
4
4
  describe("assertPathAccess", () => {
5
5
  test("blocks non-root access to auth store", () => {
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { VirtualShell } from "../src";
2
3
  import { executePipeline } from "../src/SSHMimic/executor";
3
- import { VirtualUserManager } from "../src/SSHMimic/users";
4
- import VirtualFileSystem from "../src/VirtualFileSystem";
5
4
  import { parseShellPipeline } from "../src/VirtualShell/shellParser";
6
5
 
7
6
  describe("Pipeline parser and executor", () => {
@@ -20,18 +19,16 @@ describe("Pipeline parser and executor", () => {
20
19
  });
21
20
 
22
21
  test("executes simple pipeline", async () => {
23
- const vfs = new VirtualFileSystem("/tmp");
24
- const users = new VirtualUserManager(vfs, "root-pass");
22
+ const shell = new VirtualShell("localhost");
25
23
  const pipeline = parseShellPipeline("echo hello | grep h");
26
24
 
27
25
  const result = await executePipeline(
28
26
  pipeline,
29
27
  "root",
30
28
  "localhost",
31
- users,
32
29
  "shell",
33
30
  "/",
34
- vfs,
31
+ shell
35
32
  );
36
33
 
37
34
  expect(result.exitCode).toBe(0);
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
2
2
  import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { VirtualUserManager } from "../src/SSHMimic/users";
6
5
  import VirtualFileSystem from "../src/VirtualFileSystem";
6
+ import { VirtualUserManager } from "../src/VirtualUserManager";
7
7
 
8
8
  async function withTempVfs(
9
9
  run: (vfs: VirtualFileSystem) => Promise<void>,
@@ -39,3 +39,63 @@ describe("VirtualUserManager auto sudo", () => {
39
39
  });
40
40
  });
41
41
  });
42
+
43
+ describe("VirtualUserManager quotas", () => {
44
+ test("enforces quota for writes inside user home", async () => {
45
+ await withTempVfs(async (vfs) => {
46
+ const users = new VirtualUserManager(vfs, "root-pass");
47
+ await users.initialize();
48
+ await users.addUser("alice", "alice-pass");
49
+ const startingUsage = users.getUsageBytes("alice");
50
+ await users.setQuotaBytes("alice", startingUsage + 5);
51
+
52
+ expect(() => {
53
+ users.assertWriteWithinQuota("alice", "/home/alice/note.txt", "hello");
54
+ }).not.toThrow();
55
+
56
+ vfs.writeFile("/home/alice/note.txt", "hello");
57
+
58
+ expect(() => {
59
+ users.assertWriteWithinQuota(
60
+ "alice",
61
+ "/home/alice/note.txt",
62
+ "this exceeds the configured quota",
63
+ );
64
+ }).toThrow("quota exceeded for 'alice'");
65
+ });
66
+ });
67
+
68
+ test("does not enforce home quota outside user home", async () => {
69
+ await withTempVfs(async (vfs) => {
70
+ const users = new VirtualUserManager(vfs, "root-pass");
71
+ await users.initialize();
72
+ await users.addUser("bob", "bob-pass");
73
+ await users.setQuotaBytes("bob", 1);
74
+
75
+ expect(() => {
76
+ users.assertWriteWithinQuota("bob", "/tmp/shared.txt", "large-content");
77
+ }).not.toThrow();
78
+ });
79
+ });
80
+
81
+ test("clearQuota removes enforced limit", async () => {
82
+ await withTempVfs(async (vfs) => {
83
+ const users = new VirtualUserManager(vfs, "root-pass");
84
+ await users.initialize();
85
+ await users.addUser("charlie", "charlie-pass");
86
+ await users.setQuotaBytes("charlie", 2);
87
+
88
+ expect(users.getQuotaBytes("charlie")).toBe(2);
89
+ await users.clearQuota("charlie");
90
+ expect(users.getQuotaBytes("charlie")).toBeNull();
91
+
92
+ expect(() => {
93
+ users.assertWriteWithinQuota(
94
+ "charlie",
95
+ "/home/charlie/file.txt",
96
+ "long-content",
97
+ );
98
+ }).not.toThrow();
99
+ });
100
+ });
101
+ });