diff --git a/.hwlab/hwpod-spec.meta.json b/.hwlab/hwpod-spec.meta.json index 01f670ba..b977aed2 100644 --- a/.hwlab/hwpod-spec.meta.json +++ b/.hwlab/hwpod-spec.meta.json @@ -2,11 +2,11 @@ "contractVersion": "hwpod-spec-registry-v1", "source": { "kind": "preinstalled-verified-spec", - "caseRepo": "pikasTech/hwpod-cases", + "caseRepo": "pikasTech/hwlab-case-registry", "caseId": "d601-f103-v2-compile", "caseSpecPath": "cases/d601-f103-v2-compile/hwpod-spec.yaml", "verificationIssue": "pikasTech/HWLAB#924", - "verificationPr": "pikasTech/hwpod-cases#1", + "verificationPr": "pikasTech/hwlab-case-registry#1", "verifiedAt": "2026-06-05" } } diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index 104cdaa1..4b1045e1 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -179,7 +179,7 @@ bun tools/hwpod-node.ts serve --host 127.0.0.1 --port 19678 第一阶段 CaseRun 不引入常驻调度服务,由 `hwlab-cli case` 组合 case registry repo、本地 `.state`、`hwpod-cli`、`hwpod-compiler-cli`、`hwlab-api` 和在线 `hwpod-node` 完成单次运行。标准 case registry repo 是 `pikasTech/hwlab-case-registry`,本地默认 checkout 路径是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 只作为迁移前来源。case registry repo 提供 `cases//case.json` 与同目录 `hwpod-spec.yaml`;每次运行会把 spec 复制到 `.state/hwlab-cli/caserun//.hwlab/hwpod-spec.yaml`,形成隔离的 run-local spec。 -CaseRun 的 case registry repo 与 subject repo 必须分离。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、subject 引用、期望结果和 `runs///` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,由 `case.json` 通过 `subject` 字段引用独立 Git repo、ref 和 subdir;`source` 作为旧字段名只用于迁移兼容,不再作为新 case 的标准命名。`case run` 准备阶段必须把 subject ref 解析为固定 subject commit,复制或 checkout 到 run-local workspace,再从隔离副本执行 build/download/smoke。`branch` 或 `tag` 只能作为人工入口,`evidence.json` 必须记录 subject repo、subject commit、subdir 和 run-local workspace,避免把 floating ref 或外部脏 workspace 当作可审计证据。 +CaseRun 的 case registry repo 与 subject repo 必须分离。标准 registry repo 是 `pikasTech/hwlab-case-registry`,G14 标准 checkout 是 `/root/hwlab-case-registry`;历史 `pikasTech/hwpod-cases` 和 `/root/hwpod-cases` 只作为迁移前来源,不再作为新 CaseRun 入口或写入目标。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、`subject` 引用、期望结果和 `runs///` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。subject repo 是被开发、被测试、被编译或被下载验证的真实源码仓库,由 `case.json` 通过 `subject` 字段引用独立 Git repo、ref 和 subdir;`case run` 准备阶段必须把 subject ref 解析为固定 subject commit,复制或 checkout 到 run-local workspace,再从隔离副本执行 build/download/smoke。`branch` 或 `tag` 只能作为人工入口,`evidence.json` 必须记录 subject repo、subject commit、subdir 和 run-local workspace,避免把 floating ref 或外部脏 workspace 当作可审计证据。 CaseRun 是强化学习 Harness 的最小执行单元,但它本身不是完整 epoch。完整 epoch 还需要样本选择、策略生成、奖励/评价、批量调度、跨 run 归因和自动训练闭环;第一阶段只要求一个 `caseId` 能从 case registry repo 进入真实 HWPOD 编译闭环,并把可审计产物稳定落盘。 diff --git a/internal/cloud/hwpod-node-ops.test.ts b/internal/cloud/hwpod-node-ops.test.ts index bb65f6ef..6fb0df51 100644 --- a/internal/cloud/hwpod-node-ops.test.ts +++ b/internal/cloud/hwpod-node-ops.test.ts @@ -29,7 +29,7 @@ test("cloud-api discovers preinstalled workspace hwpod-specs without hardcoded d const specDir = path.join(root, ".hwlab"); await mkdir(specDir, { recursive: true }); await writeFile(path.join(specDir, "hwpod-spec.yaml"), sampleSpecYaml(), "utf8"); - await writeFile(path.join(specDir, "hwpod-spec.meta.json"), JSON.stringify({ source: { kind: "preinstalled-verified-spec", caseRepo: "pikasTech/hwpod-cases", caseId: "d601-f103-v2-compile" } }), "utf8"); + await writeFile(path.join(specDir, "hwpod-spec.meta.json"), JSON.stringify({ source: { kind: "preinstalled-verified-spec", caseRepo: "pikasTech/hwlab-case-registry", caseId: "d601-f103-v2-compile" } }), "utf8"); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: root } }); await listen(server); try { @@ -41,6 +41,7 @@ test("cloud-api discovers preinstalled workspace hwpod-specs without hardcoded d assert.equal(payload.specs[0].name, "d601-f103-v2"); assert.equal(payload.specs[0].uid, "D601-F103-V2"); assert.equal(payload.specs[0].nodeId, "node-d601-f103-v2"); + assert.equal(payload.specs[0].source.caseRepo, "pikasTech/hwlab-case-registry"); assert.equal(payload.specs[0].source.caseId, "d601-f103-v2-compile"); } finally { await close(server); diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index dbaa494e..6bebc26d 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -9,7 +9,7 @@ import { artifactsFromKeilStatusForTest, extractKeilJobId, jobStatusCommandForTe test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", async () => { const root = await mkdtempCaseRoot(); - const caseRepo = path.join(root, "hwpod-cases"); + const caseRepo = path.join(root, "hwlab-case-registry"); const cwd = path.join(root, "hwlab"); const caseDir = path.join(caseRepo, "cases", "d601-f103-v2-compile"); try { @@ -17,7 +17,7 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); await mkdir(caseDir, { recursive: true }); await mkdir(cwd, { recursive: true }); - await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId: "d601-f103-v2-compile", hwpodSpec: "hwpod-spec.yaml" }, null, 2), "utf8"); + await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId: "d601-f103-v2-compile", hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main", subdir: "projects/01_baseline" } }, null, 2), "utf8"); await writeFile(path.join(caseDir, "hwpod-spec.yaml"), "kind: Hwpod\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n", "utf8"); const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo, "--run-id", "run-test"], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); @@ -25,6 +25,7 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", assert.equal(result.exitCode, 0); assert.equal(result.payload.action, "case.prepare.prepare"); assert.equal(result.payload.runId, "run-test"); + assert.equal(result.payload.caseRepo, caseRepo); assert.equal(result.payload.compileOnly, true); assert.match(await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-test", ".hwlab", "hwpod-spec.yaml"), "utf8"), /node-d601-f103-v2/u); } finally { @@ -32,6 +33,30 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", } }); +test("case prepare uses the new registry by default and refuses the removed hwpod-cases path", async () => { + const root = await mkdtempCaseRoot(); + const cwd = path.join(root, "hwlab"); + const newRepo = path.join(root, "hwlab-case-registry"); + const removedRepo = path.join(root, "hwpod-cases"); + const caseId = "d601-f103-v2-compile"; + try { + await createCaseRepo(newRepo, caseId); + await createCaseRepo(removedRepo, caseId); + await mkdir(cwd, { recursive: true }); + + const defaultResult = await runHwlabCli(["case", "prepare", caseId, "--run-id", "run-default"], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); + assert.equal(defaultResult.exitCode, 0); + assert.equal(defaultResult.payload.caseRepo, newRepo); + + const removedResult = await runHwlabCli(["case", "prepare", caseId, "--case-repo", removedRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z" }); + assert.equal(removedResult.exitCode, 1); + assert.equal(removedResult.payload.error.code, "removed_case_repo_path"); + assert.equal(removedResult.payload.error.details.standardCaseRepo, "/root/hwlab-case-registry"); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("case run extracts Keil async job ids from hwpod-node output", () => { const jobId = extractKeilJobId({ body: { results: [{ stdout: JSON.stringify({ ok: true, job_id: "20260605_203835_798515c0" }) }] } }); assert.equal(jobId, "20260605_203835_798515c0"); @@ -52,3 +77,12 @@ test("case run exposes Keil hex and axf artifacts in evidence summary", () => { async function mkdtempCaseRoot() { return await import("node:fs/promises").then(({ mkdtemp }) => mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-"))); } + +async function createCaseRepo(caseRepo: string, caseId: string) { + const caseDir = path.join(caseRepo, "cases", caseId); + await mkdir(path.join(caseRepo, ".git"), { recursive: true }); + await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); + await mkdir(caseDir, { recursive: true }); + await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId, hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main", subdir: "projects/01_baseline" } }, null, 2), "utf8"); + await writeFile(path.join(caseDir, "hwpod-spec.yaml"), "kind: Hwpod\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n", "utf8"); +} diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index d1be44e5..4e830ef8 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -8,6 +8,8 @@ const DEFAULT_API_URL = "http://74.48.78.17:19667"; const DEFAULT_POLL_INTERVAL_MS = 1000; const DEFAULT_JOB_TIMEOUT_MS = 50000; const MAX_JOB_TIMEOUT_MS = 50000; +const STANDARD_CASE_REPO_PATH = "/root/hwlab-case-registry"; +const REMOVED_CASE_REPO_PATH = "/root/hwpod-cases"; type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; @@ -54,11 +56,12 @@ export function caseHelp() { serviceRuntime: false, compileOnlyDefault: true, stateRoot: ".state/hwlab-cli/caserun", + standardCaseRepo: STANDARD_CASE_REPO_PATH, commands: [ - "case prepare CASE_ID --case-repo /root/hwpod-cases [--run-id RUN_ID]", - "case build CASE_ID --case-repo /root/hwpod-cases [--run-dir DIR]", - "case collect CASE_ID --case-repo /root/hwpod-cases --run-dir DIR", - "case run CASE_ID --case-repo /root/hwpod-cases" + `case prepare CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-id RUN_ID]`, + `case build CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-dir DIR]`, + `case collect CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} --run-dir DIR`, + `case run CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH}` ] }); } @@ -215,7 +218,13 @@ async function loadRunFromDirOrFail(context: CaseContext) { async function resolveCaseRepo(context: CaseContext) { const explicit = text(context.parsed.caseRepo ?? context.parsed.caseRepoPath ?? context.env.HWLAB_CASE_REPO); - const candidates = explicit ? [path.resolve(context.cwd, explicit)] : [path.resolve(context.cwd, "../hwpod-cases"), "/root/hwpod-cases"]; + if (explicit && isRemovedCaseRepoPath(path.resolve(context.cwd, explicit))) { + throw cliError("removed_case_repo_path", "the CaseRun registry moved to /root/hwlab-case-registry", { caseRepo: explicit, standardCaseRepo: STANDARD_CASE_REPO_PATH }); + } + const candidates = explicit ? [path.resolve(context.cwd, explicit)] : [ + path.resolve(context.cwd, "../hwlab-case-registry"), + STANDARD_CASE_REPO_PATH + ]; for (const candidate of candidates) { try { await readFile(path.join(candidate, ".git", "HEAD"), "utf8"); @@ -224,7 +233,12 @@ async function resolveCaseRepo(context: CaseContext) { // Try next visible candidate. } } - throw cliError("case_repo_required", "case repo is required and must be a git checkout", { option: "--case-repo", candidates }); + throw cliError("case_repo_required", "case repo is required and must be a git checkout", { option: "--case-repo", standardCaseRepo: STANDARD_CASE_REPO_PATH, candidates }); +} + +function isRemovedCaseRepoPath(candidate: string) { + const normalized = path.resolve(candidate); + return normalized === REMOVED_CASE_REPO_PATH || normalized.endsWith("/hwpod-cases"); } async function loadCaseDefinition(caseRepo: string, caseId: string) { diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index 86b14743..cd2e36cc 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -156,7 +156,7 @@ function help() { "hwlab-cli client harness submit --message TEXT --provider-profile deepseek|codex-api|minimax-m3", "hwlab-cli client harness wait TRACE_ID --timeout-ms 50000", "hwlab-cli client harness audit TRACE_ID", - "hwlab-cli case run CASE_ID --case-repo /root/hwpod-cases" + "hwlab-cli case run CASE_ID --case-repo /root/hwlab-case-registry" ] }); }