fix: support comments on issue close
This commit is contained in:
@@ -79,6 +79,10 @@ function sendJson(res: ServerResponse, status: number, payload: unknown): void {
|
||||
res.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function mockUrl(rawUrl: string | undefined): URL {
|
||||
return new URL(rawUrl ?? "/", "http://mock.local");
|
||||
}
|
||||
|
||||
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.";
|
||||
@@ -470,9 +474,14 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
],
|
||||
53: [],
|
||||
};
|
||||
let unideskAllIssuesRequestCount = 0;
|
||||
const server = createServer(async (req, res) => {
|
||||
const body = await collectBody(req);
|
||||
requests.push({ method: req.method ?? "", url: req.url ?? "", body });
|
||||
const url = mockUrl(req.url);
|
||||
const state = url.searchParams.get("state") ?? "";
|
||||
const perPage = url.searchParams.get("per_page") ?? "";
|
||||
const page = url.searchParams.get("page") ?? "1";
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/20") {
|
||||
sendJson(res, 200, { ...issue, body: boardIssueBody, updated_at: boardIssueUpdatedAt });
|
||||
return;
|
||||
@@ -521,43 +530,24 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
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));
|
||||
if (req.method === "GET" && url.pathname === "/repos/pikasTech/unidesk/issues" && state === "open" && perPage === "100" && page === "1") {
|
||||
sendJson(res, 200, issueList);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/search/issues?q=AgentRun+final+response+repo%3ApikasTech%2Funidesk+type%3Aissue&per_page=4") {
|
||||
if (req.method === "GET" && url.pathname === "/search/issues" && url.searchParams.get("q") === "AgentRun final response repo:pikasTech/unidesk type:issue" && perPage === "100" && page === "1") {
|
||||
sendJson(res, 200, { total_count: 1, incomplete_results: false, items: issueList.slice(0, 1) });
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=5") {
|
||||
sendJson(res, 200, issueList);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/HWLAB/issues?state=open&per_page=2") {
|
||||
if (req.method === "GET" && url.pathname === "/repos/pikasTech/HWLAB/issues" && state === "open" && perPage === "100" && page === "1") {
|
||||
sendJson(res, 200, hwlabIssueList);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=all&per_page=3") {
|
||||
sendJson(res, 200, issueList);
|
||||
if (req.method === "GET" && url.pathname === "/repos/pikasTech/unidesk/issues" && state === "all" && perPage === "100" && page === "1") {
|
||||
unideskAllIssuesRequestCount += 1;
|
||||
sendJson(res, 200, unideskAllIssuesRequestCount === 1 ? issueList : scanIssues);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=all&per_page=4") {
|
||||
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=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") {
|
||||
if (req.method === "GET" && url.pathname === "/repos/pikasTech/unidesk/issues" && state === "closed" && perPage === "100" && page === "1") {
|
||||
sendJson(res, 200, boardClosedIssues);
|
||||
return;
|
||||
}
|
||||
@@ -570,8 +560,9 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
if (req.method === "PATCH" && req.url === "/repos/pikasTech/unidesk/issues/20") {
|
||||
const parsed = JSON.parse(body) as JsonRecord;
|
||||
boardIssueBody = String(parsed.body ?? boardIssueBody);
|
||||
const patchedState = typeof parsed.state === "string" ? parsed.state : issue.state;
|
||||
boardIssueUpdatedAt = "2026-05-20T01:05:00Z";
|
||||
sendJson(res, 200, { ...issue, body: boardIssueBody, updated_at: boardIssueUpdatedAt });
|
||||
sendJson(res, 200, { ...issue, body: boardIssueBody, state: patchedState, updated_at: boardIssueUpdatedAt });
|
||||
return;
|
||||
}
|
||||
if (req.method === "PATCH" && req.url === "/repos/pikasTech/unidesk/issues/62") {
|
||||
@@ -699,13 +690,19 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(listDefaultState.status === 0, "issue list default state should still succeed", listDefaultState.json ?? { stdout: listDefaultState.stdout });
|
||||
const listDefaultStateData = dataOf(listDefaultState.json ?? {});
|
||||
assertCondition(listDefaultStateData.state === "open", "issue list should keep default state=open", listDefaultStateData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=2"), "issue list default should query state=open", mock.requests);
|
||||
assertCondition(mock.requests.some((request) => {
|
||||
const requestedUrl = mockUrl(request.url);
|
||||
return request.method === "GET" && requestedUrl.pathname === "/repos/pikasTech/unidesk/issues" && requestedUrl.searchParams.get("state") === "open";
|
||||
}), "issue list default should query state=open", mock.requests);
|
||||
|
||||
const searchList = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "all", "--limit", "4", "--search", "AgentRun final response", "--json", "number,title,state,url"], env);
|
||||
assertCondition(searchList.status === 0, "issue list should support search query", searchList.json ?? { stdout: searchList.stdout });
|
||||
const searchListData = dataOf(searchList.json ?? {});
|
||||
assertCondition(searchListData.search === "AgentRun final response", "issue list should expose search query", searchListData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/search/issues?q=AgentRun+final+response+repo%3ApikasTech%2Funidesk+type%3Aissue&per_page=4"), "issue list search should use GitHub Search Issues API with repo/type qualifiers", mock.requests);
|
||||
assertCondition(mock.requests.some((request) => {
|
||||
const requestedUrl = mockUrl(request.url);
|
||||
return request.method === "GET" && requestedUrl.pathname === "/search/issues" && requestedUrl.searchParams.get("q") === "AgentRun final response repo:pikasTech/unidesk type:issue";
|
||||
}), "issue list search should use GitHub Search Issues API with repo/type qualifiers", mock.requests);
|
||||
|
||||
const positionalRepoList = await runCli(["gh", "issue", "list", "pikasTech/HWLAB", "--state", "open", "--limit", "2", "--json", "number,title,state,url"], env);
|
||||
assertCondition(positionalRepoList.status === 0, "issue list positional owner/repo should succeed", positionalRepoList.json ?? { stdout: positionalRepoList.stdout });
|
||||
@@ -713,7 +710,10 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(positionalRepoListData.repo === "pikasTech/HWLAB", "issue list positional repo should become the actual request repo", positionalRepoListData);
|
||||
const positionalRepoIssues = positionalRepoListData.issues as JsonRecord[];
|
||||
assertCondition(Array.isArray(positionalRepoIssues) && positionalRepoIssues[0]?.number === 7, "issue list positional repo should return HWLAB fixture issue", positionalRepoListData);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/HWLAB/issues?state=open&per_page=2"), "issue list positional repo should query derived repo REST path", mock.requests);
|
||||
assertCondition(mock.requests.some((request) => {
|
||||
const requestedUrl = mockUrl(request.url);
|
||||
return request.method === "GET" && requestedUrl.pathname === "/repos/pikasTech/HWLAB/issues" && requestedUrl.searchParams.get("state") === "open";
|
||||
}), "issue list positional repo should query derived repo REST path", mock.requests);
|
||||
|
||||
const positionalRepoConflict = await runCli(["gh", "issue", "list", "pikasTech/HWLAB", "--repo", "pikasTech/unidesk", "--state", "open"], env);
|
||||
assertCondition(positionalRepoConflict.status !== 0, "issue list conflicting positional repo and --repo should fail", positionalRepoConflict.json ?? { stdout: positionalRepoConflict.stdout });
|
||||
@@ -1258,12 +1258,10 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const issueConflictCommands = shorthandConflictData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue read 7 --repo pikasTech/HWLAB --json body,title,state,comments"), "conflict should include the exact supported issue read command", shorthandConflictData);
|
||||
|
||||
const rawUnsupported = await runCli(["gh", "issue", "list", "--raw"], env);
|
||||
assertCondition(rawUnsupported.status !== 0, "--raw outside read/view should fail structurally", rawUnsupported.json ?? { stdout: rawUnsupported.stdout });
|
||||
const rawUnsupportedData = failedDataOf(rawUnsupported.json ?? {});
|
||||
assertCondition(rawUnsupportedData.degradedReason === "validation-failed", "unsupported --raw scope should be validation-failed", rawUnsupportedData);
|
||||
const rawUnsupportedCommands = rawUnsupportedData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(rawUnsupportedCommands) && rawUnsupportedCommands.some((command) => command.includes("gh issue read owner/name#<number> --raw")), "unsupported raw should suggest read/view raw command shape", rawUnsupportedData);
|
||||
const rawIssueList = await runCli(["gh", "issue", "list", "--raw"], env);
|
||||
assertCondition(rawIssueList.status === 0, "issue list --raw should be a supported explicit list disclosure path", rawIssueList.json ?? { stdout: rawIssueList.stdout });
|
||||
const rawIssueListData = dataOf(rawIssueList.json ?? {});
|
||||
assertCondition(rawIssueListData.command === "issue list" && rawIssueListData.rawCount === 3, "issue list --raw should keep compact list semantics with raw pagination metadata", rawIssueListData);
|
||||
|
||||
const readFields = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,title,state,comments"], env);
|
||||
assertCondition(readFields.status === 0, "common --json field selection should succeed", readFields.json ?? { stdout: readFields.stdout });
|
||||
@@ -1649,6 +1647,39 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const inlinePayload = JSON.parse(inlinePost?.body ?? "{}") as JsonRecord;
|
||||
assertCondition(inlinePayload.body === inlineBody, "inline issue comment REST payload should preserve short text", inlinePayload);
|
||||
|
||||
const closeComment = "收口:CLI close --comment contract";
|
||||
const closeDryRunRequestCountBefore = mock.requests.length;
|
||||
const closeDryRun = await runCli(["gh", "issue", "close", "20", "--repo", "pikasTech/unidesk", "--comment", closeComment, "--dry-run"], env);
|
||||
assertCondition(closeDryRun.status === 0, "issue close --comment dry-run should succeed", closeDryRun.json ?? { stdout: closeDryRun.stdout });
|
||||
assertCondition(closeDryRun.json?.command === "gh issue close 20 --repo pikasTech/unidesk --comment <body:redacted> --dry-run", "outer gh command should redact lifecycle comment", closeDryRun.json ?? {});
|
||||
const closeDryRunData = dataOf(closeDryRun.json ?? {});
|
||||
assertCondition(closeDryRunData.command === "issue close" && closeDryRunData.dryRun === true, "issue close dry-run should report lifecycle command", closeDryRunData);
|
||||
const closeDryRunComment = closeDryRunData.comment as JsonRecord;
|
||||
assertCondition(closeDryRunComment.planned === true && closeDryRunComment.source === "inline", "issue close dry-run should plan inline lifecycle comment", closeDryRunComment);
|
||||
assertCondition(String(closeDryRunComment.bodyPreview ?? "") === closeComment && typeof closeDryRunComment.bodySha === "string", "issue close dry-run should expose bounded comment metadata", closeDryRunComment);
|
||||
const closeDryRunWriteCount = mock.requests.slice(closeDryRunRequestCountBefore).filter((request) => request.method === "POST" || request.method === "PATCH").length;
|
||||
assertCondition(closeDryRunWriteCount === 0, "issue close --comment dry-run must not POST or PATCH", { requests: mock.requests.slice(closeDryRunRequestCountBefore) });
|
||||
|
||||
const closeWriteRequestCountBefore = mock.requests.length;
|
||||
const closeWrite = await runCli(["gh", "issue", "close", "20", "--repo", "pikasTech/unidesk", "--comment", closeComment], env);
|
||||
assertCondition(closeWrite.status === 0, "issue close --comment should succeed", closeWrite.json ?? { stdout: closeWrite.stdout });
|
||||
const closeWriteData = dataOf(closeWrite.json ?? {});
|
||||
assertCondition(closeWriteData.command === "issue close", "issue close write should report lifecycle command", closeWriteData);
|
||||
const closeWriteComment = closeWriteData.comment as JsonRecord;
|
||||
assertCondition(closeWriteComment.bodyOmitted === true && closeWriteComment.bodyPreview === closeComment, "issue close should summarize lifecycle comment without full body", closeWriteComment);
|
||||
const closeWriteIssue = closeWriteData.issue as JsonRecord;
|
||||
assertCondition(closeWriteIssue.state === "closed", "issue close should PATCH state=closed", closeWriteIssue);
|
||||
const closeWriteRequests = mock.requests.slice(closeWriteRequestCountBefore).filter((request) => request.url === "/repos/pikasTech/unidesk/issues/20/comments" || request.url === "/repos/pikasTech/unidesk/issues/20");
|
||||
assertCondition(closeWriteRequests.length === 2 && closeWriteRequests[0]?.method === "POST" && closeWriteRequests[1]?.method === "PATCH", "issue close --comment should POST comment before PATCH state", closeWriteRequests);
|
||||
const closeCommentPayload = JSON.parse(closeWriteRequests[0]?.body ?? "{}") as JsonRecord;
|
||||
const closePatchPayload = JSON.parse(closeWriteRequests[1]?.body ?? "{}") as JsonRecord;
|
||||
assertCondition(closeCommentPayload.body === closeComment && closePatchPayload.state === "closed", "issue close --comment payloads should preserve comment and state", { closeCommentPayload, closePatchPayload });
|
||||
|
||||
const closeCommentWrongCommand = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--comment", closeComment, "--dry-run"], env);
|
||||
assertCondition(closeCommentWrongCommand.status !== 0, "--comment outside issue close/reopen should fail structurally", closeCommentWrongCommand.json ?? { stdout: closeCommentWrongCommand.stdout });
|
||||
const closeCommentWrongData = failedDataOf(closeCommentWrongCommand.json ?? {});
|
||||
assertCondition(failureMessageOf(closeCommentWrongData).includes("only supported by gh issue close/reopen"), "wrong --comment usage should point to close/reopen", closeCommentWrongData);
|
||||
|
||||
const missingCommentBody = await runCli(["gh", "issue", "comment", "create", "36", "--repo", "pikasTech/unidesk", "--dry-run"], env);
|
||||
assertCondition(missingCommentBody.status !== 0, "issue comment create without body source should fail", missingCommentBody.json ?? { stdout: missingCommentBody.stdout });
|
||||
const missingCommentBodyData = failedDataOf(missingCommentBody.json ?? {});
|
||||
|
||||
Reference in New Issue
Block a user