fix: expose PR closeout metadata
This commit is contained in:
@@ -110,6 +110,22 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
merge_commit_sha: "merge-commit-sha",
|
||||
updated_at: "2026-05-21T08:00:00Z",
|
||||
};
|
||||
const unknownMetadataPullRequest = {
|
||||
...pullRequest,
|
||||
id: 4400,
|
||||
number: 44,
|
||||
title: "unknown metadata PR",
|
||||
html_url: "https://github.com/pikasTech/unidesk/pull/44",
|
||||
head: { ref: "feature/pr-unknown", sha: "unknown-head-sha" },
|
||||
};
|
||||
const graphqlErrorPullRequest = {
|
||||
...pullRequest,
|
||||
id: 4500,
|
||||
number: 45,
|
||||
title: "graphql error PR",
|
||||
html_url: "https://github.com/pikasTech/unidesk/pull/45",
|
||||
head: { ref: "feature/pr-graphql-error", sha: "graphql-error-head-sha" },
|
||||
};
|
||||
const server = createServer(async (req, res) => {
|
||||
const body = await collectBody(req);
|
||||
requests.push({ method: req.method ?? "", url: req.url ?? "", body });
|
||||
@@ -149,7 +165,41 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
sendJson(res, 200, mergedPullRequest);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/pulls/44") {
|
||||
sendJson(res, 200, unknownMetadataPullRequest);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && req.url === "/repos/pikasTech/unidesk/pulls/45") {
|
||||
sendJson(res, 200, graphqlErrorPullRequest);
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && req.url === "/graphql") {
|
||||
const parsed = JSON.parse(body) as { variables?: { number?: unknown } };
|
||||
const number = Number(parsed.variables?.number ?? 0);
|
||||
if (number === 44) {
|
||||
sendJson(res, 200, {
|
||||
data: {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
mergeable: "UNKNOWN",
|
||||
mergeStateStatus: null,
|
||||
headRefName: "feature/pr-unknown",
|
||||
baseRefName: "master",
|
||||
statusCheckRollup: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (number === 45) {
|
||||
sendJson(res, 200, {
|
||||
errors: [
|
||||
{ type: "FORBIDDEN", message: "Resource not accessible by integration" },
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, {
|
||||
data: {
|
||||
repository: {
|
||||
@@ -253,6 +303,8 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
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 });
|
||||
assertCondition(notes.some((line) => line.includes("low-noise read-only closeout helper")), "gh help should document PR preflight closeout helper", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("closeoutMetadata") && line.includes("UNKNOWN/null")), "gh help should document explicit closeout metadata unknowns", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR list does not fetch mergeability")), "gh help should direct closeout metadata to pr view", { notes });
|
||||
|
||||
const mock = await startMockGitHub();
|
||||
const env = {
|
||||
@@ -292,6 +344,13 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(badStateData.degradedReason === "validation-failed", "pr list unsupported state should be validation-failed", badStateData);
|
||||
assertCondition(badStateData.runnerDisposition === "business-failed", "pr list unsupported state should be business-failed", badStateData);
|
||||
|
||||
const badListCloseout = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--json", "number,mergeable,statusCheckRollup"], env);
|
||||
assertCondition(badListCloseout.status !== 0, "pr list closeout metadata fields should fail explicitly", badListCloseout.json ?? { stdout: badListCloseout.stdout });
|
||||
const badListCloseoutData = failedDataOf(badListCloseout.json ?? {});
|
||||
const badListCloseoutMessage = String((badListCloseoutData.details as JsonRecord)?.message ?? badListCloseoutData.message ?? "");
|
||||
assertCondition(badListCloseoutData.degradedReason === "validation-failed", "pr list closeout fields should be validation-failed", badListCloseoutData);
|
||||
assertCondition(badListCloseoutMessage.includes("use gh pr view <number>") && badListCloseoutMessage.includes("statusCheckRollup"), "pr list closeout failure should point to pr view", badListCloseoutData);
|
||||
|
||||
const read = await runCli(["gh", "pr", "read", "42", "--repo", "pikasTech/unidesk", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(read.status === 0, "pr read should succeed through REST", read.json ?? { stdout: read.stdout });
|
||||
const readData = dataOf(read.json ?? {});
|
||||
@@ -344,8 +403,32 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(closeoutJson.headRefName === "feature/pr-contract" && closeoutJson.baseRefName === "master", "pr view should expose PR branch names", closeoutData);
|
||||
const rollup = closeoutJson.statusCheckRollup as JsonRecord;
|
||||
assertCondition(rollup.state === "SUCCESS", "pr view should expose statusCheckRollup", closeoutData);
|
||||
const closeoutMetadata = closeoutData.closeoutMetadata as JsonRecord;
|
||||
const closeoutMergeBoundary = closeoutMetadata.mergeBoundary as JsonRecord;
|
||||
assertCondition(closeoutMetadata.ok === true && closeoutMetadata.source === "github-graphql", "pr view closeout metadata should report GraphQL source", closeoutMetadata);
|
||||
assertCondition(Array.isArray(closeoutMetadata.missingOrUnknownFields) && closeoutMetadata.missingOrUnknownFields.length === 0, "known closeout metadata should have no missing/unknown fields", closeoutMetadata);
|
||||
assertCondition(closeoutMergeBoundary.unideskCliMergeSupported === false, "closeout metadata should keep UniDesk CLI merge unsupported", closeoutMetadata);
|
||||
assertCondition(mock.requests.some((request) => request.method === "POST" && request.url === "/graphql"), "closeout metadata should use GitHub GraphQL when requested", mock.requests);
|
||||
|
||||
const unknownCloseout = await runCli(["gh", "pr", "view", "44", "--repo", "pikasTech/unidesk", "--json", "headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"], env);
|
||||
assertCondition(unknownCloseout.status === 0, "pr view unknown closeout metadata should remain a structured read success", unknownCloseout.json ?? { stdout: unknownCloseout.stdout });
|
||||
const unknownCloseoutData = dataOf(unknownCloseout.json ?? {});
|
||||
const unknownCloseoutJson = unknownCloseoutData.json as JsonRecord;
|
||||
const unknownCloseoutMetadata = unknownCloseoutData.closeoutMetadata as JsonRecord;
|
||||
const unknownFields = unknownCloseoutMetadata.missingOrUnknownFields as unknown[];
|
||||
assertCondition(unknownCloseoutJson.mergeable === "UNKNOWN" && unknownCloseoutJson.statusCheckRollup === null, "unknown closeout JSON should preserve GitHub values", unknownCloseoutData);
|
||||
assertCondition(unknownCloseoutMetadata.ok === false, "unknown closeout metadata should be explicit", unknownCloseoutMetadata);
|
||||
assertCondition(Array.isArray(unknownFields) && unknownFields.includes("mergeable") && unknownFields.includes("mergeStateStatus") && unknownFields.includes("statusCheckRollup"), "unknown closeout metadata should name missing/unknown fields", unknownCloseoutMetadata);
|
||||
assertCondition(String(unknownCloseoutMetadata.advice ?? "").includes("missing or unknown"), "unknown closeout metadata should include operator advice", unknownCloseoutMetadata);
|
||||
|
||||
const graphqlErrorCloseout = await runCli(["gh", "pr", "view", "45", "--repo", "pikasTech/unidesk", "--json", "headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"], env);
|
||||
assertCondition(graphqlErrorCloseout.status !== 0, "pr view GraphQL closeout failure should fail structurally", graphqlErrorCloseout.json ?? { stdout: graphqlErrorCloseout.stdout });
|
||||
const graphqlErrorData = failedDataOf(graphqlErrorCloseout.json ?? {});
|
||||
const graphqlErrorMetadata = graphqlErrorData.closeoutMetadata as JsonRecord;
|
||||
assertCondition(graphqlErrorData.phase === "fetch-pr-closeout-metadata", "GraphQL closeout failure should report phase", graphqlErrorData);
|
||||
assertCondition(graphqlErrorMetadata.ok === false && graphqlErrorMetadata.source === "github-graphql", "GraphQL closeout failure should include explicit metadata error", graphqlErrorData);
|
||||
assertCondition(String(graphqlErrorMetadata.message ?? "").includes("Resource not accessible"), "GraphQL closeout failure should preserve sanitized error message", graphqlErrorMetadata);
|
||||
|
||||
const requestsBeforeMergedRead = mock.requests.length;
|
||||
const mergedRead = await runCli(["gh", "pr", "read", "43", "--repo", "pikasTech/unidesk", "--json", "state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit"], env);
|
||||
assertCondition(mergedRead.status === 0, "merged pr closeout fields should succeed through REST", mergedRead.json ?? { stdout: mergedRead.stdout });
|
||||
@@ -564,6 +647,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const closeoutBoundary = mergeData?.closeoutBoundary as JsonRecord | undefined;
|
||||
assertCondition(closeoutBoundary?.ordinaryRunnerFinalActionAllowed === true, "merge block should preserve ordinary runner PR closeout policy", closeoutBoundary ?? {});
|
||||
assertCondition(closeoutBoundary?.unideskCliMergeSupported === false, "merge block should state UniDesk REST CLI merge remains unsupported", closeoutBoundary ?? {});
|
||||
assertCondition(String(closeoutBoundary?.readOnlyCloseoutCommand ?? "").includes("gh pr view 42"), "merge block should point to read-only closeout command", closeoutBoundary ?? {});
|
||||
|
||||
const deleteBlocked = await runCli(["gh", "pr", "delete", "42", "--repo", "pikasTech/unidesk"]);
|
||||
assertCondition(deleteBlocked.status !== 0, "pr hard delete should fail", deleteBlocked.json ?? { stdout: deleteBlocked.stdout });
|
||||
@@ -592,9 +676,11 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"pr list/read/view work through REST with token and no gh binary dependency",
|
||||
"pr read/view accept owner/repo#number shorthand and reject conflicting --repo",
|
||||
"pr read/view --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",
|
||||
"pr view closeout metadata fields are accepted and hydrated through GraphQL",
|
||||
"pr view closeout metadata makes GraphQL errors and UNKNOWN/null explicit",
|
||||
"pr read unsupported fields fail structurally with supported closeout fields listed",
|
||||
"pr preflight exposes redacted auth plus compact merge/status closeout metadata",
|
||||
"top-level gh preflight alias works for commander closeout",
|
||||
|
||||
Reference in New Issue
Block a user