feat: expose D601 download UART CaseRun

This commit is contained in:
root
2026-07-16 23:21:56 +02:00
parent dc2a4cc2fb
commit 5a0e853889
5 changed files with 149 additions and 3 deletions
@@ -0,0 +1,48 @@
{
"contractVersion": "hwpod-case-v1",
"caseId": "d601-f103-v2-main-marker-download-uart",
"title": "D601-F103-V2 main marker download and UART CaseRun",
"mode": "download-uart",
"hwpodSpec": "hwpod-spec.yaml",
"subject": {
"repoLocalPath": "F:\\Work\\HWLAB-CASE-F103",
"commitId": "df7a4e6e551fa90d64bde5537cc000f89d63dd20",
"subdir": "projects/01_baseline"
},
"validation": {
"mode": "download-uart",
"uart": {
"maxBytes": 8192,
"timeoutMs": 3000
}
},
"expected": {
"compile": {
"success": true,
"artifactSuffixes": [
"projects/01_baseline/Output/atk_f103.hex",
"projects/01_baseline/Output/atk_f103.axf"
]
},
"download": {
"success": true,
"summaryMarkers": [
"Programming Done",
"Verify OK",
"Application running"
]
},
"uart": {
"runIdentityRequired": true,
"rawArtifactRequired": true
}
},
"runtime": {
"authority": "hwpod-service",
"operationSequence": [
"debug.build",
"debug.download",
"io.uart.read"
]
}
}
@@ -0,0 +1,29 @@
apiVersion: hwlab.dev/v0alpha1
kind: Hwpod
metadata:
uid: D601-F103-V2
name: d601-f103-v2
spec:
targetDevice:
board: D601-F103-V2
mcu: STM32F103
workspace:
path: "F:\\Work\\HWLAB-CASE-F103"
toolchain: keil-mdk
keilProject: projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx
keilTarget: USART
keilCliPath: "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py"
debugProbe:
type: daplink
adapter: keil
probeUid: 3FD750C63E342E24
probeName: MicroLink CMSIS-DAP
programBackend: keil
ioProbe:
uart:
id: uart/1
port: COM9
baudrate: 115200
nodeBinding:
nodeId: node-d601-f103-v2
nodeType: pc-host
@@ -6,6 +6,7 @@ import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts"; import { createCloudApiServer } from "./server.ts";
import { caseRunInternalFetch } from "./server-caserun-http.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 () => { 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 calls: Array<{ url: string; authorization: string | null }> = [];
@@ -114,6 +115,74 @@ test("cloud-api exposes web CaseRun cases, run status, events and aggregate", as
} }
}); });
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 () => { test("cloud-api completes a hardware-free software smoke CaseRun", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-software-smoke-")); const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-software-smoke-"));
const stateRoot = path.join(root, "state"); const stateRoot = path.join(root, "state");
+2 -2
View File
@@ -28,7 +28,7 @@ export async function handleCaseRunHttp(request: IncomingMessage, response: Serv
casesRoute: "/v1/caserun/cases", casesRoute: "/v1/caserun/cases",
runsRoute: "/v1/caserun/runs", runsRoute: "/v1/caserun/runs",
caseAuthority: describeCaseAuthority(caseRepo, cwd), caseAuthority: describeCaseAuthority(caseRepo, cwd),
compileOnly: true compileOnly: false
}); });
return; return;
} }
@@ -100,7 +100,7 @@ export async function handleCaseRunHttp(request: IncomingMessage, response: Serv
events: record.events ?? [] events: record.events ?? []
}); });
if (suffix === "aggregate" && request.method === "GET") return sendJson(response, 200, await aggregateForRecord(record)); if (suffix === "aggregate" && request.method === "GET") return sendJson(response, 200, await aggregateForRecord(record));
if (suffix === "cancel" && request.method === "POST") return sendError(response, 409, "caserun_cancel_not_supported", "This compile-only CaseRun worker cannot be cancelled after dispatch.", { runId, status: record.status }); if (suffix === "cancel" && request.method === "POST") return sendError(response, 409, "caserun_cancel_not_supported", "This CaseRun worker cannot be cancelled after dispatch.", { runId, status: record.status });
return sendError(response, 405, "method_not_allowed", suffix === "cancel" ? "POST required." : "GET required."); return sendError(response, 405, "method_not_allowed", suffix === "cancel" ? "POST required." : "GET required.");
} }
+1 -1
View File
@@ -87,7 +87,7 @@ export function caseHelp() {
], ],
notes: [ notes: [
"case prompt 是无服务预览入口,只按真实 CaseRun 组装路径渲染输入 prompt,不准备 worktree、不提交 agent、不访问硬件。", "case prompt 是无服务预览入口,只按真实 CaseRun 组装路径渲染输入 prompt,不准备 worktree、不提交 agent、不访问硬件。",
"case build remains a compile-only HWPOD smoke stage.", "case build executes the validation plan declared by the builtin v0.3 case definition.",
"case run prepares the subject worktree, submits a prompt to HWLAB Code Agent, records agent provenance and workspace diff, then runs compile validation.", "case run prepares the subject worktree, submits a prompt to HWLAB Code Agent, records agent provenance and workspace diff, then runs compile validation.",
"case aggregate is registry-only and offline by default; it reads existing artifacts without querying runtime.", "case aggregate is registry-only and offline by default; it reads existing artifacts without querying runtime.",
"case refresh/backfill defaults to local registry artifacts; only explicit --source runtime may query the existing trace CLI.", "case refresh/backfill defaults to local registry artifacts; only explicit --source runtime may query the existing trace CLI.",