feat: add issue board audit cli

This commit is contained in:
Codex
2026-05-20 21:20:57 +00:00
parent f4c0f88312
commit 68ae2722ab
5 changed files with 597 additions and 5 deletions
+112 -1
View File
@@ -86,6 +86,28 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
created_at: "2026-05-20T00:00:00Z",
updated_at: "2026-05-20T01:00:00Z",
};
const boardIssue = {
...issue,
body: [
"# Code Queue",
"",
"## 看板(OPEN",
"",
"| Issue | Branch | 验收状态 | 相关任务 | 进度 |",
"| --- | --- | --- | --- | --- |",
"| #20 | master | meta | governance | active |",
"| #35 master:补齐 UniDesk CLI gh issue list 与 PR 驱动最小闭环前置能力 | master | pass | cq-35 | doing |",
"| #40 closed issue still in open | | | cq-40 | |",
"",
"## 看板(CLOSED",
"",
"| Issue | Branch | 验收状态 | 相关任务 | 进度 |",
"| --- | --- | --- | --- | --- |",
"| #24 | master | meta | brief | active |",
"| #36 second issue | master | pending | cq-36 | misplaced |",
"",
].join("\n"),
};
const issueList = [
{
id: 2001,
@@ -169,6 +191,64 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
updated_at: "2026-05-20T04:40:00Z",
},
];
const boardOpenIssues = [
{
id: 2000,
number: 20,
title: "长期总看板",
body: boardIssue.body,
state: "open",
html_url: "https://github.com/pikasTech/unidesk/issues/20",
comments: 1,
user: { login: "tester" },
labels: [],
created_at: "2026-05-20T00:00:00Z",
updated_at: "2026-05-20T01:00:00Z",
},
issueList[0],
issueList[1],
{
id: 2004,
number: 24,
title: "指挥简报",
body: "brief",
state: "open",
html_url: "https://github.com/pikasTech/unidesk/issues/24",
comments: 0,
user: { login: "tester" },
labels: [],
created_at: "2026-05-20T01:00:00Z",
updated_at: "2026-05-20T02:00:00Z",
},
];
const boardClosedIssues = [
{
id: 2040,
number: 40,
title: "closed issue still in open",
body: "closed body",
state: "closed",
html_url: "https://github.com/pikasTech/unidesk/issues/40",
comments: 0,
user: { login: "runner" },
labels: [],
created_at: "2026-05-19T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
},
{
id: 2041,
number: 41,
title: "closed issue missing from closed table",
body: "closed body",
state: "closed",
html_url: "https://github.com/pikasTech/unidesk/issues/41",
comments: 0,
user: { login: "runner" },
labels: [],
created_at: "2026-05-19T02:05:00Z",
updated_at: "2026-05-20T03:05:00Z",
},
];
const comments = [
{
id: 1,
@@ -206,7 +286,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
const body = await collectBody(req);
requests.push({ method: req.method ?? "", url: req.url ?? "", body });
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/20") {
sendJson(res, 200, issue);
sendJson(res, 200, boardIssue);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/20/comments?per_page=100") {
@@ -229,6 +309,14 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 200, scanIssues);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=100") {
sendJson(res, 200, boardOpenIssues);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=closed&per_page=100") {
sendJson(res, 200, boardClosedIssues);
return;
}
for (const [issueNumber, issueComments] of Object.entries(scanComments)) {
if (req.method === "GET" && req.url === `/repos/pikasTech/unidesk/issues/${issueNumber}/comments?per_page=100`) {
sendJson(res, 200, issueComments);
@@ -352,6 +440,28 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const cleanupPlanData = dataOf(cleanupPlan.json ?? {});
assertCondition(cleanupPlanData.command === "issue cleanup-plan" && cleanupPlanData.dryRun === true, "cleanup-plan should remain dry-run", cleanupPlanData);
const boardAuditRequestCountBefore = mock.requests.length;
const boardAudit = await runCli(["gh", "issue", "board-audit", "--repo", "pikasTech/unidesk", "--limit", "100", "--dry-run"], env);
assertCondition(boardAudit.status === 0, "issue board-audit should succeed as a read-only audit", boardAudit.json ?? { stdout: boardAudit.stdout, stderr: boardAudit.stderr });
const boardAuditData = dataOf(boardAudit.json ?? {});
assertCondition(boardAuditData.command === "issue board-audit" && boardAuditData.dryRun === true && boardAuditData.readOnly === true, "board-audit should be explicit read-only dry-run", boardAuditData);
const missingOpenIssues = boardAuditData.missingOpenIssues as JsonRecord[];
assertCondition(Array.isArray(missingOpenIssues) && missingOpenIssues.some((issue) => issue.number === 36), "board-audit should report open issue missing from OPEN table", missingOpenIssues);
const closedInOpenRows = boardAuditData.closedInOpenRows as JsonRecord[];
assertCondition(Array.isArray(closedInOpenRows) && closedInOpenRows.some((issue) => issue.number === 40), "board-audit should report closed issue still in OPEN table", closedInOpenRows);
const missingClosedRows = boardAuditData.missingClosedRows as JsonRecord[];
assertCondition(Array.isArray(missingClosedRows) && missingClosedRows.some((issue) => issue.number === 40) && missingClosedRows.some((issue) => issue.number === 41), "board-audit should report closed issues missing from CLOSED table", missingClosedRows);
const openInClosedRows = boardAuditData.openInClosedRows as JsonRecord[];
assertCondition(Array.isArray(openInClosedRows) && openInClosedRows.some((issue) => issue.number === 36), "board-audit should report open issue placed in CLOSED table", openInClosedRows);
const rowValidationWarnings = boardAuditData.rowValidationWarnings as JsonRecord[];
assertCondition(Array.isArray(rowValidationWarnings) && rowValidationWarnings.some((warning) => warning.issueNumber === 40 && Array.isArray(warning.missingColumns) && (warning.missingColumns as unknown[]).includes("branch") && (warning.missingColumns as unknown[]).includes("acceptance") && (warning.missingColumns as unknown[]).includes("progress")), "board-audit should report missing required board columns", rowValidationWarnings);
const ignoredIssues = boardAuditData.ignoredIssues as JsonRecord[];
assertCondition(Array.isArray(ignoredIssues) && ignoredIssues.some((issue) => issue.number === 20 && issue.reason === "known-meta") && ignoredIssues.some((issue) => issue.number === 24 && issue.reason === "known-meta"), "board-audit should ignore configured known meta issues", ignoredIssues);
const recommendedActions = boardAuditData.recommendedActions as JsonRecord[];
assertCondition(Array.isArray(recommendedActions) && recommendedActions.some((action) => action.action === "add-open-row" && action.issueNumber === 36) && recommendedActions.some((action) => action.action === "move-open-row-to-closed" && action.issueNumber === 40), "board-audit should emit machine-readable recommended actions", recommendedActions);
const boardAuditWriteCount = mock.requests.slice(boardAuditRequestCountBefore).filter((request) => request.method === "PATCH" || request.method === "DELETE" || request.method === "POST").length;
assertCondition(boardAuditWriteCount === 0, "board-audit must not write GitHub", { requests: mock.requests.slice(boardAuditRequestCountBefore) });
const badListField = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--json", "number,body"], env);
assertCondition(badListField.status !== 0, "issue list unsupported --json field should fail", badListField.json ?? { stdout: badListField.stdout });
const badListFieldData = failedDataOf(badListField.json ?? {});
@@ -518,6 +628,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"issue list default fields include labels and filter pull requests",
"issue scan-escape classifies pollution, explanatory mentions, and body risks",
"issue cleanup-plan remains dry-run with body/comment cleanup suggestions",
"issue board-audit reports missing open rows, closed/open section mismatches, missing closed rows, meta ignores, and row validation warnings without writes",
"issue create dry-run parses repeated/comma labels and exposes request plan",
"issue create sends labels through REST and preserves GitHub validation errors for missing labels",
"issue list unsupported fields and states fail structurally",