Files
pikasTech-HWLAB/scripts/cloud-api-runtime-smoke.mjs
T
2026-05-21 18:33:01 +00:00

209 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import { createServer } from "node:net";
import { createCloudApiServer } from "../internal/cloud/server.mjs";
import { ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const previousEnv = {
HWLAB_CLOUD_DB_URL: process.env.HWLAB_CLOUD_DB_URL,
HWLAB_CLOUD_DB_SSL_MODE: process.env.HWLAB_CLOUD_DB_SSL_MODE
};
function logOk(name) {
process.stdout.write(`[cloud-api-runtime-smoke] ok ${name}\n`);
}
async function freePort() {
const server = createServer();
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
const port = typeof address === "object" && address ? address.port : null;
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
return port;
}
async function requestJson(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
"content-type": "application/json",
...(options.headers ?? {})
}
});
const text = await response.text();
return {
response,
body: text ? JSON.parse(text) : null
};
}
function rpcBody(id, method, params = {}) {
return JSON.stringify({
jsonrpc: "2.0",
id,
method,
params,
meta: {
traceId: `trc_${id}`,
actorId: "usr_cloud_api_runtime_smoke",
serviceId: "hwlab-cli",
environment: ENVIRONMENT_DEV
}
});
}
async function rpc(baseUrl, id, method, params = {}) {
const { response, body } = await requestJson(`${baseUrl}/rpc`, {
method: "POST",
body: rpcBody(id, method, params)
});
assert.equal(response.status, 200, `${method} http status`);
assert.equal(body.jsonrpc, "2.0", `${method} JSON-RPC version`);
assert.equal(Object.hasOwn(body, "error"), false, body.error?.message);
return body.result;
}
async function run() {
delete process.env.HWLAB_CLOUD_DB_URL;
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
const port = await freePort();
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
const baseUrl = `http://127.0.0.1:${port}`;
try {
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.status, "blocked");
assert.equal(health.body.db.connected, false);
assert.equal(health.body.db.ready, false);
assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
assert.equal(health.body.runtime.durable, false);
assert.equal(health.body.runtime.status, "degraded");
logOk("health degraded DB contract");
const v1 = await requestJson(`${baseUrl}/v1`);
assert.equal(v1.response.status, 200);
assert.equal(v1.body.status, "degraded");
for (const method of [
"gateway.session.register",
"box.resource.register",
"box.capability.report",
"hardware.operation.request",
"hardware.invoke.shell",
"audit.event.query",
"evidence.record.query"
]) {
assert.ok(v1.body.methods.includes(method), `missing ${method}`);
}
logOk("REST adapter method contract");
const projectId = "prj_cloud-api-runtime-smoke";
const gatewaySessionId = "gws_cloud-api-runtime-smoke";
const resourceId = "res_cloud-api-runtime-smoke";
const capabilityId = "cap_cloud-api-runtime-smoke";
const gateway = await rpc(baseUrl, "req_cloud_api_runtime_gateway", "gateway.session.register", {
projectId,
gatewaySessionId,
gatewayId: "gtw_cloud-api-runtime-smoke",
serviceId: "hwlab-gateway-simu",
endpoint: "http://127.0.0.1:7101"
});
assert.equal(gateway.registered, true);
assert.equal(gateway.gatewaySession.gatewaySessionId, gatewaySessionId);
assert.equal(gateway.auditEvent.action, "gateway.session.register");
const resource = await rpc(baseUrl, "req_cloud_api_runtime_box", "box.resource.register", {
projectId,
gatewaySessionId,
resourceId,
boxId: "box_cloud-api-runtime-smoke",
resourceType: "board",
state: "available",
metadata: {
ports: ["shell0"]
}
});
assert.equal(resource.registered, true);
assert.equal(resource.resource.resourceId, resourceId);
const capability = await rpc(baseUrl, "req_cloud_api_runtime_capability", "box.capability.report", {
capabilityId,
resourceId,
projectId,
name: "shell.exec",
direction: "bidirectional",
valueType: "object",
mutatesState: true
});
assert.equal(capability.reported, true);
assert.equal(capability.capabilities[0].capabilityId, capabilityId);
logOk("gateway/box/capability write path");
const invoke = await rpc(baseUrl, "req_cloud_api_runtime_shell", "hardware.invoke.shell", {
projectId,
gatewaySessionId,
resourceId,
capabilityId,
input: {
command: "echo hwlab"
}
});
assert.equal(invoke.accepted, true);
assert.equal(invoke.operation.status, "accepted");
assert.equal(invoke.dispatch.shellExecuted, false);
assert.equal(invoke.dispatch.dispatchStatus, "not_connected");
assert.equal(invoke.evidenceRecord.kind, "trace");
assert.equal(invoke.evidenceRecord.serviceId, "hwlab-cloud-api");
assert.match(invoke.evidenceRecord.sha256, /^[a-f0-9]{64}$/);
logOk("hardware invoke shell degraded dispatch with evidence");
const audit = await rpc(baseUrl, "req_cloud_api_runtime_audit_query", "audit.event.query", {
projectId
});
assert.ok(audit.count >= 5);
assert.ok(audit.events.every((event) => event.serviceId === "hwlab-cloud-api"));
assert.ok(audit.events.every((event) => event.environment === ENVIRONMENT_DEV));
assert.ok(audit.events.some((event) => event.action === "hardware.operation.request"));
assert.ok(audit.events.some((event) => event.action === "hardware.invoke.shell"));
const evidence = await rpc(baseUrl, "req_cloud_api_runtime_evidence_query", "evidence.record.query", {
projectId
});
assert.equal(evidence.count, 1);
assert.equal(evidence.records[0].operationId, invoke.operationId);
assert.equal(evidence.records[0].metadata.dispatchStatus, "not_connected");
logOk("audit/evidence query shape");
process.stdout.write("[cloud-api-runtime-smoke] passed\n");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
restoreEnv();
}
}
function restoreEnv() {
for (const [name, value] of Object.entries(previousEnv)) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
}
await run();