fix: relax gh number target compatibility
This commit is contained in:
@@ -288,6 +288,10 @@ function failedDataOf(response: JsonRecord): JsonRecord {
|
||||
return response.data as JsonRecord;
|
||||
}
|
||||
|
||||
function failureMessageOf(data: JsonRecord): string {
|
||||
return String((data.details as JsonRecord | undefined)?.message ?? data.message ?? "");
|
||||
}
|
||||
|
||||
export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const help = await runCli(["gh", "help"]);
|
||||
assertCondition(help.status === 0, "gh help should succeed", help.json ?? { stdout: help.stdout });
|
||||
@@ -295,9 +299,8 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const usage = Array.isArray(helpData.usage) ? helpData.usage.map((value) => String(value)) : [];
|
||||
const notes = Array.isArray(helpData.notes) ? helpData.notes.map((value) => String(value)) : [];
|
||||
assertCondition(usage.some((line) => line.includes("gh pr list")), "gh help should list pr list", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read")), "gh help should list pr read", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr view")), "gh help should list pr view", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document pr shorthand and raw/full disclosure", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr view") && line.includes("number|url|owner/repo#number") && line.includes("--raw|--full")), "gh help should document standard pr view targets and raw/full disclosure", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("compatibility alias for pr view")), "gh help should list pr read compatibility alias", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh preflight")), "gh help should list top-level preflight alias", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr preflight")), "gh help should list pr preflight", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr create")), "gh help should list pr create", { usage });
|
||||
@@ -305,9 +308,10 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(usage.some((line) => line.includes("gh pr comment")), "gh help should list pr comment", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr list") && line.includes("--state open|closed|all")), "gh help should document pr list state filtering", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("mergedAt") && line.includes("mergeCommit")), "gh help should document merged PR closeout fields", { usage });
|
||||
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state pr read is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state pr view is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR read/view accept owner/repo#number shorthand")), "gh help should explain pr read/view shorthand", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR view is the canonical")), "gh help should state pr view is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("read remains") && line.includes("compatibility alias")), "gh help should state pr read is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("GitHub PR URLs") && line.includes("owner/repo#number shorthand")), "gh help should explain pr view/read URL and shorthand targets", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--number is accepted on single PR/comment numeric target commands") && line.includes("PR comment delete treats --number as commentId")), "gh help should document --number compatibility hint", { 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("PR list defaults to --state all")), "gh help should document pr list default state", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("stateDetail") && line.includes("mergedAt")), "gh help should describe closeout field normalization", { notes });
|
||||
@@ -383,15 +387,17 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(selected.body === "PR body" && selected.title === "contract PR", "pr read --json should select fields", readData);
|
||||
|
||||
const readNumberAlias = await runCli(["gh", "pr", "read", "--repo", "pikasTech/HWLAB", "--number", "7", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(readNumberAlias.status === 0, "pr read should accept --number alias", readNumberAlias.json ?? { stdout: readNumberAlias.stdout });
|
||||
assertCondition(readNumberAlias.status === 0, "pr read should accept --number compatibility alias", readNumberAlias.json ?? { stdout: readNumberAlias.stdout });
|
||||
const readNumberAliasData = dataOf(readNumberAlias.json ?? {});
|
||||
assertCondition(readNumberAliasData.repo === "pikasTech/HWLAB", "pr read --number should preserve explicit repo", readNumberAliasData);
|
||||
const readNumberAliasPr = readNumberAliasData.pullRequest as JsonRecord;
|
||||
assertCondition(readNumberAliasPr.number === 7 && readNumberAliasPr.url === "https://github.com/pikasTech/HWLAB/pull/7", "pr read --number should read the requested PR", readNumberAliasData);
|
||||
const readNumberAliasDisclosure = readNumberAliasData.disclosure as JsonRecord;
|
||||
assertCondition(String(readNumberAliasDisclosure.compatibilityHint ?? "").includes("standard gh syntax") && String(readNumberAliasDisclosure.standardCommand ?? "").includes("gh pr view 7 --repo pikasTech/HWLAB"), "pr read --number should return standard syntax hint", readNumberAliasDisclosure);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/HWLAB/pulls/7"), "pr read --number should call explicit repo REST path", mock.requests);
|
||||
|
||||
const numberAliasUnsupported = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--number", "7"], env);
|
||||
assertCondition(numberAliasUnsupported.status !== 0, "--number should not be silently ignored outside pr read/view", numberAliasUnsupported.json ?? { stdout: numberAliasUnsupported.stdout });
|
||||
assertCondition(numberAliasUnsupported.status !== 0, "--number should not be silently ignored outside standard view/read", numberAliasUnsupported.json ?? { stdout: numberAliasUnsupported.stdout });
|
||||
const numberAliasUnsupportedData = failedDataOf(numberAliasUnsupported.json ?? {});
|
||||
assertCondition(numberAliasUnsupportedData.degradedReason === "validation-failed", "unsupported --number should be validation-failed", numberAliasUnsupportedData);
|
||||
|
||||
@@ -404,11 +410,24 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(openLifecycleJson.merged === false && openLifecycleJson.mergedAt === null && openLifecycleJson.mergeCommit === null, "open pr should expose merged=false", openLifecycleData);
|
||||
|
||||
const view = await runCli(["gh", "pr", "view", "42", "--repo", "pikasTech/unidesk", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(view.status === 0, "pr view alias should succeed through REST", view.json ?? { stdout: view.stdout });
|
||||
assertCondition(view.status === 0, "pr view should succeed as canonical read path", view.json ?? { stdout: view.stdout });
|
||||
const viewData = dataOf(view.json ?? {});
|
||||
assertCondition((viewData.pullRequest as JsonRecord).number === 42, "pr view alias should expose PR details", viewData);
|
||||
assertCondition((viewData.pullRequest as JsonRecord).number === 42, "pr view should expose PR details", viewData);
|
||||
const viewSelected = viewData.json as JsonRecord;
|
||||
assertCondition(viewSelected.body === "PR body" && viewSelected.title === "contract PR", "pr view alias should preserve selected fields", viewData);
|
||||
assertCondition(viewSelected.body === "PR body" && viewSelected.title === "contract PR", "pr view should preserve selected fields", viewData);
|
||||
|
||||
const prUrlView = await runCli(["gh", "pr", "view", "https://github.com/pikasTech/HWLAB/pull/7", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(prUrlView.status === 0, "pr view should accept GitHub PR URL target", prUrlView.json ?? { stdout: prUrlView.stdout });
|
||||
const prUrlViewData = dataOf(prUrlView.json ?? {});
|
||||
assertCondition(prUrlViewData.repo === "pikasTech/HWLAB", "PR URL target should derive repo", prUrlViewData);
|
||||
assertCondition((prUrlViewData.pullRequest as JsonRecord).number === 7, "PR URL target should derive PR number", prUrlViewData);
|
||||
const prUrlDisclosure = prUrlViewData.disclosure as JsonRecord;
|
||||
assertCondition(prUrlDisclosure.shorthand && (prUrlDisclosure.shorthand as JsonRecord).source === "github-url", "PR URL target should be disclosed", prUrlDisclosure);
|
||||
|
||||
const prIssueUrlMismatch = await runCli(["gh", "pr", "view", "https://github.com/pikasTech/HWLAB/issues/7", "--json", "body"], env);
|
||||
assertCondition(prIssueUrlMismatch.status !== 0, "pr view should reject issue URLs", prIssueUrlMismatch.json ?? { stdout: prIssueUrlMismatch.stdout });
|
||||
const prIssueUrlMismatchData = failedDataOf(prIssueUrlMismatch.json ?? {});
|
||||
assertCondition(failureMessageOf(prIssueUrlMismatchData).includes("GitHub issue URL"), "pr view issue URL mismatch should be explicit", prIssueUrlMismatchData);
|
||||
|
||||
const shorthandRaw = await runCli(["gh", "pr", "view", "pikasTech/HWLAB#7", "--raw"], env);
|
||||
assertCondition(shorthandRaw.status === 0, "pr view should accept owner/repo#number shorthand with --raw", shorthandRaw.json ?? { stdout: shorthandRaw.stdout });
|
||||
@@ -428,7 +447,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(shorthandConflictData.degradedReason === "validation-failed", "pr conflicting --repo should be validation-failed", shorthandConflictData);
|
||||
assertCondition(String(shorthandConflictData.message ?? "").includes("resolves to repo pikasTech/HWLAB"), "pr conflict message should name the derived repo", shorthandConflictData);
|
||||
const prConflictCommands = shorthandConflictData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(prConflictCommands) && prConflictCommands.some((command) => command === "bun scripts/cli.ts gh pr read 7 --repo pikasTech/HWLAB --json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"), "pr conflict should include exact supported read command", shorthandConflictData);
|
||||
assertCondition(Array.isArray(prConflictCommands) && prConflictCommands.some((command) => command === "bun scripts/cli.ts gh pr view 7 --repo pikasTech/HWLAB --json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"), "pr conflict should include exact supported view command", shorthandConflictData);
|
||||
|
||||
const closeout = await runCli(["gh", "pr", "view", "42", "--repo", "pikasTech/unidesk", "--json", "mergeable,mergeStateStatus,statusCheckRollup,headRefName,baseRefName"], env);
|
||||
assertCondition(closeout.status === 0, "pr view closeout metadata fields should not be rejected", closeout.json ?? { stdout: closeout.stdout });
|
||||
@@ -648,6 +667,12 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(appendBody.mode === "append", "pr append mode should be explicit", updateAppendData);
|
||||
assertCondition(finalBody.containsBackticks === true && finalBody.containsMarkdownTable === true, "pr append should preserve markdown signals", updateAppendData);
|
||||
|
||||
const updateNumberDryRun = await runCli(["gh", "pr", "update", "--number", "42", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", bodyFile, "--dry-run"], env2);
|
||||
assertCondition(updateNumberDryRun.status === 0, "pr update should accept --number compatibility alias", updateNumberDryRun.json ?? { stdout: updateNumberDryRun.stdout });
|
||||
const updateNumberData = dataOf(updateNumberDryRun.json ?? {});
|
||||
const updateNumberHint = updateNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(String(updateNumberHint.standardCommand ?? "").includes("gh pr update 42 --repo pikasTech/unidesk"), "pr update --number should return standard syntax hint", updateNumberHint);
|
||||
|
||||
const editStdinBody = "stdin line\n`stdin code`\n| c | d |\n";
|
||||
const beforeEditRequests = mock2.requests.length;
|
||||
const editStdin = await runCli(["gh", "pr", "edit", "42", "--repo", "pikasTech/unidesk", "--title", "stdin title", "--body-file", "-"], env2, editStdinBody);
|
||||
@@ -701,6 +726,12 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(commentDelete.status === 0, "pr comment delete should succeed", commentDelete.json ?? { stdout: commentDelete.stdout });
|
||||
const commentDeleteData = dataOf(commentDelete.json ?? {});
|
||||
assertCondition(commentDeleteData.deleted === true, "pr comment delete should report deleted", commentDeleteData);
|
||||
|
||||
const commentDeleteNumber = await runCli(["gh", "pr", "comment", "delete", "--number", "9101", "--repo", "pikasTech/unidesk", "--dry-run"], env2);
|
||||
assertCondition(commentDeleteNumber.status === 0, "pr comment delete should accept --number commentId compatibility alias", commentDeleteNumber.json ?? { stdout: commentDeleteNumber.stdout });
|
||||
const commentDeleteNumberData = dataOf(commentDeleteNumber.json ?? {});
|
||||
const commentDeleteNumberHint = commentDeleteNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(commentDeleteNumberData.commentId === 9101 && String(commentDeleteNumberHint.standardCommand ?? "").includes("gh pr comment delete 9101 --repo pikasTech/unidesk"), "pr comment delete --number should point to positional commentId syntax", commentDeleteNumberData);
|
||||
} finally {
|
||||
await mock2.close();
|
||||
}
|
||||
@@ -731,9 +762,9 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"gh help lists pr create/comment",
|
||||
"pr list/read/view work through REST with token and no gh binary dependency",
|
||||
"pr list positional owner/repo targets the requested repo and conflicting --repo fails",
|
||||
"pr read supports --number alias without silently ignoring it elsewhere",
|
||||
"pr read/view accept owner/repo#number shorthand and reject conflicting --repo",
|
||||
"pr read/view --raw is explicit full disclosure",
|
||||
"pr single numeric target commands accept --number compatibility with a standard syntax hint",
|
||||
"pr view/read accept GitHub URL and owner/repo#number targets and reject conflicting --repo",
|
||||
"pr view/read --raw is explicit full disclosure",
|
||||
"pr list rejects closeout fields and points to pr view",
|
||||
"pr read normalizes open and merged lifecycle fields from REST",
|
||||
"GitHub DNS/API transients are retryable and distinct from auth or PR semantic failures",
|
||||
|
||||
Reference in New Issue
Block a user