Files
pikasTech-unidesk/scripts/src/gh-pr-ready.test.ts
T
2026-07-11 06:30:19 +02:00

158 lines
6.2 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import { withGhDefaultRendered } from "./gh/default-render";
import { ghScopedHelp } from "./gh/help";
import { prReady } from "./gh/pr-ready";
import type { GitHubPullRequest } from "./gh/types";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
function pullRequest(overrides: Partial<GitHubPullRequest> = {}): GitHubPullRequest {
return {
id: 1748,
node_id: "PR_kwDO_ready_1748",
number: 1748,
title: "PaC source artifact",
body: "body",
state: "open",
html_url: "https://github.com/pikasTech/unidesk/pull/1748",
draft: true,
head: { ref: "fix/1746-pac-source-artifact", sha: "a".repeat(40) },
base: { ref: "master", sha: "b".repeat(40) },
...overrides,
};
}
function installFetch(handler: (url: URL, init: RequestInit | undefined) => Response | Promise<Response>): string[] {
const calls: string[] = [];
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = new URL(typeof input === "string" || input instanceof URL ? input.toString() : input.url);
calls.push(`${init?.method ?? "GET"} ${url.pathname}`);
return handler(url, init);
}) as typeof fetch;
return calls;
}
describe("gh pr ready", () => {
test("marks an open draft PR ready through the shared GraphQL client", async () => {
const calls = installFetch(async (url, init) => {
if (url.pathname.endsWith("/pulls/1748")) return Response.json(pullRequest());
expect(url.pathname).toBe("/graphql");
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer test-token");
const payload = JSON.parse(String(init?.body)) as { query: string; variables: { pullRequestId: string } };
expect(payload.query).toContain("markPullRequestReadyForReview");
expect(payload.variables.pullRequestId).toBe("PR_kwDO_ready_1748");
return Response.json({
data: {
markPullRequestReadyForReview: {
pullRequest: {
id: "PR_kwDO_ready_1748",
number: 1748,
title: "PaC source artifact",
state: "OPEN",
isDraft: false,
url: "https://github.com/pikasTech/unidesk/pull/1748",
headRefName: "fix/1746-pac-source-artifact",
baseRefName: "master",
updatedAt: "2026-07-11T04:30:00Z",
},
},
},
});
});
const result = await prReady("pikasTech/unidesk", "test-token", 1748, false);
expect(result.ok).toBe(true);
expect(result.action).toBe("marked-ready");
expect(result.changed).toBe(true);
expect(result.graphQl).toBe(true);
expect((result.pullRequest as { draft?: boolean }).draft).toBe(false);
expect(calls).toEqual(["GET /repos/pikasTech/unidesk/pulls/1748", "POST /graphql"]);
});
test("is idempotent when the open PR is already ready", async () => {
const calls = installFetch(() => Response.json(pullRequest({ draft: false })));
const result = await prReady("pikasTech/unidesk", "test-token", 1748, false);
expect(result.ok).toBe(true);
expect(result.action).toBe("already-ready");
expect(result.changed).toBe(false);
expect(result.graphQl).toBe(false);
expect(calls).toEqual(["GET /repos/pikasTech/unidesk/pulls/1748"]);
const rendered = withGhDefaultRendered(["pr", "ready", "1748"], result) as { renderedText: string };
expect(rendered.renderedText).toContain("gh pr ready (already-ready)");
});
test("dry-run reads the PR and exposes a bounded mutation plan without writing", async () => {
const calls = installFetch(() => Response.json(pullRequest()));
const result = await prReady("pikasTech/unidesk", "test-token", 1748, true);
expect(result.ok).toBe(true);
expect(result.action).toBe("would-mark-ready");
expect(result.planned).toBe(true);
expect(result.changed).toBe(false);
expect(result.request).toEqual({
method: "POST",
path: "/graphql",
mutation: "markPullRequestReadyForReview",
variables: { pullRequestId: "present" },
});
expect(calls).toEqual(["GET /repos/pikasTech/unidesk/pulls/1748"]);
});
test("rejects a closed PR before mutation with a typed validation error", async () => {
const calls = installFetch(() => Response.json(pullRequest({ state: "closed" })));
const result = await prReady("pikasTech/unidesk", "test-token", 1748, false);
expect(result.ok).toBe(false);
expect(result.degradedReason).toBe("validation-failed");
expect(result.runnerDisposition).toBe("business-failed");
expect(result.phase).toBe("validate-pr-state");
expect(calls).toEqual(["GET /repos/pikasTech/unidesk/pulls/1748"]);
});
test("preserves the typed pr-not-found REST failure", async () => {
installFetch(() => Response.json({ message: "Not Found" }, { status: 404 }));
const result = await prReady("pikasTech/unidesk", "test-token", 404, false);
expect(result.ok).toBe(false);
expect(result.degradedReason).toBe("pr-not-found");
expect(result.runnerDisposition).toBe("business-failed");
expect(result.phase).toBe("fetch-pr");
});
test("preserves typed mutation permission failures", async () => {
installFetch((url) => {
if (url.pathname.endsWith("/pulls/1748")) return Response.json(pullRequest());
return Response.json(
{ message: "Resource not accessible by integration" },
{ status: 403, headers: { "x-accepted-oauth-scopes": "repo" } },
);
});
const result = await prReady("pikasTech/unidesk", "test-token", 1748, false);
expect(result.ok).toBe(false);
expect(result.degradedReason).toBe("scope-insufficient");
expect(result.runnerDisposition).toBe("infra-blocked");
expect(result.phase).toBe("mark-ready");
});
test("publishes bounded scoped help for the command", () => {
const help = ghScopedHelp(["pr", "ready", "--help"]) as { usage: string[]; notes: string[] };
expect(help.usage).toEqual([
"bun scripts/cli.ts gh pr ready <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--dry-run]",
]);
expect(help.notes.join("\n")).toContain("markPullRequestReadyForReview");
expect(help.notes.join("\n")).toContain("不改变 GitHub 状态");
});
});