Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions pstack/skills/poteto-mode/scripts/watch-pr/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,32 @@ describe("checks fallback chain", () => {
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
});

it("accepts an empty check set confirmed by both readers", async () => {
const reader = fakeReader({
fastPath: {
kind: "unusable",
exitCode: 1,
stderr: "no checks reported on the feature branch",
},
rollupPages: [{ checks: [], endCursor: null }],
});
const read = await resolveChecks(reader, context);
expect(read.source).toBe("graphql-rollup");
expect(read.checks).toEqual([]);
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
});

it("accepts valid empty fast-path JSON confirmed by the rollup", async () => {
const reader = fakeReader({
fastPath: { kind: "checks", checks: [] },
rollupPages: [{ checks: [], endCursor: null }],
});
const read = await resolveChecks(reader, context);
expect(read.source).toBe("gh-pr-checks");
expect(read.checks).toEqual([]);
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
});

it("fails closed when both paths are empty", async () => {
const reader = fakeReader({
fastPath: {
Expand All @@ -76,6 +102,20 @@ describe("checks fallback chain", () => {
);
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
});

it("fails closed when exit 1 has an unrelated error", async () => {
const reader = fakeReader({
fastPath: {
kind: "unusable",
exitCode: 1,
stderr: "resource not accessible by integration",
},
rollupPages: [{ checks: [], endCursor: null }],
});
await expect(resolveChecks(reader, context)).rejects.toBeInstanceOf(
ChecksUnavailable
);
});
});

describe("rollup node mapping", () => {
Expand Down
9 changes: 9 additions & 0 deletions pstack/skills/poteto-mode/scripts/watch-pr/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,15 @@ export async function resolveChecks(
} while (after !== null);
const fallback = nonEmpty(checks);
if (fallback !== null) return { source: "graphql-rollup", checks: fallback };
const fastPathConfirmsEmpty =
fast.kind === "checks" ||
(fast.exitCode === 1 &&
/^no checks reported on the .+ branch\s*$/im.test(fast.stderr));
if (fastPathConfirmsEmpty)
return {
source: fast.kind === "checks" ? "gh-pr-checks" : "graphql-rollup",
checks: [],
};
Comment thread
cursor[bot] marked this conversation as resolved.
const suffix =
fast.kind === "unusable"
? `fast path exit=${fast.exitCode}; GraphQL rollup was empty${firstLine(fast.stderr) ? `; ${firstLine(fast.stderr)}` : ""}`
Expand Down
36 changes: 36 additions & 0 deletions pstack/skills/poteto-mode/scripts/watch-pr/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ describe("readiness truth table", () => {
blocker: { kind: "failing-checks" },
});
});

it("classifies a PR with no configured checks as CI-clean", async () => {
const reader = fakeReader({
fastPath: { kind: "checks", checks: [] },
rollupPages: [{ checks: [], endCursor: null }],
});
const snapshot = await readSnapshot({
reader,
context: context(2),
pendingHistory: "include",
allowDraft: false,
});
expect(snapshot.kind).toBe("open");
if (snapshot.kind !== "open") throw new Error("expected open snapshot");
expect(snapshot.ci.kind).toBe("ci-clean");
expect(snapshot.ci.all).toEqual([]);
});

it("waits when a new head temporarily has no checks after prior CI passed", async () => {
const reader = fakeReader({
fastPath: { kind: "checks", checks: [] },
rollupPages: [{ checks: [], endCursor: null }],
commitRollups: [
{ oid: "previous", state: "SUCCESS" },
{ oid: "head", state: null },
],
});
await expect(readSnapshot({
reader,
context: context(3),
pendingHistory: "include",
allowDraft: false,
})).rejects.toThrow(
"PR head has no checks yet after a previously checked commit"
);
});
});

describe("snapshot query planning", () => {
Expand Down
10 changes: 9 additions & 1 deletion pstack/skills/poteto-mode/scripts/watch-pr/policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { WatcherQueryError, resolveChecks } from "./github.ts";
import {
ChecksUnavailable,
WatcherQueryError,
resolveChecks,
} from "./github.ts";
import type * as T from "./types.ts";
import { nonEmpty } from "./types.ts";
export function assessGitHubMerge(args: {
Expand Down Expand Up @@ -108,6 +112,10 @@ export async function readSnapshot(args: {
pending: pending ?? [],
github: merge.github,
};
else if (checks.checks.length === 0 && merge.hadPreviousPassingCi)
throw new ChecksUnavailable(
"PR head has no checks yet after a previously checked commit"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior SUCCESS fail-closes empty checks

Medium Severity · Logic Bug

readSnapshot throws ChecksUnavailable for a confirmed-empty check list whenever any non-head commit rollup is SUCCESS. Settled heads with no runs after skipped or path-filtered workflows match that predicate, so pollUntilTerminal retries and then emits a status-query BLOCKER instead of ci-clean. --status-only never prints a table.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 09591fc. Configure here.

else if (pending !== null)
ci = { ...base, kind: "ci-pending", failed: [], pending };
else
Expand Down
4 changes: 2 additions & 2 deletions pstack/skills/poteto-mode/scripts/watch-pr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export type FailedCheck = Extract<Check, { readonly kind: "failed" }>;
export type PendingCheck = Extract<Check, { readonly kind: "pending" }>;
export interface CheckRead {
readonly source: "gh-pr-checks" | "graphql-rollup";
readonly checks: NonEmpty<Check>;
readonly checks: readonly Check[];
}
export interface CommitRollup {
readonly oid: string;
Expand All @@ -115,7 +115,7 @@ export type GitHubMergeAllowed =
export type GitHubMergeAssessment = GitHubMergeAllowed | GitHubMergeRefusal;
interface CiBase {
readonly source: CheckRead["source"];
readonly all: NonEmpty<Check>;
readonly all: readonly Check[];
readonly hadPreviousPassingCi: boolean;
}
export type CiFailing = CiBase & {
Expand Down