import { execFile } from "node:child_process"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it, vi } from "vitest"; import { parse } from "yaml"; import { z } from "zod"; import { evaluateReadiness, runReadinessCheck, type ReadinessGitHub, type ReadinessPullRequest, } from "../../../scripts/ci/pr-readiness.mjs"; const template = await readFile( new URL("../../../.github/pull_request_template.md", import.meta.url), "utf8", ); const readyBody = template .slice( template.indexOf("## Author readiness"), template.indexOf("## Before merge"), ) .replace("Human owner: ", "Human owner: @contributor") .replaceAll("- [ ]", "- [x]"); const makePull = ( changes: Partial = {}, ): ReadinessPullRequest => ({ number: 42, state: "open", draft: false, body: readyBody, head: { sha: "a".repeat(40) }, base: { ref: "main" }, ...changes, }); const headPage = ( nodes: readonly { number: number; headRefOid: string }[], next: string | null = null, ) => ({ repository: { pullRequests: { nodes, pageInfo: { hasNextPage: next !== null, endCursor: next }, }, }, }); function fixture(changes: Partial = {}) { const pull = makePull(changes); const get = vi.fn(async () => ({ data: pull })); const createCommitStatus = vi.fn( async ( _status: Parameters< ReadinessGitHub["rest"]["repos"]["createCommitStatus"] >[0], ) => ({}), ); const paginate = vi.fn(async () => [pull]); const graphql = vi.fn( async ( ..._arguments: Parameters ): Promise => headPage([]), ); const github = { graphql, rest: { pulls: { get }, repos: { createCommitStatus, listPullRequestsAssociatedWithCommit: vi.fn(), }, }, paginate, }; const context = { repo: { owner: "morluto", repo: "rea" }, payload: { pull_request: { number: 42 } }, serverUrl: "https://github.com", runId: 123, }; const core = { info: vi.fn(), setFailed: vi.fn() }; return { github, context, core, get, createCommitStatus, paginate, graphql, pull, }; } describe("initial author declaration", () => { it("accepts the filled current template and uppercase checkmarks", () => { expect(evaluateReadiness(makePull(), template).state).toBe("success"); expect( evaluateReadiness( makePull({ body: readyBody.replaceAll("[x]", "[X]") }), template, ).state, ).toBe("success"); expect( evaluateReadiness( makePull({ body: readyBody.replaceAll("\n", "\r\n") }), template, ).state, ).toBe("success"); }); it("allows drafts without a body or declarations", () => { expect( evaluateReadiness(makePull({ draft: true, body: null }), template).state, ).toBe("pending"); }); it.each([0, 1, 2])("requires declaration %i to be checked", (index) => { let position = 0; const body = readyBody.replaceAll("- [x]", () => position++ === index ? "- [ ]" : "- [x]", ); expect(evaluateReadiness(makePull({ body }), template).state).toBe( "failure", ); }); it.each([ null, readyBody.replace("Human owner: @contributor", "Human owner:"), readyBody.replace("Human owner: @contributor", "Human owner: @one @two"), readyBody.replace( "Human owner: @contributor", "Human owner: @automation[bot]", ), readyBody + "\nHuman owner: @other\n", readyBody.replace("## Author readiness", "## Other"), readyBody + readyBody, ``, `~~~markdown\n${readyBody}\n~~~`, `\`\`\`markdown\n${readyBody}\n\`\`\``, readyBody .split("\n") .map((line) => `> ${line}`) .join("\n"), readyBody.replace( "- [x] I declare this PR ready for initial maintainer review.", "", ), readyBody + "\n- [x] I declare this PR ready for initial maintainer review.\n", ])( "rejects missing, hidden, ambiguous, or incomplete declarations (%#)", (body) => { expect(evaluateReadiness(makePull({ body }), template).state).toBe( "failure", ); }, ); it("ignores checked examples in comments and fenced blocks before the actual declaration", () => { for (const example of [ `\n`, `~~~markdown\n${readyBody}\n~~~\n`, `\`\`\`markdown\n${readyBody}\n\`\`\`\n`, ]) expect( evaluateReadiness(makePull({ body: example + readyBody }), template) .state, ).toBe("success"); }); it("does not treat an HTML comment inside a code example as hiding the real declaration", () => { const body = "~~~html\n