fix: 压缩 gh issue update 成功输出

This commit is contained in:
Codex
2026-05-24 11:34:46 +00:00
parent e54493d3de
commit 026ab7d454
3 changed files with 146 additions and 18 deletions
+60 -3
View File
@@ -74,6 +74,8 @@ function sendJson(res: ServerResponse, status: number, payload: unknown): void {
async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockRequest[]; close: () => Promise<void> }> {
const requests: MockRequest[] = [];
let shorthandIssueBody = "HWLAB-style shorthand body fixture\n\nThis is generic CLI coverage, not product data.";
let shorthandIssueUpdatedAt = "2026-05-20T03:00:00Z";
const issue = {
id: 2000,
number: 20,
@@ -90,13 +92,13 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
id: 7000,
number: 7,
title: "generic shorthand fixture",
body: "HWLAB-style shorthand body fixture\n\nThis is generic CLI coverage, not product data.",
body: shorthandIssueBody,
state: "open",
html_url: "https://github.com/pikasTech/HWLAB/issues/7",
comments: 1,
user: { login: "tester" },
created_at: "2026-05-20T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
updated_at: shorthandIssueUpdatedAt,
};
const boardIssueBodyInitial = [
"# Code Queue",
@@ -469,7 +471,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/HWLAB/issues/7") {
sendJson(res, 200, shorthandIssue);
sendJson(res, 200, { ...shorthandIssue, body: shorthandIssueBody, updated_at: shorthandIssueUpdatedAt });
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/24") {
@@ -568,6 +570,13 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 200, { ...upsertBoardIssue, body: upsertBoardIssueBody, updated_at: upsertBoardIssueUpdatedAt });
return;
}
if (req.method === "PATCH" && req.url === "/repos/pikasTech/HWLAB/issues/7") {
const parsed = JSON.parse(body) as JsonRecord;
shorthandIssueBody = String(parsed.body ?? shorthandIssueBody);
shorthandIssueUpdatedAt = "2026-05-20T03:05:00Z";
sendJson(res, 200, { ...shorthandIssue, body: shorthandIssueBody, updated_at: shorthandIssueUpdatedAt });
return;
}
if (req.method === "POST" && req.url === "/repos/pikasTech/unidesk/issues/20/comments") {
const parsed = JSON.parse(body) as JsonRecord;
sendJson(res, 201, { id: 9001, body: String(parsed.body ?? ""), html_url: "https://github.com/pikasTech/unidesk/issues/20#issuecomment-9001", user: { login: "tester" }, created_at: "2026-05-20T06:00:00Z", updated_at: "2026-05-20T06:00:00Z" });
@@ -1499,6 +1508,52 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(replaceDryRun.status === 0, "issue update replace dry-run should succeed", replaceDryRun.json ?? { stdout: replaceDryRun.stdout });
const replaceData = dataOf(replaceDryRun.json ?? {});
assertCondition(replaceData.command === "issue update" && replaceData.mode === "replace", "replace mode should be explicit", replaceData);
const replaceDisclosure = replaceData.disclosure as JsonRecord;
const replaceReadCommands = replaceData.readCommands as JsonRecord;
assertCondition(replaceDisclosure.bodyOmitted === true && replaceDisclosure.dryRunBoundedPreview === true, "issue update dry-run should disclose compact body policy", replaceDisclosure);
assertCondition(typeof replaceReadCommands.full === "string" && String(replaceReadCommands.full).includes("gh issue read 20"), "issue update dry-run should expose full body drill-down", replaceReadCommands);
const replaceWouldPatch = replaceData.wouldPatch as JsonRecord;
assertCondition(typeof replaceWouldPatch.bodySha === "string" && String(replaceWouldPatch.bodySha).length === 64, "issue update dry-run should include wouldPatch body sha", replaceWouldPatch);
assertCondition(Number(replaceWouldPatch.bodyChars ?? 0) === Number(replaceData.bodyChars ?? 0), "issue update dry-run wouldPatch should include final body chars", replaceWouldPatch);
const compactLongBody = Array.from({ length: 260 }, (_, index) => `compact-success-line-${String(index + 1).padStart(4, "0")} ${"x".repeat(80)}`).join("\n");
const compactLongFile = join(tmp, "compact-long-body.md");
writeFileSync(compactLongFile, compactLongBody, "utf8");
const compactUpdateRequestCountBefore = mock.requests.length;
const compactUpdate = await runCli(["gh", "issue", "update", "7", "--repo", "pikasTech/HWLAB", "--mode", "replace", "--body-file", compactLongFile], env);
assertCondition(compactUpdate.status === 0, "issue update non-dry-run compact success should succeed", compactUpdate.json ?? { stdout: compactUpdate.stdout, stderr: compactUpdate.stderr });
assertCondition(compactUpdate.stdout.length < 20_000, "issue update compact success stdout should stay bounded for long bodies", { bytes: compactUpdate.stdout.length });
assertCondition(!compactUpdate.stdout.includes("compact-success-line-0260"), "default issue update success stdout must not echo the full long body tail", { tail: compactUpdate.stdout.slice(-1000) });
const compactUpdateData = dataOf(compactUpdate.json ?? {});
const compactIssue = compactUpdateData.issue as JsonRecord;
assertCondition(compactUpdateData.command === "issue update" && compactUpdateData.rest === true, "compact update should report REST success", compactUpdateData);
assertCondition(!("body" in compactIssue), "default issue update success should omit issue.body", compactIssue);
assertCondition(compactIssue.bodyOmitted === true && compactIssue.fullBodyIncluded === false, "compact issue summary should mark omitted full body", compactIssue);
assertCondition(Number(compactIssue.bodyChars ?? 0) === compactLongBody.length, "compact issue summary should include bodyChars", compactIssue);
assertCondition(typeof compactIssue.bodySha === "string" && String(compactIssue.bodySha).length === 64, "compact issue summary should include bodySha", compactIssue);
assertCondition(String(compactIssue.bodyPreview ?? "").includes("compact-success-line-0001") && !String(compactIssue.bodyPreview ?? "").includes("compact-success-line-0260"), "compact issue summary should include only bounded preview", compactIssue);
const compactConcurrency = compactUpdateData.concurrency as JsonRecord;
assertCondition(compactConcurrency.checked === false && compactConcurrency.expectBodySha === null, "compact update should still report concurrency summary", compactConcurrency);
const compactGuard = compactUpdateData.guard as JsonRecord;
assertCondition(compactGuard.ok === true && typeof compactGuard.bodySha === "string", "compact update should keep guard/body sha summary", compactGuard);
const compactDisclosure = compactUpdateData.disclosure as JsonRecord;
assertCondition(compactDisclosure.bodyOmitted === true && compactDisclosure.fullBodyIncluded === false && compactDisclosure.defaultCompact === true, "compact update disclosure should be explicit", compactDisclosure);
const compactCommands = compactUpdateData.readCommands as JsonRecord;
assertCondition(String(compactCommands.body ?? "").includes("gh issue read 7 --repo pikasTech/HWLAB --json body"), "compact update should expose body read command", compactCommands);
assertCondition(String(compactCommands.full ?? "").includes("--full") && String(compactCommands.raw ?? "").includes("--raw"), "compact update should expose full/raw drill-down", compactCommands);
const compactUpdatePatchCount = mock.requests.slice(compactUpdateRequestCountBefore).filter((request) => request.method === "PATCH" && request.url === "/repos/pikasTech/HWLAB/issues/7").length;
assertCondition(compactUpdatePatchCount === 1, "compact update should PATCH GitHub exactly once", { requests: mock.requests.slice(compactUpdateRequestCountBefore) });
const explicitFullBody = "# compact full body\n\nThis body is intentionally short enough to avoid global dump while still proving explicit disclosure.\n";
const explicitFullFile = join(tmp, "explicit-full-body.md");
writeFileSync(explicitFullFile, explicitFullBody, "utf8");
const explicitFullUpdate = await runCli(["gh", "issue", "update", "7", "--repo", "pikasTech/HWLAB", "--mode", "replace", "--body-file", explicitFullFile, "--full"], env);
assertCondition(explicitFullUpdate.status === 0, "issue update --full should succeed", explicitFullUpdate.json ?? { stdout: explicitFullUpdate.stdout, stderr: explicitFullUpdate.stderr });
const explicitFullData = dataOf(explicitFullUpdate.json ?? {});
const explicitFullIssue = explicitFullData.issue as JsonRecord;
assertCondition(typeof explicitFullIssue.body === "string" && explicitFullIssue.body === explicitFullBody, "issue update --full should explicitly include the full body", explicitFullIssue);
const explicitFullDisclosure = explicitFullData.disclosure as JsonRecord;
assertCondition(explicitFullDisclosure.fullBodyIncluded === true && explicitFullDisclosure.explicitFullDisclosure === true, "issue update --full disclosure should mark full body inclusion", explicitFullDisclosure);
const commentCreate = await runCli(["gh", "issue", "comment", "create", "20", "--repo", "pikasTech/unidesk", "--body-file", appendFile], env);
assertCondition(commentCreate.status === 0, "issue comment create should succeed", commentCreate.json ?? { stdout: commentCreate.stdout });
@@ -1559,6 +1614,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"multiline Markdown and backticks are not polluted",
"expect-updated-at stale write protection blocks PATCH",
"issue update replace/append modes preserve Markdown",
"issue update non-dry-run success defaults to compact output without full issue.body and exposes bodySha plus drill-down commands",
"issue update --full explicitly includes full issue.body",
"issue comment create/delete follows CRUD shape",
"issue hard delete is structurally unsupported",
],