#!/usr/bin/env node import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { readFile } from "node:fs/promises"; import net from "node:net"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs"; import { runCli } from "../tools/hwlab-cli/lib/cli.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const children = []; function logOk(name) { process.stdout.write(`[m1-smoke] ok ${name}\n`); } function freePort() { return new Promise((resolve, reject) => { const server = net.createServer(); server.on("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : null; server.close((error) => { if (error) { reject(error); } else { resolve(port); } }); }); }); } function startNode(relativeScript, env = {}) { const child = spawn(process.execPath, [relativeScript], { cwd: repoRoot, env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] }); const output = { stdout: "", stderr: "" }; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { output.stdout += chunk; }); child.stderr.on("data", (chunk) => { output.stderr += chunk; }); const record = { child, relativeScript, output }; children.push(record); return record; } async function stopNode(record) { const { child } = record; if (child.exitCode !== null || child.signalCode !== null) { return; } await new Promise((resolve) => { const killTimer = setTimeout(() => { child.kill("SIGKILL"); }, 1500); child.once("exit", () => { clearTimeout(killTimer); resolve(); }); child.kill("SIGTERM"); }); } async function requestJson(url, options = {}) { const response = await fetch(url, { ...options, headers: { "content-type": "application/json", ...(options.headers ?? {}) } }); const text = await response.text(); const body = text ? JSON.parse(text) : null; return { response, body }; } async function waitForHttp(url, record) { const startedAt = Date.now(); let lastError; while (Date.now() - startedAt < 5000) { if (record.child.exitCode !== null) { throw new Error( `${record.relativeScript} exited before becoming ready\nstdout:\n${record.output.stdout}\nstderr:\n${record.output.stderr}` ); } try { const { response } = await requestJson(url); if (response.ok) { return; } lastError = new Error(`HTTP ${response.status}`); } catch (error) { lastError = error; } await new Promise((resolve) => setTimeout(resolve, 75)); } throw new Error( `${record.relativeScript} did not become ready at ${url}: ${lastError?.message ?? "timeout"}\nstdout:\n${record.output.stdout}\nstderr:\n${record.output.stderr}` ); } function rpcEnvelope({ id, method, params = {} }) { return { jsonrpc: "2.0", id, method, params, meta: { traceId: `trc_${id}`, serviceId: "hwlab-cli", environment: ENVIRONMENT_DEV } }; } async function smokeCloudApi() { const port = await freePort(); const service = startNode("cmd/hwlab-cloud-api/main.mjs", { HWLAB_CLOUD_API_HOST: "127.0.0.1", HWLAB_CLOUD_API_PORT: String(port), HWLAB_CLOUD_DB_URL: "", HWLAB_CLOUD_DB_SSL_MODE: "" }); const baseUrl = `http://127.0.0.1:${port}`; await waitForHttp(`${baseUrl}/live`, service); const health = await requestJson(`${baseUrl}/health`); assert.equal(health.response.status, 200); assert.equal(health.body.serviceId, "hwlab-cloud-api"); assert.equal(health.body.environment, ENVIRONMENT_DEV); assert.equal(health.body.status, "degraded"); assert.equal(health.body.db.connected, false); assert.equal(health.body.db.status, "blocked"); assert.equal(health.body.runtime.durable, false); assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]); assert.equal(health.body.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db"); assert.equal(health.body.db.secretRefs[0].redacted, true); const live = await requestJson(`${baseUrl}/live`); assert.equal(live.response.status, 200); assert.equal(live.body.status, "live"); const adapter = await requestJson(`${baseUrl}/v1`); assert.equal(adapter.response.status, 200); assert.equal(adapter.body.status, "degraded"); assert.ok(adapter.body.methods.includes("system.health")); assert.ok(adapter.body.methods.includes("gateway.session.register")); assert.ok(adapter.body.methods.includes("box.capability.report")); assert.ok(adapter.body.methods.includes("hardware.operation.request")); assert.ok(adapter.body.methods.includes("audit.event.query")); assert.ok(adapter.body.methods.includes("evidence.record.query")); const rpcHealth = await requestJson(`${baseUrl}/rpc`, { method: "POST", body: JSON.stringify(rpcEnvelope({ id: "req_m1_health", method: "system.health" })) }); assert.equal(rpcHealth.response.status, 200); assert.equal(rpcHealth.body.jsonrpc, "2.0"); assert.equal(rpcHealth.body.result.status, "degraded"); assert.equal(rpcHealth.body.result.serviceId, "hwlab-cloud-api"); assert.equal(rpcHealth.body.result.db.status, "blocked"); assert.equal(rpcHealth.body.meta.serviceId, "hwlab-cloud-api"); const projectId = "prj_mvp_topology"; const gatewaySessionId = "gws_m1_runtime"; const resourceId = "res_m1_runtime"; const capabilityId = "cap_m1_runtime"; const rpcGateway = await requestJson(`${baseUrl}/rpc`, { method: "POST", body: JSON.stringify( rpcEnvelope({ id: "req_m1_gateway", method: "gateway.session.register", params: { projectId, gatewaySessionId, gatewayId: "gtw_m1_runtime", serviceId: "hwlab-gateway-simu", endpoint: `http://127.0.0.1:${port}/gateway` } }) ) }); assert.equal(rpcGateway.response.status, 200); assert.equal(rpcGateway.body.result.registered, true); const rpcBox = await requestJson(`${baseUrl}/rpc`, { method: "POST", body: JSON.stringify( rpcEnvelope({ id: "req_m1_box", method: "box.resource.register", params: { projectId, gatewaySessionId, resourceId, boxId: "box_m1_runtime", resourceType: "board" } }) ) }); assert.equal(rpcBox.response.status, 200); assert.equal(rpcBox.body.result.registered, true); const rpcCapability = await requestJson(`${baseUrl}/rpc`, { method: "POST", body: JSON.stringify( rpcEnvelope({ id: "req_m1_capability", method: "box.capability.report", params: { capabilityId, resourceId, projectId, name: "shell.exec", direction: "bidirectional", valueType: "object", mutatesState: true } }) ) }); assert.equal(rpcCapability.response.status, 200); assert.equal(rpcCapability.body.result.reported, true); const rpcHardware = await requestJson(`${baseUrl}/rpc`, { method: "POST", body: JSON.stringify( rpcEnvelope({ id: "req_m1_hardware", method: "hardware.operation.request", params: { projectId, gatewaySessionId, resourceId, capabilityId, input: { command: "echo m1" } } }) ) }); assert.equal(rpcHardware.response.status, 200); assert.equal(rpcHardware.body.result.accepted, true); assert.equal(rpcHardware.body.result.status, "accepted"); assert.equal(rpcHardware.body.result.auditEvent.serviceId, "hwlab-cloud-api"); assert.equal(rpcHardware.body.result.persistence.durable, false); logOk("cloud-api health/live/rpc runtime"); } async function smokePatchPanelAndSimulators() { const gatewayPort = await freePort(); const boxPort = await freePort(); const patchPort = await freePort(); const gateway = startNode("cmd/hwlab-gateway-simu/main.mjs", { PORT: String(gatewayPort), HWLAB_GATEWAY_ID: "gwsimu_m1", HWLAB_BOX_RESOURCES: "res_boxsim_alpha,res_boxsim_beta" }); const box = startNode("cmd/hwlab-box-simu/main.mjs", { PORT: String(boxPort), HWLAB_BOX_ID: "boxsim_alpha" }); const patchPanel = startNode("cmd/hwlab-patch-panel/main.mjs", { PORT: String(patchPort) }); const gatewayBase = `http://127.0.0.1:${gatewayPort}`; const boxBase = `http://127.0.0.1:${boxPort}`; const patchBase = `http://127.0.0.1:${patchPort}`; await Promise.all([ waitForHttp(`${gatewayBase}/health/live`, gateway), waitForHttp(`${boxBase}/health/live`, box), waitForHttp(`${patchBase}/health/live`, patchPanel) ]); const gatewayStatus = await requestJson(`${gatewayBase}/status`); assert.equal(gatewayStatus.response.status, 200); assert.equal(gatewayStatus.body.serviceId, "hwlab-gateway-simu"); assert.equal(gatewayStatus.body.gatewayId, "gwsimu_m1"); assert.equal(gatewayStatus.body.live, true); assert.deepEqual(gatewayStatus.body.boxes, ["res_boxsim_alpha", "res_boxsim_beta"]); const boxStatus = await requestJson(`${boxBase}/status`); assert.equal(boxStatus.response.status, 200); assert.equal(boxStatus.body.serviceId, "hwlab-box-simu"); assert.equal(boxStatus.body.boxId, "boxsim_alpha"); assert.equal(boxStatus.body.live, true); assert.equal(boxStatus.body.constraints.crossDevicePropagation, "patch-panel-only"); assert.equal(boxStatus.body.constraints.localLoopbackEnabled, false); assert.ok(boxStatus.body.ports.uart0); const writePort = await requestJson(`${boxBase}/ports/write`, { method: "POST", body: JSON.stringify({ port: "uart0", value: "m1-smoke", source: "local" }) }); assert.equal(writePort.response.status, 200); assert.equal(writePort.body.accepted, true); assert.equal(writePort.body.crossDevicePropagation, "patch-panel-only"); const patchStatus = await requestJson(`${patchBase}/status`); assert.equal(patchStatus.response.status, 200); assert.equal(patchStatus.body.serviceId, "hwlab-patch-panel"); assert.equal(patchStatus.body.state, "active"); assert.equal(patchStatus.body.metadata.propagation, "patch-panel-only"); assert.equal(patchStatus.body.activeConnections.length, 2); const routed = await requestJson(`${patchBase}/signals/route`, { method: "POST", body: JSON.stringify({ fromResourceId: "res_boxsim_alpha", fromPort: "uart0", value: "m1-smoke" }) }); assert.equal(routed.response.status, 200); assert.equal(routed.body.accepted, true); assert.equal(routed.body.propagatedBy, "hwlab-patch-panel"); assert.equal(routed.body.deliveryCount, 1); assert.deepEqual(routed.body.deliveries[0], { resourceId: "res_boxsim_beta", port: "uart0", value: "m1-smoke", sourceResourceId: "res_boxsim_alpha", sourcePort: "uart0", observedAt: routed.body.deliveries[0].observedAt }); logOk("patch-panel/sim status and route runtime"); } async function smokeCliDryRunFixture() { const runtime = JSON.parse(await readFile(path.join(repoRoot, "fixtures/mvp/runtime.json"), "utf8")); assert.equal(runtime.endpoints.dev, DEV_ENDPOINT); assert.ok(runtime.serviceRoute.includes("cloud-web/cloud-api")); assert.ok(runtime.loops.some((loop) => loop.name === "hardware trusted closed loop")); assert.ok(runtime.evidence.some((record) => record.evidenceId === "evidence_mvp-runtime-plan")); let stdout = ""; let stderr = ""; const exitCode = await runCli(["test", "e2e", "--env", "dev", "--mvp", "--dry-run"], { cwd: repoRoot, stdout: { write(chunk) { stdout += chunk; } }, stderr: { write(chunk) { stderr += chunk; } } }); assert.equal(exitCode, 0, stderr); const output = JSON.parse(stdout); assert.equal(output.command, "hwlab-cli test e2e --env dev --mvp --dry-run"); assert.equal(output.mode, "dry-run"); assert.equal(output.environment, "dev"); assert.equal(output.endpoint, DEV_ENDPOINT); assert.equal(output.safety.allowNetwork, false); assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-web"), true); assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-api"), true); assert.equal(output.contractStatus.topology.gatewaySessions, 2); assert.equal(output.contractStatus.topology.boxResources, 2); assert.equal(output.contractStatus.agent.agentSessionId, "agt_m5-0001"); assert.equal(output.contractStatus.evidence.records.length, 2); assert.match(output.statement, /no DEV\/PROD changes/); logOk("CLI dry-run fixture parse"); } try { await smokeCloudApi(); await smokePatchPanelAndSimulators(); await smokeCliDryRunFixture(); process.stdout.write("[m1-smoke] passed\n"); } finally { await Promise.allSettled(children.map(stopNode)); }