#!/usr/bin/env node import { spawn } from "node:child_process"; import { mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; const root = path.resolve(import.meta.dirname, ".."); const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-workbench-native-")); const children = []; const logs = []; try { const api = start(["bun", "cmd/hwlab-workbench-api/main.ts"], { ...process.env, WORKBENCH_MODE: "native-test", WORKBENCH_NATIVE_STATE_FILE: path.join(stateDir, "state.json"), WORKBENCH_API_HOST: "127.0.0.1", WORKBENCH_API_PORT: "6677" }, "api"); children.push(api); await waitFor("http://127.0.0.1:6677/health/ready"); const web = start(["bun", "run", "workbench:web:dev"], process.env, "web"); children.push(web); await waitFor("http://127.0.0.1:5173/workbench", { acceptHtml: true }); const commandHealth = await requiredJson(await fetch("http://127.0.0.1:6677/v1/workbench/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ operation: "health" }) }), "command.health"); if (commandHealth.mode !== "native-test") throw new Error("native command transport did not expose mode=native-test"); const actorHeaders = { "content-type": "application/json", cookie: "hwlab_session=native-test" }; const sessionResponse = await fetch("http://127.0.0.1:6677/v1/agent/sessions", { method: "POST", headers: actorHeaders, body: JSON.stringify({ providerProfile: "native-test" }) }); const session = await requiredJson(sessionResponse, "session.create"); const sessionId = session.session?.sessionId; if (!sessionId) throw new Error("native session.create returned no sessionId"); const traceId = "trc_native_smoke"; const submit = await requiredJson(await fetch("http://127.0.0.1:6677/v1/agent/chat", { method: "POST", headers: actorHeaders, body: JSON.stringify({ sessionId, traceId, message: "native smoke" }) }), "turn.submit"); if (submit.orchestrationMode !== "native-test" || submit.resultSynthesized !== false) throw new Error("native submit mode/authority contract failed"); const cancel = await requiredJson(await fetch("http://127.0.0.1:6677/v1/agent/chat/cancel", { method: "POST", headers: actorHeaders, body: JSON.stringify({ sessionId, traceId }) }), "turn.cancel"); if (cancel.status !== "cancel_requested") throw new Error("native cancel was not accepted"); process.stdout.write(`${JSON.stringify({ ok: true, mode: "native-test", apiUrl: "http://127.0.0.1:6677", webUrl: "http://127.0.0.1:5173/workbench", sessionId, traceId, services: { api: "independent", web: "vite-hmr" }, terminalAuthority: "fixture-only", resultSynthesized: false })}\n`); } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "workbench_native_smoke_failed", message: error instanceof Error ? error.message : String(error) }, logs: logs.slice(-20) })}\n`); process.exitCode = 1; } finally { for (const child of children.reverse()) child.kill("SIGTERM"); await Promise.all(children.map((child) => child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : new Promise((resolve) => child.once("exit", resolve)))); await rm(stateDir, { recursive: true, force: true }); } function start(command, env, service) { const child = spawn(command[0], command.slice(1), { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] }); child.stdout.on("data", (chunk) => append(service, "stdout", chunk)); child.stderr.on("data", (chunk) => append(service, "stderr", chunk)); return child; } function append(service, stream, chunk) { for (const line of String(chunk).split("\n").filter(Boolean)) logs.push({ service, stream, line: line.slice(0, 500) }); if (logs.length > 100) logs.splice(0, logs.length - 100); } async function waitFor(url, options = {}) { const deadline = Date.now() + 30_000; while (Date.now() < deadline) { const response = await fetch(url).catch(() => null); if (response?.ok && (!options.acceptHtml || String(response.headers.get("content-type")).includes("text/html"))) return; await new Promise((resolve) => setTimeout(resolve, 250)); } throw new Error(`timed out waiting for ${url}`); } async function requiredJson(response, operation) { const body = await response.json().catch(() => null); if (!response.ok || !body) throw new Error(`${operation} failed with HTTP ${response.status}`); return body; }