From 5a0e853889bf7072489a719da0c6694be9ca1041 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 23:21:56 +0200 Subject: [PATCH] feat: expose D601 download UART CaseRun --- .../case.json | 48 +++++++++++++ .../hwpod-spec.yaml | 29 ++++++++ internal/cloud/server-caserun-http.test.ts | 69 +++++++++++++++++++ internal/cloud/server-caserun-http.ts | 4 +- tools/src/hwlab-caserun-runtime.ts | 2 +- 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 cases/d601-f103-v2-main-marker-download-uart/case.json create mode 100644 cases/d601-f103-v2-main-marker-download-uart/hwpod-spec.yaml diff --git a/cases/d601-f103-v2-main-marker-download-uart/case.json b/cases/d601-f103-v2-main-marker-download-uart/case.json new file mode 100644 index 00000000..29d2b084 --- /dev/null +++ b/cases/d601-f103-v2-main-marker-download-uart/case.json @@ -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" + ] + } +} diff --git a/cases/d601-f103-v2-main-marker-download-uart/hwpod-spec.yaml b/cases/d601-f103-v2-main-marker-download-uart/hwpod-spec.yaml new file mode 100644 index 00000000..cfd7477c --- /dev/null +++ b/cases/d601-f103-v2-main-marker-download-uart/hwpod-spec.yaml @@ -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 diff --git a/internal/cloud/server-caserun-http.test.ts b/internal/cloud/server-caserun-http.test.ts index d167e522..3af95938 100644 --- a/internal/cloud/server-caserun-http.test.ts +++ b/internal/cloud/server-caserun-http.test.ts @@ -6,6 +6,7 @@ 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 }> = []; @@ -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 () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-software-smoke-")); const stateRoot = path.join(root, "state"); diff --git a/internal/cloud/server-caserun-http.ts b/internal/cloud/server-caserun-http.ts index a8a82e3a..4a9d1f3d 100644 --- a/internal/cloud/server-caserun-http.ts +++ b/internal/cloud/server-caserun-http.ts @@ -28,7 +28,7 @@ export async function handleCaseRunHttp(request: IncomingMessage, response: Serv casesRoute: "/v1/caserun/cases", runsRoute: "/v1/caserun/runs", caseAuthority: describeCaseAuthority(caseRepo, cwd), - compileOnly: true + compileOnly: false }); return; } @@ -100,7 +100,7 @@ export async function handleCaseRunHttp(request: IncomingMessage, response: Serv events: record.events ?? [] }); 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."); } diff --git a/tools/src/hwlab-caserun-runtime.ts b/tools/src/hwlab-caserun-runtime.ts index 6483fa46..4744b86b 100644 --- a/tools/src/hwlab-caserun-runtime.ts +++ b/tools/src/hwlab-caserun-runtime.ts @@ -87,7 +87,7 @@ export function caseHelp() { ], notes: [ "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 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.",