fix: guard hwlab product routing from issue 20

This commit is contained in:
Codex
2026-05-23 01:34:35 +00:00
parent 4f3cd04873
commit cb8335306f
6 changed files with 198 additions and 17 deletions
+95 -7
View File
@@ -737,7 +737,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(Array.isArray(boardRowGetRow.cells) && boardRowGetRow.cells[1] === "OPEN", "board-row get should expose the GitHub status column", boardRowGetRow);
assertCondition(boardRowGetFields.branch === "master" && boardRowGetFields.status === "pass" && boardRowGetFields.validation === "pass" && boardRowGetFields.tasks === "cq-35" && String(boardRowGetFields.focus ?? "").includes("当前关注点"), "board-row get should expose canonical field aliases", boardRowGetFields);
const boardRowGetHint = boardRowGetData.codeQueueBoardHint as JsonRecord;
assertCondition(boardRowGetHint.detected === false && String(boardRowGetHint.warning ?? "").includes("#20 is the long-term board only"), "board-row get should remind callers not to put daily briefs into #20", boardRowGetHint);
assertCondition(boardRowGetHint.detected === false && String(boardRowGetHint.warning ?? "").includes("governance board only"), "board-row get should remind callers that #20 is governance-only", boardRowGetHint);
const boardRowUpsertUpdateRequestCountBefore = mock.requests.length;
const boardRowUpsertUpdate = await runCli([
@@ -968,7 +968,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const boardRowAddNoGuardData = dataOf(boardRowAddNoGuard.json ?? {});
assertCondition(boardRowAddNoGuardData.dryRun === true && boardRowAddNoGuardData.planned === true, "board-row add without guard should not PATCH GitHub", boardRowAddNoGuardData);
const boardRowAddNoGuardPlan = boardRowAddNoGuardData.add as JsonRecord;
assertCondition(boardRowAddNoGuardPlan.section === "open" && boardRowAddNoGuardPlan.insertAfterLine > 0, "board-row add without guard should still return an insertion plan", boardRowAddNoGuardPlan);
assertCondition(boardRowAddNoGuardPlan.section === "open" && Number(boardRowAddNoGuardPlan.insertAfterLine ?? 0) > 0, "board-row add without guard should still return an insertion plan", boardRowAddNoGuardPlan);
const boardRowAddNoGuardWriteCount = mock.requests.slice(boardRowAddNoGuardRequestCountBefore).filter((request) => request.method === "PATCH").length;
assertCondition(boardRowAddNoGuardWriteCount === 0, "board-row add without guard must not PATCH GitHub", { requests: mock.requests.slice(boardRowAddNoGuardRequestCountBefore) });
@@ -978,7 +978,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const boardRowAddDryRunData = dataOf(boardRowAddDryRun.json ?? {});
assertCondition(boardRowAddDryRunData.command === "issue board-row add" && boardRowAddDryRunData.dryRun === true && boardRowAddDryRunData.planned === true, "board-row add should default to dry-run", boardRowAddDryRunData);
const boardRowAddDryRunPlan = boardRowAddDryRunData.add as JsonRecord;
assertCondition(boardRowAddDryRunPlan.section === "open" && boardRowAddDryRunPlan.validation.actualStatus === "OPEN", "board-row add dry-run should validate the target section and GitHub status", boardRowAddDryRunPlan);
const boardRowAddDryRunValidation = boardRowAddDryRunPlan.validation as JsonRecord;
assertCondition(boardRowAddDryRunPlan.section === "open" && boardRowAddDryRunValidation.actualStatus === "OPEN", "board-row add dry-run should validate the target section and GitHub status", boardRowAddDryRunPlan);
const boardRowAddDryRunPatchCount = mock.requests.slice(boardRowAddDryRunRequestCountBefore).filter((request) => request.method === "PATCH").length;
assertCondition(boardRowAddDryRunPatchCount === 0, "board-row add dry-run must not PATCH GitHub", { requests: mock.requests.slice(boardRowAddDryRunRequestCountBefore) });
@@ -992,7 +993,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const boardRowAddData = dataOf(boardRowAdd.json ?? {});
assertCondition(boardRowAddData.dryRun === false && boardRowAddData.rest === true, "board-row add should report a real REST update", boardRowAddData);
const boardRowAddPlan = boardRowAddData.add as JsonRecord;
assertCondition(boardRowAddPlan.section === "open" && boardRowAddPlan.validation.expectedStatus === "OPEN" && boardRowAddPlan.validation.actualStatus === "OPEN", "board-row add should validate section/status alignment", boardRowAddPlan);
const boardRowAddValidation = boardRowAddPlan.validation as JsonRecord;
assertCondition(boardRowAddPlan.section === "open" && boardRowAddValidation.expectedStatus === "OPEN" && boardRowAddValidation.actualStatus === "OPEN", "board-row add should validate section/status alignment", boardRowAddPlan);
const boardRowAddRequests = mock.requests.slice(boardRowAddRequestCountBefore).filter((request) => request.method === "PATCH" && request.url === "/repos/pikasTech/unidesk/issues/20");
assertCondition(boardRowAddRequests.length === 1, "board-row add should send exactly one PATCH", { requests: mock.requests.slice(boardRowAddRequestCountBefore) });
const boardRowAddPayload = JSON.parse(boardRowAddRequests[0]?.body ?? "{}") as JsonRecord;
@@ -1066,7 +1068,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const boardRowMoveDryRunData = dataOf(boardRowMoveDryRun.json ?? {});
assertCondition(boardRowMoveDryRunData.command === "issue board-row move" && boardRowMoveDryRunData.dryRun === true && boardRowMoveDryRunData.planned === true, "board-row move should default to dry-run", boardRowMoveDryRunData);
const boardRowMoveDryRunPlan = boardRowMoveDryRunData.move as JsonRecord;
assertCondition(boardRowMoveDryRunPlan.from === "open" && boardRowMoveDryRunPlan.to === "closed" && boardRowMoveDryRunPlan.status.requested === "CLOSED", "board-row move dry-run should plan the cross-section migration", boardRowMoveDryRunPlan);
const boardRowMoveDryRunStatus = boardRowMoveDryRunPlan.status as JsonRecord;
assertCondition(boardRowMoveDryRunPlan.from === "open" && boardRowMoveDryRunPlan.to === "closed" && boardRowMoveDryRunStatus.requested === "CLOSED", "board-row move dry-run should plan the cross-section migration", boardRowMoveDryRunPlan);
const boardRowMoveConflict = await runCli(["gh", "issue", "board-row", "move", "35", "--repo", "pikasTech/unidesk", "--board-issue", "20", "--to", "closed", "--status", "OPEN", "--dry-run"], env);
assertCondition(boardRowMoveConflict.status !== 0, "board-row move status conflict should fail structurally", boardRowMoveConflict.json ?? { stdout: boardRowMoveConflict.stdout, stderr: boardRowMoveConflict.stderr });
@@ -1082,7 +1085,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const boardRowMoveData = dataOf(boardRowMove.json ?? {});
assertCondition(boardRowMoveData.dryRun === false && boardRowMoveData.rest === true, "board-row move should report a real REST update", boardRowMoveData);
const boardRowMovePlan = boardRowMoveData.move as JsonRecord;
assertCondition(boardRowMovePlan.from === "open" && boardRowMovePlan.to === "closed" && boardRowMovePlan.status.new === "CLOSED", "board-row move should update the GitHub status column", boardRowMovePlan);
const boardRowMoveStatus = boardRowMovePlan.status as JsonRecord;
assertCondition(boardRowMovePlan.from === "open" && boardRowMovePlan.to === "closed" && boardRowMoveStatus.new === "CLOSED", "board-row move should update the GitHub status column", boardRowMovePlan);
const boardRowMoveLinePlan = boardRowMovePlan.linePlan as JsonRecord;
const boardRowMoveSectionPlan = boardRowMovePlan.sectionPlan as JsonRecord;
assertCondition(boardRowMoveLinePlan.action === "move" && Number(boardRowMoveLinePlan.sourceLineNumber ?? 0) > 0 && Number(boardRowMoveLinePlan.newLineNumber ?? 0) > 0, "board-row move should expose a line plan", boardRowMoveLinePlan);
@@ -1123,7 +1127,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const readIssue = readBodyData.issue as JsonRecord;
assertCondition(typeof readIssue.body === "string" && readIssue.body.includes("## 看板(OPEN"), ".data.issue.body should remain readable", readBodyData);
const readBodyHint = readBodyData.codeQueueBoardHint as JsonRecord;
assertCondition(readBodyHint.detected === false && String(readBodyHint.warning ?? "").includes("#20 is the long-term board only"), "issue read #20 should remind callers not to put daily briefs into #20", readBodyHint);
assertCondition(readBodyHint.detected === false && String(readBodyHint.warning ?? "").includes("governance board only"), "issue read #20 should remind callers that #20 is governance-only", readBodyHint);
const selectedJson = readBodyData.json as JsonRecord;
assertCondition(typeof selectedJson.body === "string" && selectedJson.body === readIssue.body, "selected json body should match issue body", readBodyData);
assertCondition(!("comments" in selectedJson), "--json body should not imply comments field", selectedJson);
@@ -1216,6 +1220,88 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const pollutedBoardHint = pollutedBoardGuard.codeQueueBoardHint as JsonRecord;
assertCondition(pollutedBoardHint.detected === true && String(pollutedBoardHint.route ?? "").includes("daily rolling commander brief"), "#20 guard should hint to move updates to the daily brief issue", pollutedBoardHint);
const hwlabProductBoardFile = join(tmp, "hwlab-product-board.md");
writeFileSync(hwlabProductBoardFile, [
"# Code Queue",
"",
"## 看板(OPEN",
"",
"| Issue | GitHub 状态 | Branch | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
"| --- | --- | --- | --- | --- | --- | --- |",
"| [pikasTech/HWLAB#108](https://github.com/pikasTech/HWLAB/issues/108) HWLAB user feedback | OPEN | main | product | cq-hwlab | Cloud Workbench 用户反馈 | doing |",
"",
].join("\n"), "utf8");
const hwlabProductBoard = await runCli(["gh", "issue", "update", "20", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", hwlabProductBoardFile, "--dry-run"], env);
assertCondition(hwlabProductBoard.status !== 0, "#20 body guard should reject HWLAB product issue rows", hwlabProductBoard.json ?? { stdout: hwlabProductBoard.stdout });
const hwlabProductBoardData = failedDataOf(hwlabProductBoard.json ?? {});
const hwlabProductBoardGuard = hwlabProductBoardData.guard as JsonRecord;
assertCondition(Array.isArray(hwlabProductBoardGuard.failures) && hwlabProductBoardGuard.failures.includes("code-queue-board-contains-hwlab-product-work"), "#20 guard should report HWLAB product routing pollution", hwlabProductBoardGuard);
const hwlabProductHint = hwlabProductBoardGuard.codeQueueBoardHint as JsonRecord;
const hwlabProductRouting = hwlabProductHint.hwlabProductRouting as JsonRecord;
assertCondition(hwlabProductRouting.detected === true && String(hwlabProductRouting.route ?? "").includes("pikasTech/HWLAB"), "#20 guard should route HWLAB product issues to the HWLAB repo", hwlabProductRouting);
const hwlabProductUpsert = await runCli([
"gh",
"issue",
"board-row",
"upsert",
"108",
"--repo",
"pikasTech/unidesk",
"--board-issue",
"20",
"--section",
"open",
"--branch",
"main",
"--tasks",
"cq-hwlab",
"--summary",
"pikasTech/HWLAB#108 HWLAB user feedback",
"--focus",
"Cloud Workbench 用户反馈",
"--validation",
"product",
"--progress",
"doing",
"--dry-run",
], env);
assertCondition(hwlabProductUpsert.status !== 0, "#20 board-row upsert should reject HWLAB product issue rows", hwlabProductUpsert.json ?? { stdout: hwlabProductUpsert.stdout });
const hwlabProductUpsertData = failedDataOf(hwlabProductUpsert.json ?? {});
const hwlabProductUpsertGuard = hwlabProductUpsertData.guard as JsonRecord;
assertCondition(Array.isArray(hwlabProductUpsertGuard.warnings) && hwlabProductUpsertGuard.warnings.includes("code-queue-board-contains-hwlab-product-work"), "board-row upsert guard should report HWLAB product routing pollution", hwlabProductUpsertGuard);
const hwlabGovernanceGuard = await runCli([
"gh",
"issue",
"board-row",
"upsert",
"109",
"--repo",
"pikasTech/unidesk",
"--board-issue",
"20",
"--section",
"open",
"--branch",
"master",
"--tasks",
"cq-guard",
"--summary",
"UniDesk CLI guard for HWLAB#108 routing",
"--focus",
"commander governance guard prevents HWLAB product misfile",
"--validation",
"dry-run guard",
"--progress",
"ready",
"--dry-run",
], env);
assertCondition(hwlabGovernanceGuard.status === 0, "#20 board-row upsert should allow UniDesk governance rows that mention HWLAB as routing context", hwlabGovernanceGuard.json ?? { stdout: hwlabGovernanceGuard.stdout });
const hwlabGovernanceGuardData = dataOf(hwlabGovernanceGuard.json ?? {});
const hwlabGovernanceGuardSummary = hwlabGovernanceGuardData.guard as JsonRecord;
assertCondition(hwlabGovernanceGuardSummary.ok === true && !(hwlabGovernanceGuardSummary.warnings as unknown[]).includes("code-queue-board-contains-hwlab-product-work"), "governance rows mentioning HWLAB should not be classified as HWLAB product work", hwlabGovernanceGuardSummary);
const commanderBriefBlocked = await runCli(["gh", "issue", "edit", "24", "--repo", "pikasTech/unidesk", "--body-file", missingHeadingFile, "--dry-run"], env);
assertCondition(commanderBriefBlocked.status !== 0, "#24 missing heading should fail", commanderBriefBlocked.json ?? { stdout: commanderBriefBlocked.stdout });
const commanderBriefData = failedDataOf(commanderBriefBlocked.json ?? {});
@@ -1397,6 +1483,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"unsupported --json fields fail structurally",
"issue edit --body-file rejects literal null",
"#20/#24 body profile guards reject missing headings or wrong profile",
"#20 body and board-row guards reject HWLAB product issue routing and point to pikasTech/HWLAB",
"#20 board-row guard allows UniDesk governance rows that mention HWLAB only as routing context",
"#24 commander-brief profile remains compatible",
"daily commander brief issues match commander-brief profile by title",
"non-brief issues fail commander-brief profile without printing token",