fix: reduce board audit noise
This commit is contained in:
@@ -93,21 +93,38 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
"",
|
||||
"## 看板(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 | |",
|
||||
"| Issue | Branch | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
"| #20 | master | meta | governance | 关注:#19 / #26 / #30 | active |",
|
||||
"| #35 | master | pass | cq-35 | 当前关注点:#19 / #26 / #30 | doing |",
|
||||
"| #40 | | | cq-40 | 复核:#19 / #26 / #30 | |",
|
||||
"",
|
||||
"## 看板(CLOSED)",
|
||||
"",
|
||||
"| Issue | Branch | 验收状态 | 相关任务 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
"| #24 | master | meta | brief | active |",
|
||||
"| #36 second issue | master | pending | cq-36 | misplaced |",
|
||||
"| Issue | Branch | 验收状态 | 相关 Code Queue 任务 | 当前关注点 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
"| #24 | master | meta | brief | 常驻:#24 / #20 | active |",
|
||||
"| #36 | master | pending | cq-36 | 复核:#35 / #40 | done |",
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
const legacyBoardIssue = {
|
||||
...issue,
|
||||
id: 2600,
|
||||
number: 60,
|
||||
title: "遗留总看板",
|
||||
body: [
|
||||
"# Code Queue",
|
||||
"",
|
||||
"## 看板(OPEN)",
|
||||
"",
|
||||
"| 当前项 | Branch | 验收状态 | 相关任务 | 进度 |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
"| #101 / #102 说明:#101 / #102 | master | pass | cq-legacy | doing |",
|
||||
"",
|
||||
].join("\n"),
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/60",
|
||||
};
|
||||
const issueList = [
|
||||
{
|
||||
id: 2001,
|
||||
@@ -221,6 +238,23 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
updated_at: "2026-05-20T02:00:00Z",
|
||||
},
|
||||
];
|
||||
const legacyBoardOpenIssues = [
|
||||
{
|
||||
id: 2600,
|
||||
number: 60,
|
||||
title: "遗留总看板",
|
||||
body: legacyBoardIssue.body,
|
||||
state: "open",
|
||||
html_url: "https://github.com/pikasTech/unidesk/issues/60",
|
||||
comments: 0,
|
||||
user: { login: "tester" },
|
||||
labels: [],
|
||||
created_at: "2026-05-20T01:00:00Z",
|
||||
updated_at: "2026-05-20T02:00:00Z",
|
||||
},
|
||||
issueList[0],
|
||||
issueList[1],
|
||||
];
|
||||
const boardClosedIssues = [
|
||||
{
|
||||
id: 2040,
|
||||
@@ -289,10 +323,18 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, boardIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/60") {
|
||||
sendJson(res, 200, legacyBoardIssue);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/20/comments?per_page=100") {
|
||||
sendJson(res, 200, comments);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/60/comments?per_page=100") {
|
||||
sendJson(res, 200, []);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=2") {
|
||||
sendJson(res, 200, issueList.slice(0, 2));
|
||||
return;
|
||||
@@ -313,6 +355,14 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, boardOpenIssues);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=60") {
|
||||
sendJson(res, 200, legacyBoardOpenIssues);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=closed&per_page=60") {
|
||||
sendJson(res, 200, boardClosedIssues);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=closed&per_page=100") {
|
||||
sendJson(res, 200, boardClosedIssues);
|
||||
return;
|
||||
@@ -455,6 +505,8 @@ export async function runGhCliIssueGuardContract(): Promise<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);
|
||||
assertCondition(!rowValidationWarnings.some((warning) => warning.kind === "multiple-issue-references"), "board-audit should not treat explanatory issue references outside the Issue column as row-key conflicts", rowValidationWarnings);
|
||||
assertCondition(!rowValidationWarnings.some((warning) => Array.isArray(warning.missingColumns) && (warning.missingColumns as unknown[]).includes("relatedTask")), "board-audit should recognize relatedTask from the current header", 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[];
|
||||
@@ -462,6 +514,15 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
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 legacyBoardAuditRequestCountBefore = mock.requests.length;
|
||||
const legacyBoardAudit = await runCli(["gh", "issue", "board-audit", "--repo", "pikasTech/unidesk", "--board-issue", "60", "--limit", "60", "--dry-run"], env);
|
||||
assertCondition(legacyBoardAudit.status === 0, "legacy board-audit fixture should succeed", legacyBoardAudit.json ?? { stdout: legacyBoardAudit.stdout, stderr: legacyBoardAudit.stderr });
|
||||
const legacyBoardAuditData = dataOf(legacyBoardAudit.json ?? {});
|
||||
const legacyWarnings = legacyBoardAuditData.rowValidationWarnings as JsonRecord[];
|
||||
assertCondition(Array.isArray(legacyWarnings) && legacyWarnings.some((warning) => warning.kind === "multiple-issue-references" && warning.issueNumber === 101), "legacy board-audit fixture should keep whole-row fallback when no Issue column exists", legacyWarnings);
|
||||
const legacyBoardWriteCount = mock.requests.slice(legacyBoardAuditRequestCountBefore).filter((request) => request.method === "PATCH" || request.method === "DELETE" || request.method === "POST").length;
|
||||
assertCondition(legacyBoardWriteCount === 0, "legacy board-audit must not write GitHub", { requests: mock.requests.slice(legacyBoardAuditRequestCountBefore) });
|
||||
|
||||
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 ?? {});
|
||||
|
||||
+19
-5
@@ -1228,11 +1228,19 @@ function boardHeaderColumnKind(header: string): BoardRequiredColumn | null {
|
||||
const normalized = normalizeBoardHeader(header);
|
||||
if (["branch", "分支", "目标分支", "工作分支"].includes(normalized)) return "branch";
|
||||
if (["acceptance", "验收", "验收状态", "验收结果", "验收标准"].includes(normalized)) return "acceptance";
|
||||
if (["relatedtask", "task", "codequeue", "codequeuetask", "相关任务", "任务", "codequeue任务", "cq任务"].includes(normalized)) return "relatedTask";
|
||||
if (["relatedtask", "task", "codequeue", "codequeuetask", "相关任务", "任务", "codequeue任务", "cq任务", "相关codequeue任务", "相关codequeuetask"].includes(normalized)) return "relatedTask";
|
||||
if (["progress", "进度", "状态", "当前进度"].includes(normalized)) return "progress";
|
||||
return null;
|
||||
}
|
||||
|
||||
function boardIssueColumnIndex(headers: string[]): number | null {
|
||||
for (let index = 0; index < headers.length; index += 1) {
|
||||
const normalized = normalizeBoardHeader(headers[index]);
|
||||
if (["issue", "issue#", "issuenumber", "issueid", "issue编号", "issue号", "编号"].includes(normalized)) return index;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function markdownCells(line: string): string[] {
|
||||
const trimmed = line.trim();
|
||||
const withoutOuterPipes = trimmed.replace(/^\|/u, "").replace(/\|$/u, "");
|
||||
@@ -1303,12 +1311,14 @@ function parseBoardTables(body: string): { sections: BoardTableSection[]; warnin
|
||||
const kind = boardHeaderColumnKind(header);
|
||||
if (kind !== null && !columnMap.has(kind)) columnMap.set(kind, headerIndex);
|
||||
});
|
||||
const issueColumnIndex = boardIssueColumnIndex(headers);
|
||||
const rows: BoardTableRow[] = [];
|
||||
let rowIndex = index + 2;
|
||||
while (rowIndex < lines.length && lines[rowIndex].trim().startsWith("|")) {
|
||||
if (!isMarkdownTableSeparator(lines[rowIndex])) {
|
||||
const cells = markdownCells(lines[rowIndex]);
|
||||
const issueNumbers = extractIssueNumbers(lines[rowIndex]);
|
||||
const issueCell = issueColumnIndex === null ? undefined : cells[issueColumnIndex];
|
||||
const issueNumbers = issueColumnIndex === null ? extractIssueNumbers(lines[rowIndex]) : extractIssueNumbers(issueCell ?? "");
|
||||
const columns: Partial<Record<BoardRequiredColumn, string>> = {};
|
||||
for (const column of BOARD_AUDIT_REQUIRED_COLUMNS) {
|
||||
const cellIndex = columnMap.get(column);
|
||||
@@ -1321,7 +1331,9 @@ function parseBoardTables(body: string): { sections: BoardTableSection[]; warnin
|
||||
cells,
|
||||
issueNumbers,
|
||||
issueNumber: issueNumbers[0] ?? null,
|
||||
title: cells.find((cell) => extractIssueNumbers(cell).length > 0) ?? cells[0] ?? null,
|
||||
title: issueColumnIndex === null
|
||||
? (cells.find((cell) => extractIssueNumbers(cell).length > 0) ?? cells[0] ?? null)
|
||||
: (issueCell ?? cells[0] ?? null),
|
||||
columns,
|
||||
};
|
||||
rows.push(row);
|
||||
@@ -1341,7 +1353,9 @@ function parseBoardTables(body: string): { sections: BoardTableSection[]; warnin
|
||||
section: currentKind,
|
||||
lineNumber: row.lineNumber,
|
||||
kind: "multiple-issue-references",
|
||||
message: "Board table row contains multiple issue references; audit uses the first number as the row key.",
|
||||
message: issueColumnIndex === null
|
||||
? "Board table row contains multiple issue references; audit uses the first number as the row key."
|
||||
: "Board table row contains multiple issue references in the Issue column; audit uses the first number as the row key.",
|
||||
rowPreview: preview(row.raw),
|
||||
});
|
||||
}
|
||||
@@ -2862,7 +2876,7 @@ export function ghHelp(): unknown {
|
||||
"When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
|
||||
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments; GitHub Markdown writes intentionally use --body-file only.",
|
||||
"issue scan-escape classifies literal \\n findings as suspected-pollution, explanatory-mention, or risk, and emits cleanupSuggestions with body/comment ids plus diff-like previews. cleanup-plan is an alias that remains dry-run/read-only.",
|
||||
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It compares GitHub open/closed issue lists with the board OPEN/CLOSED tables and reports missingOpenIssues, closedInOpenRows, missingClosedRows, rowValidationWarnings, ignoredIssues, and recommendedActions. #20 and #24 are known meta issues by default.",
|
||||
"issue board-audit is read-only and defaults to repo pikasTech/unidesk plus board issue #20. It compares GitHub open/closed issue lists with the board OPEN/CLOSED tables and reports missingOpenIssues, closedInOpenRows, missingClosedRows, rowValidationWarnings, ignoredIssues, and recommendedActions. When an Issue column exists, row.issueNumber is taken from that column; #20 and #24 are known meta issues by default.",
|
||||
"issue edit 24 --notify-claudeqq-brief-diff reads the old issue body, PATCHes the new body, and sends only newly added '## 更新 ... 北京时间' sections to ClaudeQQ; ClaudeQQ failure does not roll back GitHub.",
|
||||
"Commander brief ClaudeQQ defaults to private target 645275593 through backend-core /api/microservices/claudeqq/proxy; UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_* env vars can override target, base URL, timeout, and enabled state.",
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
|
||||
Reference in New Issue
Block a user