Something went wrong. Try again.
Cursor plugin specification and official plugins
Something went wrong. Try again.
9.1 kB · 306 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307import { describe, expect, it } from "bun:test";import { ChecksUnavailable, WatcherQueryError, mapRollupNode, orderStack, parsePullRequest, parseReviewThreads, resolveChecks, resolveContext,} from "./github.ts";import { fakeReader, failedCheck, passingCheck, pendingCheck,} from "./fakes.test-helper.ts";import { parsePrNumber } from "./types.ts";
const context = { owner: "owner", repo: "repo", number: parsePrNumber(42),};
describe("checks fallback chain", () => { it("uses a non-empty fast-path result without a rollup query", async () => { const reader = fakeReader({ fastPath: { kind: "checks", checks: [passingCheck("fast")] }, }); const read = await resolveChecks(reader, context); expect(read.source).toBe("gh-pr-checks"); expect(read.checks.map((check) => check.name)).toEqual(["fast"]); expect(reader.calls).toEqual(["checksFastPath"]); });
it("paginates GraphQL when the fast path is unusable", async () => { const reader = fakeReader({ fastPath: { kind: "unusable", exitCode: 8, stderr: "" }, rollupPages: [ { checks: [passingCheck("first")], endCursor: "next" }, { checks: [failedCheck("second")], endCursor: null }, ], }); const read = await resolveChecks(reader, context); expect(read.source).toBe("graphql-rollup"); expect(read.checks.map((check) => check.name)).toEqual(["first", "second"]); expect(reader.calls).toEqual([ "checksFastPath", "checkRollupPage:null", "checkRollupPage:next", ]); });
it("falls back when valid fast-path JSON represented an empty list", async () => { const reader = fakeReader({ fastPath: { kind: "checks", checks: [] }, rollupPages: [{ checks: [pendingCheck("fallback")], endCursor: null }], }); expect((await resolveChecks(reader, context)).checks[0].name).toBe( "fallback" ); expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]); });
it("fails closed when both paths are empty", async () => { const reader = fakeReader({ fastPath: { kind: "unusable", exitCode: 8, stderr: "credential cannot read checks", }, }); await expect(resolveChecks(reader, context)).rejects.toBeInstanceOf( ChecksUnavailable ); expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]); });});
describe("rollup node mapping", () => { it("maps terminal and non-terminal CheckRun states fail closed", () => { const cases = [ ["IN_PROGRESS", null, "pending", "PENDING"], ["COMPLETED", "SUCCESS", "passed", "SUCCESS"], ["COMPLETED", "NEUTRAL", "skipped", "NEUTRAL"], ["COMPLETED", "SKIPPED", "skipped", "SKIPPED"], ["COMPLETED", "ACTION_REQUIRED", "failed", "ACTION_REQUIRED"], ["COMPLETED", "TIMED_OUT", "failed", "FAILURE"], ["COMPLETED", "FUTURE_VALUE", "failed", "FAILURE"], ] as const; for (const [status, conclusion, kind, reportedState] of cases) { expect( mapRollupNode({ __typename: "CheckRun", name: "ci", status, conclusion, }) ).toMatchObject({ kind, reportedState }); } });
it("classifies an in-progress Code Review Gate from the rollup as the gate", () => { expect( mapRollupNode({ __typename: "CheckRun", name: "Code Review Gate", status: "IN_PROGRESS", conclusion: null, }) ).toMatchObject({ kind: "code-review-gate" }); expect( mapRollupNode({ __typename: "StatusContext", context: "Code Review Gate", state: "PENDING", }) ).toMatchObject({ kind: "code-review-gate" }); });
it("maps StatusContext states and drops unknown typenames", () => { expect( mapRollupNode({ __typename: "StatusContext", context: "ci", state: "EXPECTED", }) ).toMatchObject({ kind: "pending", reportedState: "PENDING" }); expect( mapRollupNode({ __typename: "StatusContext", context: "ci", state: "FUTURE_VALUE", }) ).toMatchObject({ kind: "failed", reportedState: "FUTURE_VALUE" }); expect(mapRollupNode({ __typename: "FutureNode" })).toBeNull(); });});
describe("closed enum parsing", () => { const rawPullRequest = { mergeable: "MERGEABLE", mergeStateStatus: "CLEAN", reviewDecision: "APPROVED", headRefOid: "head", headRefName: "feature", baseRefName: "main", state: "OPEN", mergedAt: null, isDraft: false, };
it("accepts mergeStateStatus CONFLICTING", () => { expect( parsePullRequest( { ...rawPullRequest, mergeStateStatus: "CONFLICTING" }, context ).mergeStateStatus ).toBe("CONFLICTING"); });
it("reads gh's empty reviewDecision as no decision rather than a parse failure", () => { expect( parsePullRequest({ ...rawPullRequest, reviewDecision: "" }, context) .reviewDecision ).toBeNull(); });
it("still rejects an unknown reviewDecision", () => { expect(() => parsePullRequest({ ...rawPullRequest, reviewDecision: "MAYBE" }, context) ).toThrow(WatcherQueryError); });
it("rejects unknown enum values as retryable errors carrying the raw value", () => { try { parsePullRequest( { ...rawPullRequest, mergeStateStatus: "FUTURE_STATE" }, context ); throw new Error("expected parser to throw"); } catch (error) { expect(error).toBeInstanceOf(WatcherQueryError); if (!(error instanceof WatcherQueryError)) throw error; expect(error.failure).toMatchObject({ kind: "missing-key", retryable: true, rawValue: '"FUTURE_STATE"', }); } });});
it("annotates Bugbot threads with distinct review-pass counts", () => { const response = { data: { repository: { pullRequest: { reviewThreads: { nodes: [ { id: "one", isResolved: false, comments: { nodes: [ { body: "RUN_ID: run-1", createdAt: "now", path: "a.ts", line: 1, author: { login: "bugbot" }, }, ], }, }, { id: "two", isResolved: false, comments: { nodes: [ { body: "CURSOR_AUTOMATION_ID: run-2 severity high", createdAt: "now", path: null, line: null, author: { login: "cursor" }, }, ], }, }, { id: "resolved", isResolved: true, comments: { nodes: [ { body: "RUN_ID: run-3", createdAt: "now", path: null, line: null, author: { login: "bugbot" }, }, ], }, }, ], }, }, }, }, }; const threads = parseReviewThreads(response); expect(threads).toHaveLength(2); expect(threads.map((thread) => thread.isBugbot)).toEqual([true, true]); expect(threads.map((thread) => thread.bugbotReviewPasses)).toEqual([3, 3]);});
describe("context and stack discovery", () => { it("returns a fully explicit context without any reader call", async () => { const reader = fakeReader(); expect( await resolveContext({ reader, owner: "explicit", repo: "repo", pr: context.number, }) ).toEqual({ owner: "explicit", repo: "repo", number: context.number }); expect(reader.calls).toEqual([]); });
it("uses the local origin before currentPr for an explicit number", async () => { const reader = fakeReader({ origin: { owner: "local", repo: "checkout" } }); expect( await resolveContext({ reader, owner: null, repo: null, pr: context.number, }) ).toEqual({ owner: "local", repo: "checkout", number: context.number }); expect(reader.calls).toEqual(["originRepo"]); });
it("orders the connected stack bottom-to-top", () => { const ordered = orderStack(context, [ { number: parsePrNumber(41), headRefName: "base-feature", baseRefName: "main", }, { number: context.number, headRefName: "feature", baseRefName: "base-feature", }, { number: parsePrNumber(43), headRefName: "upstack", baseRefName: "feature", }, ]); expect(ordered.map((item) => Number(item.number))).toEqual([41, 42, 43]); });});