|
| 1 | +import { readFileSync } from "node:fs"; |
1 | 2 | import { afterEach, describe, expect, it, vi } from "vitest"; |
2 | 3 | import { |
3 | 4 | claimMaintainerRecapPeriod, |
|
8 | 9 | countRecentAuditEventsForActorAndTarget, |
9 | 10 | countRecentAuditEventsForActorInRepo, |
10 | 11 | countRecentAuditEventsForActorInRepoWithTargetSuffix, |
| 12 | + recentStaleRecheckDeniedPullNumbers, |
11 | 13 | findHottestInconclusiveReviewTargetForRepo, |
12 | 14 | findHottestReviewTargetForRepo, |
13 | 15 | hasAuditEventForDelivery, |
@@ -946,6 +948,124 @@ describe("database row parser hardening", () => { |
946 | 948 | expect(await countRecentAuditEventsForActorInRepoWithTargetSuffix(env, "chatty", "github_app.monitored_mention_ping", "owner/foo_bar", "mention:some_login", "2026-06-24T09:00:00.000Z")).toBe(1); |
947 | 949 | }); |
948 | 950 |
|
| 951 | + describe("recentStaleRecheckDeniedPullNumbers (#orb-stale-recheck-priority)", () => { |
| 952 | + it("returns the PR number for a duplicate-cluster-winner staleness denial", async () => { |
| 953 | + const env = createTestEnv(); |
| 954 | + await recordAuditEvent(env, { |
| 955 | + eventType: "agent.action.close", |
| 956 | + actor: "loopover", |
| 957 | + targetKey: "owner/repo#5", |
| 958 | + outcome: "denied", |
| 959 | + detail: "duplicate-cluster winner #7437 is no longer open — action not executed", |
| 960 | + createdAt: "2026-06-24T10:00:00.000Z", |
| 961 | + }); |
| 962 | + |
| 963 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/repo", "2026-06-24T09:00:00.000Z")).toEqual([5]); |
| 964 | + }); |
| 965 | + |
| 966 | + it("returns the PR number for a base-conflict or a review-thread staleness denial, and dedupes repeats for the same PR", async () => { |
| 967 | + const env = createTestEnv(); |
| 968 | + await recordAuditEvent(env, { |
| 969 | + eventType: "agent.action.close", |
| 970 | + actor: "loopover", |
| 971 | + targetKey: "owner/repo#6", |
| 972 | + outcome: "denied", |
| 973 | + detail: "the base-branch conflict that justified this close has since cleared — action not executed", |
| 974 | + createdAt: "2026-06-24T10:00:00.000Z", |
| 975 | + }); |
| 976 | + // A second denial for the SAME pr later in the window must not produce a duplicate entry. |
| 977 | + await recordAuditEvent(env, { |
| 978 | + eventType: "agent.action.merge", |
| 979 | + actor: "loopover", |
| 980 | + targetKey: "owner/repo#6", |
| 981 | + outcome: "denied", |
| 982 | + detail: "the review thread(s) that justified this close are now all resolved — action not executed", |
| 983 | + createdAt: "2026-06-24T10:05:00.000Z", |
| 984 | + }); |
| 985 | + |
| 986 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/repo", "2026-06-24T09:00:00.000Z")).toEqual([6]); |
| 987 | + }); |
| 988 | + |
| 989 | + it("ignores a denial reason that is NOT one of the three self-resolving staleness reasons", async () => { |
| 990 | + const env = createTestEnv(); |
| 991 | + // A manual-review-label denial is durable (needs a human to remove the label) -- retrying fast wastes a |
| 992 | + // cycle for zero output, so it must not be surfaced here. |
| 993 | + await recordAuditEvent(env, { |
| 994 | + eventType: "agent.action.merge", |
| 995 | + actor: "loopover", |
| 996 | + targetKey: "owner/repo#7", |
| 997 | + outcome: "denied", |
| 998 | + detail: 'manual-review label "manual-review" is present on the live PR — merge not executed', |
| 999 | + createdAt: "2026-06-24T10:00:00.000Z", |
| 1000 | + }); |
| 1001 | + // A live-CI staleness denial already gets a fresh look from the check-run/status webhook that changed |
| 1002 | + // CI in the first place, so it's deliberately excluded too. |
| 1003 | + await recordAuditEvent(env, { |
| 1004 | + eventType: "agent.action.merge", |
| 1005 | + actor: "loopover", |
| 1006 | + targetKey: "owner/repo#8", |
| 1007 | + outcome: "denied", |
| 1008 | + detail: "live CI is no longer passing (now: pending) — action not executed", |
| 1009 | + createdAt: "2026-06-24T10:00:00.000Z", |
| 1010 | + }); |
| 1011 | + |
| 1012 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/repo", "2026-06-24T09:00:00.000Z")).toEqual([]); |
| 1013 | + }); |
| 1014 | + |
| 1015 | + it("ignores a matching denial outside the actor/eventType/outcome/repo/time scope", async () => { |
| 1016 | + const env = createTestEnv(); |
| 1017 | + const matchingDetail = "duplicate-cluster winner #1 is no longer open — action not executed"; |
| 1018 | + // Wrong actor. |
| 1019 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "someone-else", targetKey: "owner/repo#10", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1020 | + // Wrong event type (a "completed" close whose OWN detail happens to reuse the phrase, e.g. a comment quoting it). |
| 1021 | + await recordAuditEvent(env, { eventType: "agent.action.hold", actor: "loopover", targetKey: "owner/repo#11", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1022 | + // Wrong outcome (the SAME reason text, but the action actually went through). |
| 1023 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/repo#12", outcome: "completed", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1024 | + // Wrong repo. |
| 1025 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/other-repo#13", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1026 | + // Before the cutoff. |
| 1027 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/repo#14", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T07:00:00.000Z" }); |
| 1028 | + |
| 1029 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/repo", "2026-06-24T09:00:00.000Z")).toEqual([]); |
| 1030 | + }); |
| 1031 | + |
| 1032 | + it("treats the repo name as a literal LIKE prefix (regression mirroring countRecentAuditEventsForActorInRepo's escaping fix)", async () => { |
| 1033 | + const env = createTestEnv(); |
| 1034 | + const matchingDetail = "duplicate-cluster winner #1 is no longer open — action not executed"; |
| 1035 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/foo_bar#20", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1036 | + // owner/fooXbar is a DIFFERENT repo that would spuriously match "owner/foo_bar#%" if `_` were left as a |
| 1037 | + // SQL wildcard instead of being escaped. |
| 1038 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/fooXbar#21", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:01:00.000Z" }); |
| 1039 | + |
| 1040 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/foo_bar", "2026-06-24T09:00:00.000Z")).toEqual([20]); |
| 1041 | + }); |
| 1042 | + |
| 1043 | + it("skips a matching row whose targetKey fails to parse into a pull number (matches the LIKE prefix but has a non-numeric suffix)", async () => { |
| 1044 | + const env = createTestEnv(); |
| 1045 | + const matchingDetail = "duplicate-cluster winner #1 is no longer open — action not executed"; |
| 1046 | + // Satisfies the "owner/repo#%" SQL prefix but parsePullRequestTargetKey rejects the non-integer suffix. |
| 1047 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/repo#abc", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:00:00.000Z" }); |
| 1048 | + await recordAuditEvent(env, { eventType: "agent.action.close", actor: "loopover", targetKey: "owner/repo#22", outcome: "denied", detail: matchingDetail, createdAt: "2026-06-24T10:01:00.000Z" }); |
| 1049 | + |
| 1050 | + expect(await recentStaleRecheckDeniedPullNumbers(env, "owner/repo", "2026-06-24T09:00:00.000Z")).toEqual([22]); |
| 1051 | + }); |
| 1052 | + |
| 1053 | + // #orb-stale-recheck-priority parity guard: STALE_RECHECK_DENIAL_DETAIL_PATTERN hand-types the three |
| 1054 | + // "denied" reason strings the executor's live staleness rechecks can produce, rather than importing them |
| 1055 | + // from agent-action-executor.ts -- that module imports FROM db/repositories.ts, so the reverse import would |
| 1056 | + // create a real module-load cycle (same hazard CONCRETE_EVIDENCE_BLOCKER_CODES's own parity guard, |
| 1057 | + // agent-actions.test.ts, documents). This reads the real producer source text instead, so a future rewording |
| 1058 | + // at the producer fails this test immediately rather than silently making the hand-typed pattern permanently |
| 1059 | + // unmatchable. |
| 1060 | + it.each(["duplicate-cluster winner #${action.duplicateWinnerPrNumber} is no longer open", "the base-branch conflict that justified this close has since cleared", "the review thread(s) that justified this close are now all resolved"])( |
| 1061 | + "%s is still produced (not merely mentioned) in its producer (src/services/agent-action-executor.ts)", |
| 1062 | + (reason) => { |
| 1063 | + const source = readFileSync("src/services/agent-action-executor.ts", "utf8"); |
| 1064 | + expect(source).toContain(reason); |
| 1065 | + }, |
| 1066 | + ); |
| 1067 | + }); |
| 1068 | + |
949 | 1069 | it("findHottestReviewTargetForRepo returns the PR with the most published surfaces in the window, scoped to ONE repo (#orb-ci-stuck-repeat)", async () => { |
950 | 1070 | const env = createTestEnv(); |
951 | 1071 | const publish = (targetKey: string, createdAt: string) => |
|
0 commit comments