Files
pikasTech-HWLAB/cmd/hwlab-device-pod/main.test.ts
T
2026-05-29 08:10:19 +08:00

123 lines
4.7 KiB
TypeScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { createServer } from "node:http";
import test from "node:test";
const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
test("device pod executor exposes cloud-api authority boundary and method guards", async () => {
const service = await startDevicePod("device-pod-test");
try {
const health = await fetchJson(`http://127.0.0.1:${service.port}/health/live`);
assert.equal(health.serviceId, "hwlab-device-pod");
assert.equal(health.status, "live");
assert.equal(health.devicePodId, "device-pod-test");
assert.equal(health.contractVersion, "device-pod-executor-v1");
assert.equal(health.role, "device-pod-internal-executor");
assert.equal(health.authority, "hwlab-cloud-api");
assert.equal(health.fake, false);
const list = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods`);
assert.equal(list.serviceId, "hwlab-device-pod");
assert.equal(list.selectedDevicePodId, "device-pod-test");
assert.equal(list.devicePods.length, 1);
assert.equal(list.source.fake, false);
const status = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/status`);
assert.equal(status.devicePodId, "device-pod-test");
assert.equal(status.status, "blocked");
assert.equal(status.summary.blocker.code, "gateway_dispatch_unavailable");
const events = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/events?limit=1`);
assert.equal(events.events.length, 1);
assert.equal(events.truncation.limit, 1);
assert.equal(events.truncation.truncated, false);
assert.equal(events.events[0].blocker.code, "gateway_dispatch_unavailable");
const externalJob = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ intent: "workspace.ls" })
});
assert.equal(externalJob.status, 403);
assert.equal((await externalJob.json()).error.code, "cloud_api_authority_required");
const internalJob = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
method: "POST",
headers: { "content-type": "application/json", "x-hwlab-internal-service": "hwlab-cloud-api" },
body: JSON.stringify({ intent: "workspace.ls", traceId: "trc_device_pod_test" })
});
assert.equal(internalJob.status, 409);
assert.equal((await internalJob.json()).blocker.code, "gateway_dispatch_unavailable");
const missing = await fetch(`http://127.0.0.1:${service.port}/missing`);
assert.equal(missing.status, 404);
assert.deepEqual(await missing.json(), { error: "not_found", path: "/missing" });
const methodGuard = await fetch(`http://127.0.0.1:${service.port}/health`, { method: "POST" });
assert.equal(methodGuard.status, 405);
assert.deepEqual(await methodGuard.json(), { error: "method_not_allowed", method: "POST", path: "/health" });
} finally {
await service.stop();
}
});
async function startDevicePod(devicePodId) {
const port = await freePort();
const child = spawn(bunCommand, ["run", "cmd/hwlab-device-pod/main.ts"], {
cwd: process.cwd(),
env: {
...process.env,
HWLAB_DEVICE_POD_PORT: String(port),
HWLAB_DEVICE_POD_ID: devicePodId,
HWLAB_ENVIRONMENT: "v02"
},
stdio: ["ignore", "pipe", "pipe"]
});
await waitForListening(child, "device pod");
return {
port,
stop: () => stopChild(child)
};
}
async function fetchJson(url) {
const response = await fetch(url);
assert.equal(response.ok, true, `${url} returned ${response.status}`);
return response.json();
}
async function freePort() {
const server = createServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
await new Promise((resolve) => server.close(resolve));
return port;
}
async function waitForListening(child, label) {
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`${label} did not start: ${stderr}`)), 5000);
child.stdout.on("data", (chunk) => {
if (chunk.toString().includes("listening")) {
clearTimeout(timer);
resolve();
}
});
child.on("exit", (code) => {
clearTimeout(timer);
reject(new Error(`${label} exited early code=${code}: ${stderr}`));
});
});
}
async function stopChild(child) {
if (child.exitCode !== null) return;
child.kill("SIGTERM");
await new Promise((resolve) => child.once("exit", resolve));
}