fix: migrate caserun registry to hwlab-case-registry

This commit is contained in:
Codex Agent
2026-06-06 10:09:39 +08:00
parent 6afdb83e01
commit 2a651d11d3
6 changed files with 62 additions and 13 deletions
+2 -2
View File
@@ -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"
}
}
+1 -1
View File
@@ -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/<caseId>/case.json` 与同目录 `hwpod-spec.yaml`;每次运行会把 spec 复制到 `.state/hwlab-cli/caserun/<runId>/.hwlab/hwpod-spec.yaml`,形成隔离的 run-local spec。
CaseRun 的 case registry repo 与 subject repo 必须分离。case registry repo 只保存 `case.json`、HWPOD spec 或 spec 引用、subject 引用、期望结果和 `runs/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。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/<caseId>/<runId>/` 审计产物;它不保存或替代实际固件、工程、测试样例或产品代码库。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 编译闭环,并把可审计产物稳定落盘。
+2 -1
View File
@@ -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);
+36 -2
View File
@@ -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");
}
+20 -6
View File
@@ -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<string, string | undefined>;
type ParsedArgs = Record<string, unknown> & { _: 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) {
+1 -1
View File
@@ -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"
]
});
}