Add gh issue list REST contract

This commit is contained in:
Codex
2026-05-20 12:27:12 +00:00
parent 688851414a
commit 4b65f713fa
5 changed files with 312 additions and 33 deletions
@@ -86,6 +86,48 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
created_at: "2026-05-20T00:00:00Z",
updated_at: "2026-05-20T01:00:00Z",
};
const issueList = [
{
id: 2001,
number: 35,
title: "master:补齐 UniDesk CLI gh issue list 与 PR 驱动最小闭环前置能力",
body: "issue list body",
state: "open",
html_url: "https://github.com/pikasTech/unidesk/issues/35",
comments: 0,
user: { login: "commander" },
labels: [{ name: "cli", color: "1d76db", description: "CLI work" }],
created_at: "2026-05-20T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
},
{
id: 2002,
number: 36,
title: "second issue",
body: "second body",
state: "open",
html_url: "https://github.com/pikasTech/unidesk/issues/36",
comments: 0,
user: { login: "runner" },
labels: [],
created_at: "2026-05-20T02:05:00Z",
updated_at: "2026-05-20T03:05:00Z",
},
{
id: 3001,
number: 37,
title: "pull request should be filtered",
body: "pr body",
state: "open",
html_url: "https://github.com/pikasTech/unidesk/pull/37",
comments: 0,
user: { login: "runner" },
labels: [],
pull_request: { html_url: "https://github.com/pikasTech/unidesk/pull/37" },
created_at: "2026-05-20T02:10:00Z",
updated_at: "2026-05-20T03:10:00Z",
},
];
const comments = [
{
id: 1,
@@ -107,6 +149,18 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 200, comments);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues?state=open&per_page=2") {
sendJson(res, 200, issueList.slice(0, 2));
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/unidesk/issues?state=all&per_page=3") {
sendJson(res, 200, issueList);
return;
}
if (req.method === "PATCH" && req.url === "/repos/pikasTech/unidesk/issues/20") {
const parsed = JSON.parse(body) as JsonRecord;
sendJson(res, 200, { ...issue, body: String(parsed.body ?? issue.body), updated_at: "2026-05-20T01:05:00Z" });
@@ -134,6 +188,45 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
UNIDESK_GITHUB_API_URL: mock.baseUrl,
};
try {
const listOpen = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "2", "--json", "number,title,state,url"], env);
assertCondition(listOpen.status === 0, "issue list should support state/limit/json", listOpen.json ?? { stdout: listOpen.stdout });
const listOpenData = dataOf(listOpen.json ?? {});
assertCondition(listOpenData.state === "open", "issue list should preserve state", listOpenData);
assertCondition(listOpenData.limit === 2, "issue list should preserve limit", listOpenData);
assertCondition(listOpenData.count === 2, "issue list should return bounded issues", listOpenData);
const listOpenIssues = listOpenData.issues as JsonRecord[];
assertCondition(Array.isArray(listOpenIssues), "issue list should expose issues array", listOpenData);
assertCondition(listOpenIssues[0]?.number === 35, "issue list should expose number field", listOpenData);
assertCondition(listOpenIssues[0]?.title === "master:补齐 UniDesk CLI gh issue list 与 PR 驱动最小闭环前置能力", "issue list should expose title field", listOpenData);
assertCondition(!("labels" in listOpenIssues[0]), "issue list --json should select only requested fields", listOpenIssues[0]);
const acceptanceList = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "5", "--json", "number,title,state,url"], env);
assertCondition(acceptanceList.status === 0, "acceptance issue list command should succeed under mock GitHub", acceptanceList.json ?? { stdout: acceptanceList.stdout });
const acceptanceListData = dataOf(acceptanceList.json ?? {});
assertCondition(acceptanceListData.limit === 5, "acceptance issue list command should preserve limit=5", acceptanceListData);
assertCondition(acceptanceListData.count === 2, "acceptance issue list command should filter PRs and keep issues", acceptanceListData);
const listDefaultFields = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "all", "--limit", "3"], env);
assertCondition(listDefaultFields.status === 0, "issue list should support default fields", listDefaultFields.json ?? { stdout: listDefaultFields.stdout });
const listDefaultData = dataOf(listDefaultFields.json ?? {});
assertCondition(listDefaultData.count === 2, "issue list should filter pull requests from GitHub issues endpoint", listDefaultData);
const defaultIssues = listDefaultData.issues as JsonRecord[];
const firstLabels = defaultIssues[0]?.labels as JsonRecord[];
assertCondition(Array.isArray(firstLabels) && firstLabels[0]?.name === "cli", "issue list default fields should include labels", listDefaultData);
assertCondition(defaultIssues.every((item) => typeof item.number === "number" && typeof item.url === "string"), "issue list default fields should expose stable JSON", listDefaultData);
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 ?? {});
assertCondition(badListFieldData.degradedReason === "validation-failed", "issue list unsupported --json should be validation-failed", badListFieldData);
assertCondition(badListFieldData.runnerDisposition === "business-failed", "issue list unsupported --json should be business-failed", badListFieldData);
const badState = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "triaged"], env);
assertCondition(badState.status !== 0, "issue list unsupported state should fail", badState.json ?? { stdout: badState.stdout });
const badStateData = failedDataOf(badState.json ?? {});
assertCondition(badStateData.degradedReason === "validation-failed", "issue list unsupported state should be validation-failed", badStateData);
assertCondition(badStateData.runnerDisposition === "business-failed", "issue list unsupported state should be business-failed", badStateData);
const viewBody = await runCli(["gh", "issue", "view", "20", "--repo", "pikasTech/unidesk", "--json", "body"], env);
assertCondition(viewBody.status === 0, "issue view --json body should succeed", viewBody.json ?? { stdout: viewBody.stdout });
const viewBodyData = dataOf(viewBody.json ?? {});
@@ -213,6 +306,10 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
ok: true,
checks: [
"issue view --json body preserves .data.issue.body",
"issue list supports state/limit/json with stable selected fields",
"acceptance issue list command succeeds under mock GitHub",
"issue list default fields include labels and filter pull requests",
"issue list unsupported fields and states fail structurally",
"issue view supports body,title,state,comments selection",
"unsupported --json fields fail structurally",
"issue edit --body-file rejects literal null",