Files
pikasTech-HWLAB/scripts/m1-contract-smoke.mjs
T
Lyon 357a68bb95 fix: layer dev DB readiness evidence
Refs #49

Merged by commander after reviewing Code Queue task codex_1779422762669_1. Adds redacted DB readiness layering and refreshes DEV endpoint/report evidence. Follow-up: deploy/verify the new readiness probe in DEV via standard image CI/CD.
2026-05-22 12:27:01 +08:00

417 lines
13 KiB
JavaScript

#!/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.liveConnected, false);
assert.equal(health.body.db.connectionAttempted, false);
assert.equal(health.body.db.connectionResult, "not_attempted_missing_env");
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));
}