feat: expose D601 download UART CaseRun
This commit is contained in:
@@ -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 { 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");
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user