Files
zeekayandHanzo Dev 51a68b9311 Rebrand shuru as Hanzo VM
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]>
2026-09-01 11:49:10 -07:00

322 lines
7.2 KiB
TypeScript

/**
* Mock hanzo-vm binary for unit testing the SDK's streaming/notification features.
* Speaks the same JSON-RPC 2.0 protocol as the real CLI --stdio mode.
*/
const decoder = new TextDecoder();
function writeLine(obj: unknown) {
const line = `${JSON.stringify(obj)}\n`;
process.stdout.write(line);
}
// Send ready notification
writeLine({ jsonrpc: "2.0", method: "ready" });
const reader = Bun.stdin.stream().getReader();
let remainder = "";
async function main() {
while (true) {
const { done, value } = await reader.read();
if (done) break;
remainder += decoder.decode(value, { stream: true });
while (true) {
const idx = remainder.indexOf("\n");
if (idx === -1) break;
const line = remainder.slice(0, idx);
remainder = remainder.slice(idx + 1);
if (!line.trim()) continue;
const msg = JSON.parse(line);
await handleMessage(msg);
}
}
}
async function handleMessage(msg: {
id?: number;
method: string;
params?: Record<string, unknown>;
}) {
switch (msg.method) {
case "exec": {
const argv = msg.params?.argv as string[];
const cmd = argv?.join(" ") ?? "";
if (cmd.includes("echo hello")) {
writeLine({
jsonrpc: "2.0",
id: msg.id,
result: { stdout: "hello\n", stderr: "", exit_code: 0 },
});
} else {
writeLine({
jsonrpc: "2.0",
id: msg.id,
result: { stdout: "", stderr: "", exit_code: 0 },
});
}
break;
}
case "spawn": {
const argv = msg.params?.argv as string[];
const cwd = msg.params?.cwd as string | undefined;
const cmd = argv?.join(" ") ?? "";
const pid = `p${msg.id}`;
// Respond with pid immediately
writeLine({ jsonrpc: "2.0", id: msg.id, result: { pid } });
// Simulate streaming output based on command
if (cmd.includes("stream-test")) {
// Send 3 stdout chunks, 1 stderr chunk, then exit
for (let i = 1; i <= 3; i++) {
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "output",
params: {
pid,
stream: "stdout",
data: Buffer.from(`chunk${i}\n`).toString("base64"),
},
});
}
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "output",
params: {
pid,
stream: "stderr",
data: Buffer.from("warn\n").toString("base64"),
},
});
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 0 },
});
} else if (cmd.includes("exit-42")) {
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 42 },
});
} else if (cmd.includes("long-running")) {
// Send output slowly — will be killed
const interval = setInterval(() => {
writeLine({
jsonrpc: "2.0",
method: "output",
params: {
pid,
stream: "stdout",
data: Buffer.from("tick\n").toString("base64"),
},
});
}, 50);
// Store interval so kill can clear it
(globalThis as Record<string, unknown>)[`interval_${pid}`] = interval;
} else if (cmd.includes("cwd-test")) {
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "output",
params: {
pid,
stream: "stdout",
data: Buffer.from(`cwd=${cwd ?? "none"}\n`).toString("base64"),
},
});
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 0 },
});
} else if (cmd.includes("stdin-echo")) {
// Wait for input notifications — they'll be forwarded
// The test will send input and then kill
// We just exit after a short delay
await Bun.sleep(200);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 0 },
});
} else {
// Default: immediate exit
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 0 },
});
}
break;
}
case "kill": {
const pid = msg.params?.pid as string;
const key = `interval_${pid}`;
const interval = (globalThis as Record<string, unknown>)[key] as
| ReturnType<typeof setInterval>
| undefined;
if (interval) {
clearInterval(interval);
delete (globalThis as Record<string, unknown>)[key];
}
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
// Send exit after kill
await Bun.sleep(10);
writeLine({
jsonrpc: "2.0",
method: "exit",
params: { pid, code: 137 },
});
break;
}
case "input": {
// In mock, we just ignore input data
break;
}
case "watch": {
const watchPath = msg.params?.path as string;
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
// Simulate a realistic sequence of file change events
const events = [
{ path: `${watchPath}/src/main.ts`, event: "modify" },
{ path: `${watchPath}/src/new.ts`, event: "create" },
{ path: `${watchPath}/src/old.ts`, event: "delete" },
{ path: `${watchPath}/src/renamed.ts`, event: "rename" },
];
for (const evt of events) {
await Bun.sleep(15);
writeLine({
jsonrpc: "2.0",
method: "file_change",
params: evt,
});
}
break;
}
case "read_file": {
writeLine({
jsonrpc: "2.0",
id: msg.id,
result: {
content: Buffer.from("file content").toString("base64"),
},
});
break;
}
case "write_file": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
break;
}
case "mkdir": {
const path = msg.params?.path as string;
if (path === "/fail") {
writeLine({
jsonrpc: "2.0",
id: msg.id,
error: { code: -32000, message: "mkdir /fail: Permission denied" },
});
} else {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
}
break;
}
case "read_dir": {
const dirPath = msg.params?.path as string;
if (dirPath === "/nonexistent") {
writeLine({
jsonrpc: "2.0",
id: msg.id,
error: { code: -32000, message: "read_dir /nonexistent: No such file or directory" },
});
} else {
writeLine({
jsonrpc: "2.0",
id: msg.id,
result: {
entries: [
{ name: "file.txt", type: "file", size: 42 },
{ name: "subdir", type: "dir", size: 4096 },
{ name: "link", type: "symlink", size: 10 },
],
},
});
}
break;
}
case "stat": {
const statPath = msg.params?.path as string;
if (statPath === "/nonexistent") {
writeLine({
jsonrpc: "2.0",
id: msg.id,
error: { code: -32000, message: "stat /nonexistent: No such file or directory" },
});
} else {
writeLine({
jsonrpc: "2.0",
id: msg.id,
result: {
size: 1024,
mode: 0o100644,
mtime: 1700000000,
is_dir: false,
is_file: true,
is_symlink: false,
},
});
}
break;
}
case "remove": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
break;
}
case "rename": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
break;
}
case "copy": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
break;
}
case "chmod": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
break;
}
case "checkpoint": {
writeLine({ jsonrpc: "2.0", id: msg.id, result: {} });
process.exit(0);
}
}
}
main().catch(() => process.exit(0));