mirror of
https://github.com/hanzoai/vm.git
synced 2026-10-01 00:47:56 +00:00
Crates keep their history: vm (lib, was shuru-vm), hanzo-vm (CLI package and binary, was shuru-cli), vm-guest, vm-proto, vm-proxy, vm-sdk, vm-store, vm-darwin, vm-linux; all at 0.1.0, Apache-2.0, hanzoai/vm. Config is vm.json, data lives in ~/.hanzo/vm (HANZO_VM_HOME), env vars are HANZO_VM_*, the guest runs as /usr/bin/vm-guest, and assets download from hanzoai/vm releases. The TypeScript SDK is @hanzo/vm. NOTICE credits shuru as the origin. prepare-rootfs.sh builds the image natively on arm64 Linux with mke2fs -d and re-execs in Docker only on macOS. Includes rustfmt over the tree and fixes for clippy 1.98 lints. Co-authored-by: Hanzo Dev <[email protected]>
222 lines
4.9 KiB
TypeScript
222 lines
4.9 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import { resolve } from "node:path";
|
|
import { Sandbox } from "../../src/sandbox";
|
|
import type { FileChangeEvent } from "../../src/types";
|
|
|
|
const MOCK_BIN = `bun ${resolve(import.meta.dir, "mock-vm.ts")}`;
|
|
|
|
describe("spawn", () => {
|
|
let sb: Sandbox;
|
|
|
|
beforeAll(async () => {
|
|
sb = await Sandbox.start({ bin: MOCK_BIN });
|
|
}, 10_000);
|
|
|
|
afterAll(async () => {
|
|
await sb?.stop();
|
|
}, 5_000);
|
|
|
|
test("streams stdout chunks", async () => {
|
|
const proc = await sb.spawn("stream-test");
|
|
const chunks: string[] = [];
|
|
|
|
proc.on("stdout", (data) => {
|
|
chunks.push(data.toString());
|
|
});
|
|
|
|
const code = await proc.exited;
|
|
expect(code).toBe(0);
|
|
expect(chunks.join("")).toBe("chunk1\nchunk2\nchunk3\n");
|
|
});
|
|
|
|
test("streams stderr", async () => {
|
|
const proc = await sb.spawn("stream-test");
|
|
const stderrChunks: string[] = [];
|
|
|
|
proc.on("stderr", (data) => {
|
|
stderrChunks.push(data.toString());
|
|
});
|
|
|
|
await proc.exited;
|
|
expect(stderrChunks.join("")).toBe("warn\n");
|
|
});
|
|
|
|
test("exit event fires", async () => {
|
|
const proc = await sb.spawn("stream-test");
|
|
let exitCode = -1;
|
|
|
|
proc.on("exit", (code) => {
|
|
exitCode = code;
|
|
});
|
|
|
|
const code = await proc.exited;
|
|
expect(code).toBe(0);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("non-zero exit code", async () => {
|
|
const proc = await sb.spawn("exit-42");
|
|
const code = await proc.exited;
|
|
expect(code).toBe(42);
|
|
});
|
|
|
|
test("passes cwd to spawn", async () => {
|
|
const proc = await sb.spawn("cwd-test", { cwd: "/workspace" });
|
|
const chunks: string[] = [];
|
|
|
|
proc.on("stdout", (data) => {
|
|
chunks.push(data.toString());
|
|
});
|
|
|
|
await proc.exited;
|
|
expect(chunks.join("")).toBe("cwd=/workspace\n");
|
|
});
|
|
|
|
test("has pid", async () => {
|
|
const proc = await sb.spawn("stream-test");
|
|
expect(proc.pid).toMatch(/^p\d+$/);
|
|
await proc.exited;
|
|
});
|
|
});
|
|
|
|
describe("kill", () => {
|
|
let sb: Sandbox;
|
|
|
|
beforeAll(async () => {
|
|
sb = await Sandbox.start({ bin: MOCK_BIN });
|
|
}, 10_000);
|
|
|
|
afterAll(async () => {
|
|
await sb?.stop();
|
|
}, 5_000);
|
|
|
|
test("kills a running process", async () => {
|
|
const proc = await sb.spawn("long-running");
|
|
const chunks: string[] = [];
|
|
|
|
proc.on("stdout", (data) => {
|
|
chunks.push(data.toString());
|
|
});
|
|
|
|
// Let it run a bit
|
|
await Bun.sleep(120);
|
|
|
|
await proc.kill();
|
|
const code = await proc.exited;
|
|
expect(code).toBe(137);
|
|
expect(chunks.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("watch", () => {
|
|
let sb: Sandbox;
|
|
|
|
beforeAll(async () => {
|
|
sb = await Sandbox.start({ bin: MOCK_BIN });
|
|
}, 10_000);
|
|
|
|
afterAll(async () => {
|
|
await sb?.stop();
|
|
}, 5_000);
|
|
|
|
test("receives all event types", async () => {
|
|
const events: FileChangeEvent[] = [];
|
|
|
|
await sb.watch("/workspace", (event) => {
|
|
events.push(event);
|
|
});
|
|
|
|
// Wait for mock to send all events
|
|
await Bun.sleep(150);
|
|
|
|
expect(events.length).toBe(4);
|
|
expect(events[0]).toEqual({
|
|
path: "/workspace/src/main.ts",
|
|
event: "modify",
|
|
});
|
|
expect(events[1]).toEqual({
|
|
path: "/workspace/src/new.ts",
|
|
event: "create",
|
|
});
|
|
expect(events[2]).toEqual({
|
|
path: "/workspace/src/old.ts",
|
|
event: "delete",
|
|
});
|
|
expect(events[3]).toEqual({
|
|
path: "/workspace/src/renamed.ts",
|
|
event: "rename",
|
|
});
|
|
});
|
|
|
|
test("events include full path from watched root", async () => {
|
|
const events: FileChangeEvent[] = [];
|
|
|
|
await sb.watch("/custom/path", (event) => {
|
|
events.push(event);
|
|
});
|
|
|
|
await Bun.sleep(150);
|
|
|
|
// All paths should be relative to the watched root
|
|
for (const evt of events) {
|
|
expect(evt.path.startsWith("/custom/path/")).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("watch resolves immediately without blocking", async () => {
|
|
const start = Date.now();
|
|
await sb.watch("/fast", () => {});
|
|
const elapsed = Date.now() - start;
|
|
// watch() should return immediately (< 1s), not block waiting for events
|
|
expect(elapsed).toBeLessThan(1000);
|
|
});
|
|
});
|
|
|
|
describe("exec still works", () => {
|
|
let sb: Sandbox;
|
|
|
|
beforeAll(async () => {
|
|
sb = await Sandbox.start({ bin: MOCK_BIN });
|
|
}, 10_000);
|
|
|
|
afterAll(async () => {
|
|
await sb?.stop();
|
|
}, 5_000);
|
|
|
|
test("buffered exec returns full result", async () => {
|
|
const result = await sb.exec("echo hello");
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.stdout).toBe("hello\n");
|
|
});
|
|
});
|
|
|
|
describe("multiple concurrent spawns", () => {
|
|
let sb: Sandbox;
|
|
|
|
beforeAll(async () => {
|
|
sb = await Sandbox.start({ bin: MOCK_BIN });
|
|
}, 10_000);
|
|
|
|
afterAll(async () => {
|
|
await sb?.stop();
|
|
}, 5_000);
|
|
|
|
test("two spawns run concurrently", async () => {
|
|
const proc1 = await sb.spawn("stream-test");
|
|
const proc2 = await sb.spawn("exit-42");
|
|
|
|
const [code1, code2] = await Promise.all([proc1.exited, proc2.exited]);
|
|
expect(code1).toBe(0);
|
|
expect(code2).toBe(42);
|
|
});
|
|
|
|
test("each spawn gets unique pid", async () => {
|
|
const proc1 = await sb.spawn("stream-test");
|
|
const proc2 = await sb.spawn("stream-test");
|
|
|
|
expect(proc1.pid).not.toBe(proc2.pid);
|
|
|
|
await Promise.all([proc1.exited, proc2.exited]);
|
|
});
|
|
});
|