Files
pikasTech-HWLAB/internal/cloud/server-caserun-http.test.ts
T
2026-07-16 23:21:56 +02:00

337 lines
16 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts";
import { caseRunInternalFetch } from "./server-caserun-http.ts";
import { caseValidationPlanFromDefinition, hwpodCommandForValidationStep } from "../../tools/src/hwlab-caserun-validation.ts";
test("CaseRun internal requests reuse the existing bootstrap admin API key only for the internal origin", async () => {
const calls: Array<{ url: string; authorization: string | null }> = [];
const fetchImpl = async (input: URL | RequestInfo, init: RequestInit = {}) => {
const headers = new Headers(init.headers);
calls.push({ url: input instanceof Request ? input.url : String(input), authorization: headers.get("authorization") });
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
};
const internalFetch = caseRunInternalFetch(fetchImpl as typeof fetch, "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667", { HWLAB_BOOTSTRAP_ADMIN_API_KEY: "hwl_live_test" });
await internalFetch("http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", { method: "POST" });
await internalFetch("https://lab-dev.hwpod.com/v1/hwpod-node-ops", { method: "POST" });
await internalFetch(new Request("http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", { headers: { authorization: "Bearer caller-token" } }), { method: "POST" });
assert.deepEqual(calls, [
{ url: "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", authorization: "Bearer hwl_live_test" },
{ url: "https://lab-dev.hwpod.com/v1/hwpod-node-ops", authorization: null },
{ url: "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", authorization: "Bearer caller-token" }
]);
});
test("cloud-api exposes web CaseRun cases, run status, events and aggregate", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-api-"));
const stateRoot = path.join(root, "state");
const caseDir = path.join(root, "cases", "d601-f103-v2-compile");
await mkdir(caseDir, { recursive: true });
await writeFile(path.join(caseDir, "case.json"), JSON.stringify({
contractVersion: "hwpod-case-v1",
caseId: "d601-f103-v2-compile",
title: "D601 compile",
mode: "compile-only",
hwpodSpec: "hwpod-spec.yaml",
subject: { repoLocalPath: "F:\\Work\\HWLAB-CASE-F103", commitId: "abc", subdir: "projects/01_baseline" }
}), "utf8");
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), sampleSpecYaml(), "utf8");
const seen: any[] = [];
const accessController = {
async ensureBootstrap() {},
async requireNavAccess() { return true; }
};
const server = createCloudApiServer({
accessController,
env: { PATH: process.env.PATH, HWLAB_CASERUN_CASE_REPO: root, HWLAB_CASERUN_STATE_DIR: stateRoot, HWLAB_ENVIRONMENT: "v03", HWLAB_CLOUD_API_PORT: "6667" },
caseRunExecutor: async (context: any, emit: any) => {
seen.push(context.parsed);
await emit("prepared", { runDir: context.parsed.runDir });
await emit("build-completed", { jobId: "job-test" });
return {
summary: { status: "recorded", jobId: "job-test", artifactCount: 2, artifacts: [{ path: "atk_f103.hex" }, { path: "atk_f103.axf" }] },
evidence: { status: "recorded", keilJob: { jobId: "job-test", status: "completed" }, artifacts: [{ path: "atk_f103.hex" }, { path: "atk_f103.axf" }] },
run: { runId: context.parsed.runId, caseId: context.parsed.caseId }
};
}
});
await listen(server);
try {
const indexResponse = await fetch(`${serverUrl(server)}/v1`);
const indexPayload = await indexResponse.json();
assert.equal(indexPayload.caserun.route, "/v1/caserun");
const casesResponse = await fetch(`${serverUrl(server)}/v1/caserun/cases`);
const casesPayload = await casesResponse.json();
assert.equal(casesResponse.status, 200);
assert.equal(casesPayload.count, 1);
assert.equal(casesPayload.cases[0].caseId, "d601-f103-v2-compile");
const startResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ caseId: "d601-f103-v2-compile" })
});
const startPayload = await startResponse.json();
assert.equal(startResponse.status, 202);
assert.ok(["queued", "running"].includes(startPayload.status));
assert.match(startPayload.runId, /^web-d601-f103-v2-compile-/u);
assert.equal(startPayload.statusUrl, `/v1/caserun/runs/${startPayload.runId}`);
const runPayload = await waitForRun(server, startPayload.runId, "completed");
assert.equal(runPayload.terminal, true);
assert.equal(runPayload.facts.jobId, "job-test");
assert.equal(runPayload.facts.artifactCount, 2);
assert.equal(runPayload.references.events.count >= 3, true);
assert.match(runPayload.references.aggregate.sha256, /^[a-f0-9]{64}$/u);
assert.equal(runPayload.sourceAuthority.authority, "caserun-archived-facts");
assert.equal(seen[0].noCaseRepoRecord, true);
assert.equal(seen[0].caseRepo, root);
assert.equal(seen[0].apiUrl, "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667");
const eventsResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs/${startPayload.runId}/events`);
const eventsPayload = await eventsResponse.json();
assert.equal(eventsResponse.status, 200);
assert.equal(eventsPayload.runId, runPayload.runId);
assert.equal(eventsPayload.caseId, runPayload.caseId);
assert.equal(eventsPayload.sourceAuthority.authority, runPayload.sourceAuthority.authority);
assert.ok(eventsPayload.events.some((event: any) => event.stage === "build-completed"));
const aggregateResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs/${startPayload.runId}/aggregate`);
const aggregatePayload = await aggregateResponse.json();
assert.equal(aggregateResponse.status, 200);
assert.equal(aggregatePayload.summary.jobId, "job-test");
assert.match(aggregatePayload.sha256, /^[a-f0-9]{64}$/u);
assert.equal(aggregatePayload.sourceAuthority.authority, runPayload.sourceAuthority.authority);
} finally {
await close(server);
await rm(root, { recursive: true, force: true });
}
});
test("builtin CaseRun exposes and preflights the D601 download and UART case", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-download-uart-"));
const caseId = "d601-f103-v2-main-marker-download-uart";
const caseFile = path.join(process.cwd(), "cases", caseId, "case.json");
const definition = JSON.parse(await readFile(caseFile, "utf8"));
const validationPlan = caseValidationPlanFromDefinition(definition);
assert.equal(validationPlan.mode, "download-uart");
assert.equal(validationPlan.compileOnly, false);
assert.deepEqual(validationPlan.steps.map((step) => step.kind), ["build", "download", "uart"]);
assert.deepEqual(validationPlan.steps.map((step) => step.expectedOp), ["debug.build", "debug.download", "io.uart.read"]);
assert.deepEqual(validationPlan.steps.map((step) => hwpodCommandForValidationStep(step, {
executable: "bun",
cliPath: "tools/hwpod-cli.ts",
specPath: ".hwlab/hwpod-spec.yaml",
reason: "focused preflight"
}).slice(2, 4)), [["build", "--spec"], ["download", "--spec"], ["uart", "read"]]);
const accessController = {
async ensureBootstrap() {},
async requireNavAccess() { return true; }
};
const server = createCloudApiServer({
accessController,
env: { PATH: process.env.PATH, HWLAB_CASERUN_REPO_ROOT: process.cwd(), HWLAB_CASERUN_STATE_DIR: stateRoot, HWLAB_ENVIRONMENT: "v03", HWLAB_CLOUD_API_PORT: "6667" },
caseRunExecutor: async (context: any, emit: any) => {
await emit("prepared", { runDir: context.parsed.runDir });
throw Object.assign(new Error("No HWPOD node is available for focused preflight."), {
code: "hwpod_node_unavailable",
details: { blocker: { code: "hwpod_node_unavailable", summary: "No HWPOD node is available.", retryable: true } }
});
}
});
await listen(server);
try {
const casesResponse = await fetch(`${serverUrl(server)}/v1/caserun/cases`);
const casesPayload = await casesResponse.json();
const matches = casesPayload.cases.filter((item: any) => item.caseId === caseId);
assert.equal(matches.length, 1);
assert.deepEqual(matches[0], {
caseId,
title: "D601-F103-V2 main marker download and UART CaseRun",
mode: "download-uart",
hwpodSpec: "hwpod-spec.yaml",
hardwareRequired: true,
available: true,
subject: definition.subject,
expected: definition.expected,
runtime: definition.runtime
});
const startResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ caseId })
});
const started = await startResponse.json();
assert.equal(startResponse.status, 202);
assert.match(started.runId, /^web-d601-f103-v2-main-marker-download-uart-/u);
const failed = await waitForRun(server, started.runId, "failed");
assert.equal(failed.caseId, caseId);
assert.equal(failed.terminal, true);
assert.equal(failed.blocker.code, "hwpod_node_unavailable");
} finally {
await close(server);
await rm(stateRoot, { recursive: true, force: true });
}
});
test("cloud-api completes a hardware-free software smoke CaseRun", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-software-smoke-"));
const stateRoot = path.join(root, "state");
const caseDir = path.join(root, "cases", "software-smoke");
await mkdir(caseDir, { recursive: true });
await writeFile(path.join(caseDir, "case.json"), JSON.stringify({
contractVersion: "hwlab-caserun-software-smoke-v1",
caseId: "software-smoke",
title: "software smoke",
mode: "software-smoke"
}), "utf8");
const accessController = {
async ensureBootstrap() {},
async requireNavAccess() { return true; }
};
const server = createCloudApiServer({
accessController,
env: { PATH: process.env.PATH, HWLAB_CASERUN_CASE_REPO: root, HWLAB_CASERUN_STATE_DIR: stateRoot, HWLAB_ENVIRONMENT: "v03", HWLAB_CLOUD_API_PORT: "6667" }
});
await listen(server);
try {
const casesResponse = await fetch(`${serverUrl(server)}/v1/caserun/cases`);
const casesPayload = await casesResponse.json();
assert.equal(casesPayload.cases[0].caseId, "software-smoke");
assert.equal(casesPayload.cases[0].hardwareRequired, false);
assert.equal(casesPayload.cases[0].available, true);
const startResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ caseId: "software-smoke" })
});
const started = await startResponse.json();
assert.equal(startResponse.status, 202);
const completed = await waitForRun(server, started.runId, "completed");
assert.equal(completed.terminal, true);
assert.equal(completed.facts.artifactCount, 1);
const eventsResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs/${started.runId}/events`);
const eventsPayload = await eventsResponse.json();
assert.ok(eventsPayload.events.some((event: any) => event.stage === "software-smoke-completed"));
const aggregateResponse = await fetch(`${serverUrl(server)}/v1/caserun/runs/${started.runId}/aggregate`);
const aggregate = await aggregateResponse.json();
assert.equal(aggregate.status, "completed");
assert.equal(aggregate.evidence.hardwareRequired, false);
assert.match(aggregate.sha256, /^[a-f0-9]{64}$/u);
const artifact = JSON.parse(await readFile(path.join(stateRoot, "runs", started.runId, "software-smoke.json"), "utf8"));
assert.deepEqual(artifact.checks, { caseDefinitionLoaded: true, runDirectoryWritable: true, backgroundWorkerRunning: true });
} finally {
await close(server);
await rm(root, { recursive: true, force: true });
}
});
test("cloud-api projects queued, running, completed and failed records with one read-model shape", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-read-model-"));
const stateRoot = path.join(root, "state");
const accessController = {
async ensureBootstrap() {},
async requireNavAccess() { return true; }
};
const server = createCloudApiServer({
accessController,
env: { PATH: process.env.PATH, HWLAB_CASERUN_CASE_REPO: root, HWLAB_CASERUN_STATE_DIR: stateRoot }
});
await mkdir(path.join(stateRoot, "records"), { recursive: true });
const statuses = ["queued", "running", "completed", "failed"];
for (const status of statuses) {
const runId = `read-model-${status}`;
const record = {
runId,
caseId: "read-model-case",
status,
stage: status === "running" ? "build" : status,
runDir: path.join(stateRoot, "runs", runId),
createdAt: "2026-07-16T00:00:00.000Z",
updatedAt: "2026-07-16T00:00:01.000Z",
caseAuthority: { kind: "case-repo" },
events: [],
result: status === "completed" ? {
summary: { jobId: "job-read-model", artifactCount: 1, artifactManifestPath: "runs/read-model/artifact-manifest.json", artifactManifestSha256: "a".repeat(64) },
evidence: { status: "recorded", replay: { contractVersion: "hwpod-case-validation-replay-v1" }, validation: { archivePath: "validation-observations.json" }, hwpod: { operation: { rawArtifactRef: { path: "raw.json" } } } },
run: { runId, caseId: "read-model-case" }
} : null,
blocker: status === "failed" ? { code: "hwpod_build_failed", summary: "HWPOD build failed", layer: "hwpod" } : null,
error: status === "failed" ? { code: "caserun_failed", message: "CaseRun failed." } : null
};
await mkdir(record.runDir, { recursive: true });
await writeFile(path.join(stateRoot, "records", `${runId}.json`), `${JSON.stringify(record)}\n`, "utf8");
}
await listen(server);
try {
for (const status of statuses) {
const response = await fetch(`${serverUrl(server)}/v1/caserun/runs/read-model-${status}`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.deepEqual(Object.keys(payload).sort(), ["blocker", "caseId", "contractVersion", "createdAt", "error", "facts", "ok", "references", "runId", "sourceAuthority", "stage", "status", "terminal", "updatedAt"].sort());
assert.equal(payload.status, status);
assert.equal(payload.terminal, ["completed", "failed"].includes(status));
assert.equal(payload.references.status.href, `/v1/caserun/runs/read-model-${status}`);
}
const completed = await (await fetch(`${serverUrl(server)}/v1/caserun/runs/read-model-completed`)).json();
assert.equal(completed.references.manifest.sha256, "a".repeat(64));
assert.equal(completed.references.replay.ref, "validation-observations.json");
assert.equal(completed.references.hwpod.path, "raw.json");
const failed = await (await fetch(`${serverUrl(server)}/v1/caserun/runs/read-model-failed`)).json();
assert.equal(failed.blocker.code, "hwpod_build_failed");
} finally {
await close(server);
await rm(root, { recursive: true, force: true });
}
});
async function waitForRun(server: any, runId: string, status: string) {
const deadline = Date.now() + 3000;
let last: any = null;
while (Date.now() < deadline) {
const response = await fetch(`${serverUrl(server)}/v1/caserun/runs/${runId}`);
last = await response.json();
if (last.status === status) return last;
await new Promise((resolve) => setTimeout(resolve, 50));
}
assert.fail(`run did not reach ${status}: ${JSON.stringify(last)}`);
}
async function listen(server: any) {
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
}
async function close(server: any) {
await new Promise((resolve, reject) => server.close((error: Error | undefined) => (error ? reject(error) : resolve(undefined))));
}
function serverUrl(server: any) {
return `http://127.0.0.1:${server.address().port}`;
}
function sampleSpecYaml() {
return `apiVersion: hwlab.dev/v0alpha1
kind: Hwpod
metadata:
uid: D601-F103-V2
name: d601-f103-v2
spec:
workspace:
path: "F:\\\\Work\\\\HWLAB-CASE-F103"
nodeBinding:
nodeId: node-d601-f103-v2
`;
}