feat: add device-pod build verify (#827)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -346,6 +346,53 @@ test("device-pod executor maps v0.1 CLI options to device-host-cli argv", async
|
||||
}
|
||||
});
|
||||
|
||||
test("device-pod executor returns build verify synchronously for one-call artifact verification (HWLAB #821)", async () => {
|
||||
let dispatchedBody = null;
|
||||
const cloudApi = createServer(async (request, response) => {
|
||||
dispatchedBody = await requestJson(request);
|
||||
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
||||
response.end(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: dispatchedBody.id,
|
||||
result: {
|
||||
status: "completed",
|
||||
dispatch: {
|
||||
dispatchStatus: "completed",
|
||||
stdout: JSON.stringify({ ok: true, action: "workspace.build.verify", data: { buildJob: { jobId: "keil-job-1" }, artifacts: [], missing: [] } }),
|
||||
exitCode: 0
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
await new Promise((resolve) => cloudApi.listen(0, "127.0.0.1", resolve));
|
||||
const cloudPort = cloudApi.address().port;
|
||||
const service = await startDevicePod("device-pod-test", { HWLAB_CLOUD_API_INTERNAL_URL: `http://127.0.0.1:${cloudPort}` });
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
|
||||
method: "POST",
|
||||
headers: internalHeaders({ "content-type": "application/json" }),
|
||||
body: JSON.stringify({
|
||||
jobId: "job_verify_sync",
|
||||
intent: "workspace.evidence",
|
||||
args: { kind: "verify", expected: "axf,hex", headBytes: 16 },
|
||||
profile: { devicePodId: "device-pod-test", target: { id: "target-test" }, route: { gatewaySessionId: "gws_test", hostCli: "node tools/device-host-cli.mjs" } }
|
||||
})
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.status, "completed");
|
||||
assert.equal(body.job.id, "job_verify_sync");
|
||||
const verify = JSON.parse(body.output.text);
|
||||
assert.equal(verify.action, "workspace.build.verify");
|
||||
assert.equal(verify.data.buildJob.jobId, "keil-job-1");
|
||||
assert.match(dispatchedBody.params.input.command, / workspace evidence verify --expected "axf,hex" --head-bytes 16$/u);
|
||||
} finally {
|
||||
await service.stop();
|
||||
await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test("device-pod executor maps absorbed G14 device-pod operations to host CLI argv", async () => {
|
||||
const commands = [];
|
||||
const cloudApi = createServer(async (request, response) => {
|
||||
@@ -382,6 +429,12 @@ test("device-pod executor maps absorbed G14 device-pod operations to host CLI ar
|
||||
path: "User/new.c",
|
||||
group: "User"
|
||||
}, "job_map_keil", baseBody);
|
||||
await submitAndWait(service.port, "workspace.evidence", {
|
||||
kind: "verify",
|
||||
expected: "axf,hex",
|
||||
headBytes: 16,
|
||||
target: "FREQ_Controller_FW"
|
||||
}, "job_map_verify", baseBody);
|
||||
await submitAndWait(service.port, "io.uart.jsonrpc", {
|
||||
uartId: "uart/1",
|
||||
method: "gpio.read",
|
||||
@@ -394,9 +447,10 @@ test("device-pod executor maps absorbed G14 device-pod operations to host CLI ar
|
||||
assert.match(commands[1], / workspace put User\/new\.c --content-b64 /u);
|
||||
assert.match(commands[1], / --create-dirs$/u);
|
||||
assert.match(commands[2], / workspace keil add-source User\/new\.c --base projects\/app --group User$/u);
|
||||
assert.match(commands[3], / io-probe uart\/1 jsonrpc gpio\.read /u);
|
||||
assert.match(commands[3], / --params "\{\\"pin\\":\\"PB5\\"\}" /u);
|
||||
assert.match(commands[3], / --require-jsonrpc-result --expect-result-field pin --expect-result-field value$/u);
|
||||
assert.match(commands[3], / workspace evidence verify --target FREQ_Controller_FW --expected "axf,hex" --head-bytes 16$/u);
|
||||
assert.match(commands[4], / io-probe uart\/1 jsonrpc gpio\.read /u);
|
||||
assert.match(commands[4], / --params "\{\\"pin\\":\\"PB5\\"\}" /u);
|
||||
assert.match(commands[4], / --require-jsonrpc-result --expect-result-field pin --expect-result-field value$/u);
|
||||
} finally {
|
||||
await service.stop();
|
||||
await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve())));
|
||||
@@ -430,7 +484,7 @@ async function submitAndWait(port, intent, args, jobId, extraBody = {}, expected
|
||||
headers: internalHeaders({ "content-type": "application/json" }),
|
||||
body: JSON.stringify({ jobId, intent, args, traceId: `trc_${jobId}`, operationId: `op_${jobId}`, ...extraBody })
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.ok([200, 202].includes(response.status), `unexpected job create status ${response.status}`);
|
||||
return await waitForJobStatus(port, "device-pod-test", jobId, expectedStatus);
|
||||
}
|
||||
|
||||
|
||||
@@ -185,12 +185,13 @@ async function handleInternalJob(request, response, id) {
|
||||
const now = new Date().toISOString();
|
||||
const profile = normalizeObject(body.profile);
|
||||
const route = normalizeObject(profile.route);
|
||||
const args = normalizeObject(body.args);
|
||||
const jobId = typeof body.jobId === "string" && body.jobId.startsWith("job_") ? body.jobId : `job_devicepod_${randomUUID()}`;
|
||||
const command = deviceHostCommand(profile, {
|
||||
id: jobId,
|
||||
devicePodId: id,
|
||||
intent: typeof body.intent === "string" ? body.intent : "unknown",
|
||||
args: normalizeObject(body.args)
|
||||
args
|
||||
});
|
||||
const dispatchBlocker = gatewayDispatchPreflightBlocker(route, command);
|
||||
const job = {
|
||||
@@ -198,6 +199,7 @@ async function handleInternalJob(request, response, id) {
|
||||
devicePodId: id,
|
||||
status: dispatchBlocker ? "blocked" : "running",
|
||||
intent: typeof body.intent === "string" ? body.intent : "unknown",
|
||||
args,
|
||||
reason: typeof body.reason === "string" ? body.reason : "",
|
||||
traceId,
|
||||
operationId,
|
||||
@@ -213,10 +215,20 @@ async function handleInternalJob(request, response, id) {
|
||||
source: sourcePayload()
|
||||
};
|
||||
jobs.set(jobKey(id, job.id), job);
|
||||
if (!dispatchBlocker && shouldSynchronouslyDispatch(job)) {
|
||||
const dispatched = await dispatchGatewayJob({ job, route });
|
||||
const finalJob = dispatched ?? job;
|
||||
jsonResponse(response, finalJob.status === "completed" ? 200 : 409, jobOutputPayload(finalJob));
|
||||
return;
|
||||
}
|
||||
if (!dispatchBlocker) dispatchGatewayJob({ job, route });
|
||||
jsonResponse(response, dispatchBlocker ? 409 : 202, jobPayload(job, { accepted: !dispatchBlocker }));
|
||||
}
|
||||
|
||||
function shouldSynchronouslyDispatch(job) {
|
||||
return job.intent === "workspace.evidence" && textOr(job.args?.kind, "") === "verify";
|
||||
}
|
||||
|
||||
function handleGetInternalJob(request, response, id, subpath) {
|
||||
if (!isInternalCaller(request)) return rejectExternalJobRoute(response);
|
||||
const parts = subpath.split("/");
|
||||
@@ -290,18 +302,21 @@ async function dispatchGatewayJob({ job, route }) {
|
||||
if (current && terminalJobStatus(current.status)) return;
|
||||
const next = jobFromGatewayDispatch(job, payload, response.status);
|
||||
jobs.set(jobKey(job.devicePodId, job.id), next);
|
||||
return next;
|
||||
} catch (error) {
|
||||
const current = jobs.get(jobKey(job.devicePodId, job.id));
|
||||
if (current && terminalJobStatus(current.status)) return;
|
||||
const now = new Date().toISOString();
|
||||
jobs.set(jobKey(job.devicePodId, job.id), {
|
||||
const next = {
|
||||
...job,
|
||||
status: "failed",
|
||||
updatedAt: now,
|
||||
completedAt: now,
|
||||
output: boundedOutput({ text: "", error: error?.message ?? "gateway dispatch failed" }),
|
||||
blocker: gatewayDispatchFailedBlocker(error?.message ?? "gateway dispatch failed")
|
||||
});
|
||||
};
|
||||
jobs.set(jobKey(job.devicePodId, job.id), next);
|
||||
return next;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
@@ -581,7 +596,7 @@ function deviceHostArgs(intent, args = {}) {
|
||||
"evidence",
|
||||
textOr(args.kind, "build"),
|
||||
...jobIdArgs(args),
|
||||
...hostOptionArgs(args, ["tail", "full", "path", "target"])
|
||||
...hostOptionArgs(args, ["tail", "full", "path", "target", "expected", "headBytes"])
|
||||
];
|
||||
}
|
||||
if (intent === "debug.evidence") {
|
||||
|
||||
Reference in New Issue
Block a user