import { describe, expect, it } from "vitest"; import { GiB, InterfaceOverridesBuilder, intoRootfsSource, InvalidConfigError, MiB, MountBuilder, NetworkBuilder, PatchBuilder, Sandbox, SecretBuilder, Stdin, } from "../../dist/index.js"; describe("intoRootfsSource", () => { it("treats absolute paths as bind mounts", () => { expect(intoRootfsSource("/srv/rootfs")).toEqual({ kind: "bind", path: "/srv/rootfs", }); }); it("treats relative paths as bind mounts", () => { expect(intoRootfsSource("./rootfs")).toEqual({ kind: "bind", path: "./rootfs", }); }); it("recognises disk-image extensions regardless of leading slash", () => { expect(intoRootfsSource("./alpine.qcow2")).toEqual({ kind: "disk", path: "./alpine.qcow2", format: "qcow2", }); expect(intoRootfsSource("foo.raw")).toEqual({ kind: "disk", path: "foo.raw", format: "raw", }); }); it("falls back to OCI references", () => { expect(intoRootfsSource("python:3.12")).toEqual({ kind: "oci", reference: "python:3.12", }); }); it("passes through structured RootfsSource values", () => { const src = { kind: "oci" as const, reference: "alpine" }; expect(intoRootfsSource(src)).toBe(src); }); }); describe("MountBuilder", () => { it("builds a bind mount with default writeable flag", () => { const m = new MountBuilder("/data").bind("/host/data").build(); expect(m).toEqual({ kind: "bind", host: "/host/data", guest: "/data", readonly: false, noexec: false, nosuid: false, nodev: false, statVirtualization: "strict", hostPermissions: "private", }); }); it("builds a tmpfs mount with size and uniform readonly", () => { const m = new MountBuilder("/scratch") .tmpfs() .size(MiB(64)) .readonly() .build(); expect(m).toEqual({ kind: "tmpfs", guest: "/scratch", sizeMib: 64, readonly: true, noexec: false, nosuid: false, nodev: false, }); }); it("propagates noexec across mount kinds", () => { expect( new MountBuilder("/data").bind("/host/data").noexec().build(), ).toMatchObject({ kind: "bind", noexec: true }); expect(new MountBuilder("/cache").named("v1").noexec().build()).toMatchObject( { kind: "named", noexec: true }, ); expect(new MountBuilder("/scratch").tmpfs().noexec().build()).toMatchObject({ kind: "tmpfs", noexec: true, }); expect( new MountBuilder("/seed").disk("./fixture.qcow2").noexec().build(), ).toMatchObject({ kind: "disk", noexec: true }); }); it("propagates nosuid and nodev across mount kinds", () => { expect( new MountBuilder("/data").bind("/host/data").nosuid().nodev().build(), ).toMatchObject({ kind: "bind", nosuid: true, nodev: true }); expect(new MountBuilder("/cache").named("v1").nosuid().nodev().build()).toMatchObject( { kind: "named", nosuid: true, nodev: true }, ); expect(new MountBuilder("/scratch").tmpfs().nosuid().nodev().build()).toMatchObject({ kind: "tmpfs", nosuid: true, nodev: true, }); expect( new MountBuilder("/seed").disk("./fixture.qcow2").nosuid().nodev().build(), ).toMatchObject({ kind: "disk", nosuid: true, nodev: true }); }); it("auto-infers disk format from the host extension", () => { const m = new MountBuilder("/seed") .disk("./fixture.qcow2") .fstype("ext4") .readonly() .build(); expect(m).toMatchObject({ kind: "disk", host: "./fixture.qcow2", format: "qcow2", fstype: "ext4", readonly: true, }); }); it("rejects .size() on a non-tmpfs mount", () => { const builder = new MountBuilder("/data").bind("/host").size(MiB(10)); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects .format() on a non-disk mount", () => { const builder = new MountBuilder("/data").bind("/host").format("qcow2"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects .fstype() on a non-disk mount", () => { const builder = new MountBuilder("/data").bind("/host").fstype("ext4"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects unset mount kind", () => { expect(() => new MountBuilder("/data").build()).toThrow(InvalidConfigError); }); it("rejects fstypes containing forbidden separators", () => { const builder = new MountBuilder("/data") .disk("./d.raw") .fstype("ext4,foo"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects relative guest paths", () => { const builder = new MountBuilder("data").bind("/host"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects guest paths containing : or ;", () => { const builder = new MountBuilder("/foo:bar").bind("/host"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("defaults bind mount to strict + private when policies are not set", () => { const m = new MountBuilder("/data").bind("/host/data").build(); expect(m.statVirtualization).toBe("strict"); expect(m.hostPermissions).toBe("private"); }); it("propagates explicit stat-virt and host-perms on a bind mount", () => { const m = new MountBuilder("/work") .bind("./project") .statVirtualization("relaxed") .hostPermissions("mirror") .build(); expect(m).toMatchObject({ kind: "bind", statVirtualization: "relaxed", hostPermissions: "mirror", }); }); it("propagates stat-virt + host-perms on a named volume", () => { const m = new MountBuilder("/cache") .named("my-cache") .statVirtualization("off") .build(); expect(m).toMatchObject({ kind: "named", name: "my-cache", statVirtualization: "off", hostPermissions: "private", }); }); it("rejects unknown stat-virt strings at the FFI boundary", () => { expect(() => new MountBuilder("/data").bind("/host").statVirtualization("bogus"), ).toThrow(/invalid stat_virtualization/); }); it("rejects unknown host-perms strings at the FFI boundary", () => { expect(() => new MountBuilder("/data").bind("/host").hostPermissions("public"), ).toThrow(/invalid host_permissions/); }); it("rejects stat-virt on a tmpfs mount at build time", () => { const builder = new MountBuilder("/scratch") .tmpfs() .statVirtualization("relaxed"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects host-perms on a disk mount at build time", () => { const builder = new MountBuilder("/data") .disk("./d.raw") .hostPermissions("mirror"); expect(() => builder.build()).toThrow(InvalidConfigError); }); it("rejects Off + Mirror at build time", () => { const builder = new MountBuilder("/data") .bind("/host") .statVirtualization("off") .hostPermissions("mirror"); expect(() => builder.build()).toThrow(/Off cannot be combined with/); }); it("rejects commas in bind host paths at build time", () => { const builder = new MountBuilder("/data").bind("/host/with,comma"); expect(() => builder.build()).toThrow(/must not contain ','/); }); }); describe("PatchBuilder", () => { it("collects patches in declaration order", () => { const patches = new PatchBuilder() .text("/etc/cfg", "x", { mode: 0o644 }) .mkdir("/var/cache", { mode: 0o755 }) .copyFile("./host.pem", "/etc/cert.pem", { replace: true }) .build(); expect(patches).toHaveLength(3); expect(patches[0]).toMatchObject({ kind: "text", mode: 0o644 }); expect(patches[1]).toMatchObject({ kind: "mkdir", mode: 0o755 }); expect(patches[2]).toMatchObject({ kind: "copyFile", replace: true }); }); }); describe("SandboxBuilder.build", () => { it("requires .image()", async () => { await expect(Sandbox.builder("x").build()).rejects.toThrow( InvalidConfigError, ); }); it("renders branded sizes back to plain numbers", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .memory(GiB(2)) .build(); expect((cfg.resources as { memoryMib: number }).memoryMib).toBe(2048); }); it("collects volumes through the MountBuilder callback", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .volume("/data", (m) => m.named("v1").readonly()) .volume("/tmp", (m) => m.tmpfs().size(MiB(64))) .build(); expect(cfg.mounts).toHaveLength(2); // The Rust VolumeMount enum serializes externally-tagged with a shared // options object for mount behavior. expect(cfg.mounts[0]).toMatchObject({ type: "Named", name: "v1", options: { readonly: true }, }); expect(cfg.mounts[1]).toMatchObject({ type: "Tmpfs", sizeMib: 64, }); }); it("sets the restricted security profile", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .security("restricted") .build(); expect(cfg.securityProfile).toBe("restricted"); }); it("sets lifecycle ephemeral policy", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .ephemeral(true) .build(); expect((cfg.lifecycle as { ephemeral: boolean }).ephemeral).toBe(true); }); it("invalid volume invocations defer to .build() / .create()", async () => { const builder = Sandbox.builder("x") .image("alpine") .volume("/bad", (m) => m.bind("/host").size(MiB(1))); await expect(builder.build()).rejects.toThrow(InvalidConfigError); }); it("defaults metricsSampleIntervalMs to 1000", async () => { const cfg = await Sandbox.builder("x").image("alpine").build(); expect((cfg.runtime as { metricsSampleIntervalMs: number }).metricsSampleIntervalMs).toBe( 1000, ); }); it("metricsSampleIntervalMs sets the persisted value", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .metricsSampleIntervalMs(5000) .build(); expect((cfg.runtime as { metricsSampleIntervalMs: number }).metricsSampleIntervalMs).toBe( 5000, ); }); it("metricsSampleIntervalMs(0) disables sampling", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .metricsSampleIntervalMs(0) .build(); expect((cfg.runtime as { metricsSampleIntervalMs: number | null }).metricsSampleIntervalMs).toBe( null, ); }); it("disableMetricsSample overrides metricsSampleIntervalMs", async () => { const cfg = await Sandbox.builder("x") .image("alpine") .metricsSampleIntervalMs(5000) .disableMetricsSample() .build(); expect((cfg.runtime as { metricsSampleIntervalMs: number }).metricsSampleIntervalMs).toBe( 5000, ); expect((cfg.runtime as { disableMetricsSample: boolean }).disableMetricsSample).toBe(true); }); }); describe("InterfaceOverridesBuilder", () => { it("constructs cleanly and accepts MTU + IPv4 + IPv6 + MAC", () => { const b = new InterfaceOverridesBuilder() .mtu(9000) .ipv4("172.16.0.5") .ipv6("fd42:6d73:62::5") .mac("aa:bb:cc:dd:ee:ff"); expect(b).toBeInstanceOf(InterfaceOverridesBuilder); }); it("MTU out of range rejected at the call", () => { expect(() => new InterfaceOverridesBuilder().mtu(70_000)).toThrow(); }); it("invalid IPv4 surfaces at .interface() drain", () => { expect(() => new NetworkBuilder().interface((io) => io.ipv4("999.999.999.999")), ).toThrow(/invalid IPv4/); }); it("invalid MAC surfaces", () => { expect(() => new NetworkBuilder().interface((io) => io.mac("zz:zz:zz:zz:zz:zz")), ).toThrow(/MAC/); }); it("valid overrides flow through NetworkBuilder.build()", () => { const cfg = new NetworkBuilder() .interface((io) => io.mtu(9000).ipv4("172.16.0.5")) .ipv4Pool("172.31.240.0/24") .ipv6Pool("fd7a:115c:a1e0:100::/56") .build() as { interface: { mtu: number; ipv4Address: string; ipv4Pool: string; ipv6Pool: string; }; }; expect(cfg.interface.mtu).toBe(9000); expect(cfg.interface.ipv4Address).toBe("172.16.0.5"); expect(cfg.interface.ipv4Pool).toBe("172.31.240.0/24"); expect(cfg.interface.ipv6Pool).toBe("fd7a:115c:a1e0:100::/56"); }); }); describe("NetworkBuilder.secretEnvSimple (3-arg shorthand)", () => { it("accepts the 3-arg form mirroring Rust core", () => { const cfg = new NetworkBuilder() .secretEnvSimple("API_KEY", "sk-abc", "api.example.com") .build() as { secrets: { secrets: ReadonlyArray<{ envVar: string; placeholder: string }>; }; }; expect(cfg.secrets.secrets).toHaveLength(1); expect(cfg.secrets.secrets[0].envVar).toBe("API_KEY"); // Placeholder defaults to the value when omitted. expect(cfg.secrets.secrets[0].placeholder).toBe("sk-abc"); }); }); describe("NetworkBuilder secret passthrough", () => { it("builds global passthrough violation policy", () => { const cfg = new NetworkBuilder() .onSecretViolation((v) => v .blockAndTerminate() .passthroughHost("api.anthropic.com") .passthroughHostPattern("*.anthropic.com"), ) .build() as { secrets: { onViolation: { passthrough: unknown[]; }; }; }; expect(cfg.secrets.onViolation).toEqual({ passthrough: [ { exact: "api.anthropic.com" }, { wildcard: "*.anthropic.com" }, ], }); }); it("builds per-secret passthrough violation policy", () => { const secret = new SecretBuilder() .env("API_KEY") .value("sk-abc") .allowHost("api.github.com") .onViolation((v) => v .blockAndLog() .passthroughHost("api.anthropic.com") .passthroughHostPattern("*.anthropic.com"), ) .build(); expect(secret.allowedHosts).toEqual(["api.github.com"]); }); }); describe("NetworkBuilder ports", () => { it("keeps loopback default and supports explicit bind addresses", () => { const cfg = new NetworkBuilder() .port(8080, 80) .portBind("0.0.0.0", 8081, 81) .portUdpBind("::", 5353, 53) .build() as { ports: ReadonlyArray<{ hostBind: string; hostPort: number; guestPort: number; protocol: "tcp" | "udp"; }>; }; expect(cfg.ports[0]).toMatchObject({ hostBind: "127.0.0.1", hostPort: 8080, guestPort: 80, protocol: "tcp", }); expect(cfg.ports[1]).toMatchObject({ hostBind: "0.0.0.0", hostPort: 8081, guestPort: 81, protocol: "tcp", }); expect(cfg.ports[2]).toMatchObject({ hostBind: "::", hostPort: 5353, guestPort: 53, protocol: "udp", }); }); }); describe("Stdin factory", () => { it("emits the right discriminants", () => { expect(Stdin.null()).toEqual({ kind: "null" }); expect(Stdin.pipe()).toEqual({ kind: "pipe" }); const bytes = Stdin.bytes("hello"); expect(bytes).toMatchObject({ kind: "bytes" }); if (bytes.kind === "bytes") { expect(new TextDecoder().decode(bytes.data)).toBe("hello"); } }); });