fix: migrate caserun registry to hwlab-case-registry
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 编译闭环,并把可审计产物稳定落盘。
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user