fix(cli): support gh read shorthand raw

This commit is contained in:
Codex
2026-05-23 05:11:05 +00:00
parent 3b554e6154
commit d0c762da06
6 changed files with 251 additions and 24 deletions
@@ -86,6 +86,18 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
created_at: "2026-05-20T00:00:00Z",
updated_at: "2026-05-20T01:00:00Z",
};
const shorthandIssue = {
id: 7000,
number: 7,
title: "generic shorthand fixture",
body: "HWLAB-style shorthand body fixture\n\nThis is generic CLI coverage, not product data.",
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",
};
const boardIssueBodyInitial = [
"# Code Queue",
"",
@@ -431,6 +443,10 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 200, { ...issue, body: boardIssueBody, updated_at: boardIssueUpdatedAt });
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/HWLAB/issues/7") {
sendJson(res, 200, shorthandIssue);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/24") {
sendJson(res, 200, legacyCommanderBriefIssue);
return;
@@ -455,6 +471,10 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
sendJson(res, 200, comments);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/HWLAB/issues/7/comments?per_page=100") {
sendJson(res, 200, [{ id: 7001, body: "shorthand comment", html_url: "https://github.com/pikasTech/HWLAB/issues/7#issuecomment-7001", user: { login: "tester" }, created_at: "2026-05-20T03:10:00Z", updated_at: "2026-05-20T03:10:00Z" }]);
return;
}
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/issues/60/comments?per_page=100") {
sendJson(res, 200, []);
return;
@@ -570,6 +590,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(usage.some((line) => line.includes("gh issue list")), "gh help should list issue list", { usage });
assertCondition(usage.some((line) => line.includes("gh issue read")), "gh help should list issue read", { usage });
assertCondition(usage.some((line) => line.includes("gh issue view")), "gh help should list issue view", { usage });
assertCondition(usage.some((line) => line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document issue shorthand and raw/full disclosure", { usage });
assertCondition(usage.some((line) => line.includes("gh issue board-row list")), "gh help should list board-row list", { usage });
assertCondition(usage.some((line) => line.includes("gh issue board-row update")), "gh help should list board-row update", { usage });
assertCondition(usage.some((line) => line.includes("gh issue board-row add")), "gh help should list board-row add", { usage });
@@ -578,6 +599,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
assertCondition(usage.some((line) => line.includes("gh issue board-row delete")), "gh help should list board-row delete", { usage });
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state issue read is canonical", { notes });
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state issue view is alias", { notes });
assertCondition(notes.some((line) => line.includes("owner/repo#number shorthand")), "gh help should explain read/view shorthand", { notes });
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
assertCondition(notes.some((line) => line.includes("board-row update changes one table cell")), "gh help should describe board-row update safety", { notes });
assertCondition(notes.some((line) => line.includes("board-row upsert updates an existing row")), "gh help should describe board-row upsert safety", { notes });
assertCondition(notes.some((line) => line.includes("board-row add/move/delete are row-scoped")), "gh help should describe board-row row mutation safety", { notes });
@@ -1113,6 +1136,33 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
const viewSelectedJson = viewBodyData.json as JsonRecord;
assertCondition(typeof viewSelectedJson.body === "string" && viewSelectedJson.body === readIssue.body, "issue view alias should preserve selected json body", viewBodyData);
const shorthandRaw = await runCli(["gh", "issue", "view", "pikasTech/HWLAB#7", "--raw"], env);
assertCondition(shorthandRaw.status === 0, "issue view should accept owner/repo#number shorthand with --raw", shorthandRaw.json ?? { stdout: shorthandRaw.stdout });
const shorthandRawData = dataOf(shorthandRaw.json ?? {});
assertCondition(shorthandRawData.repo === "pikasTech/HWLAB", "issue shorthand should derive repo from owner/repo#number", shorthandRawData);
const shorthandIssueData = shorthandRawData.issue as JsonRecord;
assertCondition(shorthandIssueData.number === 7 && String(shorthandIssueData.body ?? "").includes("shorthand body fixture"), "issue shorthand should read the requested issue", shorthandRawData);
const shorthandDisclosure = shorthandRawData.disclosure as JsonRecord;
assertCondition(shorthandDisclosure.raw === true && shorthandDisclosure.fullDisclosure === true, "--raw should mark explicit full disclosure", shorthandDisclosure);
const shorthandSelected = shorthandRawData.json as JsonRecord;
assertCondition(shorthandSelected.body === shorthandIssueData.body && Array.isArray(shorthandSelected.comments), "--raw should select the supported full issue read field set", shorthandRawData);
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/HWLAB/issues/7"), "issue shorthand should call the derived repo REST path", mock.requests);
const shorthandConflict = await runCli(["gh", "issue", "read", "pikasTech/HWLAB#7", "--repo", "pikasTech/unidesk", "--raw"], env);
assertCondition(shorthandConflict.status !== 0, "issue shorthand with conflicting --repo should fail", shorthandConflict.json ?? { stdout: shorthandConflict.stdout });
const shorthandConflictData = failedDataOf(shorthandConflict.json ?? {});
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);
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 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 });
const readFieldsData = dataOf(readFields.json ?? {});
@@ -1324,6 +1374,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
checks: [
"issue read --json body preserves .data.issue.body",
"issue view remains a compatibility alias",
"issue read/view accept owner/repo#number shorthand and reject conflicting --repo",
"issue read/view --raw is explicit full disclosure",
"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",
@@ -1341,6 +1393,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
"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",
"unknown/full disclosure option guidance remains actionable",
"unsupported --json fields fail structurally",
"issue edit --body-file rejects literal null",
"#20/#24 body profile guards reject missing headings or wrong profile",