Add gh issue list REST contract
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user