fix: expose issue lifecycle json fields

This commit is contained in:
Codex
2026-06-04 10:54:49 +00:00
parent 17e0be8580
commit 2c67aa5a36
3 changed files with 45 additions and 11 deletions
+30 -3
View File
@@ -98,6 +98,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
user: { login: "tester" },
created_at: "2026-05-20T00:00:00Z",
updated_at: "2026-05-20T01:00:00Z",
closed_at: null,
};
const shorthandIssue = {
id: 7000,
@@ -110,6 +111,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
user: { login: "tester" },
created_at: "2026-05-20T02:00:00Z",
updated_at: shorthandIssueUpdatedAt,
closed_at: null,
};
const boardIssueBodyInitial = [
"# Code Queue",
@@ -254,6 +256,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [{ name: "cli", color: "1d76db", description: "CLI work" }],
created_at: "2026-05-20T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
closed_at: null,
},
{
id: 2002,
@@ -267,6 +270,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [],
created_at: "2026-05-20T02:05:00Z",
updated_at: "2026-05-20T03:05:00Z",
closed_at: null,
},
{
id: 3001,
@@ -296,6 +300,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [],
created_at: "2026-05-20T02:30:00Z",
updated_at: "2026-05-20T03:30:00Z",
closed_at: null,
},
];
const scanIssues = [
@@ -413,6 +418,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [],
created_at: "2026-05-18T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
closed_at: "2026-05-20T03:15:00Z",
},
{
id: 2040,
@@ -426,6 +432,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [],
created_at: "2026-05-19T02:00:00Z",
updated_at: "2026-05-20T03:00:00Z",
closed_at: "2026-05-20T03:20:00Z",
},
{
id: 2041,
@@ -439,6 +446,7 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
labels: [],
created_at: "2026-05-19T02:05:00Z",
updated_at: "2026-05-20T03:05:00Z",
closed_at: "2026-05-20T03:25:00Z",
},
];
const comments = [
@@ -663,6 +671,11 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const mock = await startMockGitHub();
const tmp = mkdtempSync(join(tmpdir(), "unidesk-gh-issue-guard-"));
const startedAt = Date.now();
const heartbeat = setInterval(() => {
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
process.stderr.write(`[gh-issue-contract] elapsed=${elapsedSeconds}s requests=${mock.requests.length}\n`);
}, 10_000);
const env = {
GH_TOKEN: "contract-token-should-not-print",
UNIDESK_GITHUB_API_URL: mock.baseUrl,
@@ -680,6 +693,18 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
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 listOpenLifecycle = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "open", "--limit", "2", "--json", "number,state,closed,closedAt"], env);
assertCondition(listOpenLifecycle.status === 0, "issue list lifecycle fields should succeed", listOpenLifecycle.json ?? { stdout: listOpenLifecycle.stdout });
const listOpenLifecycleData = dataOf(listOpenLifecycle.json ?? {});
const listOpenLifecycleIssues = listOpenLifecycleData.issues as JsonRecord[];
assertCondition(listOpenLifecycleIssues[0]?.closed === false && listOpenLifecycleIssues[0]?.closedAt === null, "open issue list rows should expose closed=false and closedAt=null", listOpenLifecycleData);
const listClosedLifecycle = await runCli(["gh", "issue", "list", "--repo", "pikasTech/unidesk", "--state", "closed", "--limit", "2", "--json", "number,state,closed,closedAt"], env);
assertCondition(listClosedLifecycle.status === 0, "closed issue list lifecycle fields should succeed", listClosedLifecycle.json ?? { stdout: listClosedLifecycle.stdout });
const listClosedLifecycleData = dataOf(listClosedLifecycle.json ?? {});
const listClosedLifecycleIssues = listClosedLifecycleData.issues as JsonRecord[];
assertCondition(listClosedLifecycleIssues[0]?.state === "closed" && listClosedLifecycleIssues[0]?.closed === true && listClosedLifecycleIssues[0]?.closedAt === "2026-05-20T03:15:00Z", "closed issue list rows should expose closed=true and closedAt", listClosedLifecycleData);
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 ?? {});
@@ -1256,18 +1281,19 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(shorthandConflictData.degradedReason === "validation-failed", "conflicting --repo should be validation-failed", shorthandConflictData);
assertCondition(String(shorthandConflictData.message ?? "").includes("resolves to repo pikasTech/HWLAB"), "conflict message should name the derived repo", shorthandConflictData);
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);
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue read 7 --repo pikasTech/HWLAB --json body,title,state,closed,closedAt,comments,number,url,author,createdAt,updatedAt"), "conflict should include the exact supported issue read command", shorthandConflictData);
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);
const readFields = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,title,state,closed,closedAt,comments"], env);
assertCondition(readFields.status === 0, "common --json field selection should succeed", readFields.json ?? { stdout: readFields.stdout });
const readFieldsData = dataOf(readFields.json ?? {});
const fieldsJson = readFieldsData.json as JsonRecord;
assertCondition(fieldsJson.title === "长期总看板", "selected json title should be exposed", fieldsJson);
assertCondition(fieldsJson.closed === false && fieldsJson.closedAt === null, "open issue read should expose lifecycle fields", fieldsJson);
assertCondition(Array.isArray(fieldsJson.comments) && fieldsJson.comments.length === 1, "selected json comments should be exposed", fieldsJson);
const unsupported = await runCli(["gh", "issue", "read", "20", "--repo", "pikasTech/unidesk", "--json", "body,unknown"], env);
@@ -1760,7 +1786,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"issue create dry-run parses repeated/comma labels, supports stdin, rejects inline --body, and exposes request plan",
"issue create sends labels through REST and preserves GitHub validation errors for missing labels",
"issue list unsupported fields and states fail structurally",
"issue read supports body,title,state,comments selection",
"issue read supports body,title,state,closed,closedAt,comments selection",
"unknown/full disclosure option guidance remains actionable",
"unsupported --json fields fail structurally",
"issue edit --body-file rejects literal null",
@@ -1783,6 +1809,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
],
};
} finally {
clearInterval(heartbeat);
rmSync(tmp, { recursive: true, force: true });
await mock.close();
}