@@ -17,7 +17,11 @@ import {
1717 CAPTURE_UNOBTAINABLE_REASON ,
1818 type ScreenshotMatrixPair ,
1919} from "../../src/review/screenshot-table-gate" ;
20- import type { ScreenshotTableGateConfig } from "../../src/types" ;
20+ import type { Advisory , PullRequestFileRecord , ScreenshotTableGateConfig } from "../../src/types" ;
21+ import { maybeAddScreenshotTableAdvisoryFinding } from "../../src/queue/processors" ;
22+ import { planAgentMaintenanceActions , type AgentActionPlanInput } from "../../src/settings/agent-actions" ;
23+ import type { GateCheckConclusion } from "../../src/rules/advisory" ;
24+ import { createTestEnv } from "../helpers/d1" ;
2125
2226function config ( overrides : Partial < ScreenshotTableGateConfig > = { } ) : ScreenshotTableGateConfig {
2327 return { ...DEFAULT_SCREENSHOT_TABLE_GATE , whenLabels : [ ] , whenPaths : [ ] , ...overrides } ;
@@ -999,3 +1003,220 @@ describe("enforcement degrade when capture is unobtainable (#9881)", () => {
9991003 expect ( before ) . toEqual ( evaluateScreenshotTableGate ( { ...violatingInput , captureUnobtainable : undefined } ) ) ;
10001004 } ) ;
10011005} ) ;
1006+
1007+ // #10060: maybeAddScreenshotTableAdvisoryFinding previously early-returned before evaluating anything unless
1008+ // `action === "advisory"`, so a `close`/`block` repo whose gate was degraded (#9881) never surfaced a finding
1009+ // anywhere — the close/hold comment that would have said so never fires on the degraded path either, so the
1010+ // maintainer learned nothing. These tests pin the fixed wiring.
1011+ describe ( "maybeAddScreenshotTableAdvisoryFinding degrade wiring (#10060)" , ( ) => {
1012+ function advisory ( ) : Advisory {
1013+ return {
1014+ id : "adv-1" ,
1015+ targetType : "pull_request" ,
1016+ repoFullName : "acme/widgets" ,
1017+ pullNumber : 7 ,
1018+ targetKey : "acme/widgets#7" ,
1019+ headSha : "sha7" ,
1020+ conclusion : "neutral" ,
1021+ severity : "info" ,
1022+ title : "LoopOver advisory available" ,
1023+ summary : "ok" ,
1024+ findings : [ ] ,
1025+ generatedAt : "2026-07-31T00:00:00.000Z" ,
1026+ } ;
1027+ }
1028+
1029+ const NO_TABLE_FILES : PullRequestFileRecord [ ] = [
1030+ { repoFullName : "acme/widgets" , pullNumber : 7 , path : "src/app.tsx" , status : "modified" , additions : 1 , deletions : 0 , changes : 1 , payload : { } } ,
1031+ ] ;
1032+
1033+ function gateConfig ( action : "close" | "block" | "advisory" ) : ScreenshotTableGateConfig {
1034+ return { ...DEFAULT_SCREENSHOT_TABLE_GATE , enabled : true , whenLabels : [ ] , whenPaths : [ ] , action } ;
1035+ }
1036+
1037+ it ( "action: close, violated, captureUnobtainable: true — appends exactly one finding naming the remedy, and the same inputs plan no close/hold" , async ( ) => {
1038+ const env = createTestEnv ( ) ;
1039+ const adv = advisory ( ) ;
1040+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1041+ advisory : adv ,
1042+ repoFullName : "acme/widgets" ,
1043+ pullNumber : 7 ,
1044+ screenshotTableGateConfig : gateConfig ( "close" ) ,
1045+ prBody : "no table here" ,
1046+ prLabels : [ ] ,
1047+ botCaptureSatisfied : false ,
1048+ captureUnobtainable : true ,
1049+ files : NO_TABLE_FILES ,
1050+ } ) ;
1051+ expect ( adv . findings ) . toHaveLength ( 1 ) ;
1052+ expect ( adv . findings [ 0 ] ?. code ) . toBe ( "screenshot_table_missing" ) ;
1053+ expect ( adv . findings [ 0 ] ?. detail ) . toContain ( CAPTURE_UNOBTAINABLE_REASON ) ;
1054+ expect ( adv . findings [ 0 ] ?. publicText ) . toContain ( CAPTURE_UNOBTAINABLE_REASON ) ;
1055+
1056+ // The same degraded facts the real caller threads through (processors.ts): screenshotTableMatch and
1057+ // screenshotEvidenceHold both stay absent, and screenshotTableEvidenceUnresolved stays false, so the
1058+ // planner falls through to ordinary disposition instead of closing or holding.
1059+ const plan = planAgentMaintenanceActions ( {
1060+ blockerTitles : [ ] ,
1061+ autonomy : { merge : "auto" , close : "auto" , review_state_label : "auto" } ,
1062+ autoMaintain : { requireApprovals : 1 , mergeMethod : "squash" } ,
1063+ slopGateMinScore : 60 ,
1064+ changedPaths : [ ] ,
1065+ hardGuardrailGlobs : [ ] ,
1066+ authorIsOwner : false ,
1067+ authorIsAdmin : false ,
1068+ authorIsAutomationBot : false ,
1069+ ciState : "passed" ,
1070+ conclusion : "success" as GateCheckConclusion ,
1071+ manualReviewLabel : "human-review" ,
1072+ screenshotTableMatch : undefined ,
1073+ screenshotEvidenceHold : undefined ,
1074+ screenshotTableEvidenceUnresolved : false ,
1075+ pr : { labels : [ ] , mergeableState : "clean" , reviewDecision : "APPROVED" } ,
1076+ } satisfies AgentActionPlanInput ) ;
1077+ expect ( plan . some ( ( a ) => a . actionClass === "close" ) ) . toBe ( false ) ;
1078+ expect ( plan . some ( ( a ) => a . actionClass === "label" && a . label === "human-review" && a . labelOp !== "remove" ) ) . toBe ( false ) ;
1079+ } ) ;
1080+
1081+ it ( "action: block, violated, captureUnobtainable: true — appends the same degraded finding, and no hold is planned" , async ( ) => {
1082+ const env = createTestEnv ( ) ;
1083+ const adv = advisory ( ) ;
1084+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1085+ advisory : adv ,
1086+ repoFullName : "acme/widgets" ,
1087+ pullNumber : 7 ,
1088+ screenshotTableGateConfig : gateConfig ( "block" ) ,
1089+ prBody : "no table here" ,
1090+ prLabels : [ ] ,
1091+ botCaptureSatisfied : false ,
1092+ captureUnobtainable : true ,
1093+ files : NO_TABLE_FILES ,
1094+ } ) ;
1095+ expect ( adv . findings ) . toHaveLength ( 1 ) ;
1096+ expect ( adv . findings [ 0 ] ?. detail ) . toContain ( CAPTURE_UNOBTAINABLE_REASON ) ;
1097+
1098+ const plan = planAgentMaintenanceActions ( {
1099+ blockerTitles : [ ] ,
1100+ autonomy : { merge : "auto" , review_state_label : "auto" } ,
1101+ autoMaintain : { requireApprovals : 1 , mergeMethod : "squash" } ,
1102+ slopGateMinScore : 60 ,
1103+ changedPaths : [ ] ,
1104+ hardGuardrailGlobs : [ ] ,
1105+ authorIsOwner : false ,
1106+ authorIsAdmin : false ,
1107+ authorIsAutomationBot : false ,
1108+ ciState : "passed" ,
1109+ conclusion : "success" as GateCheckConclusion ,
1110+ manualReviewLabel : "human-review" ,
1111+ screenshotTableMatch : undefined ,
1112+ screenshotEvidenceHold : undefined ,
1113+ screenshotTableEvidenceUnresolved : false ,
1114+ pr : { labels : [ ] , mergeableState : "clean" , reviewDecision : "APPROVED" } ,
1115+ } satisfies AgentActionPlanInput ) ;
1116+ expect ( plan . some ( ( a ) => a . actionClass === "label" && a . label === "human-review" && a . labelOp !== "remove" ) ) . toBe ( false ) ;
1117+ expect ( plan . some ( ( a ) => a . actionClass === "close" ) ) . toBe ( false ) ;
1118+ } ) ;
1119+
1120+ it ( "action: close, violated, captureUnobtainable: false — pins today's behavior: NO advisory finding" , async ( ) => {
1121+ const env = createTestEnv ( ) ;
1122+ const adv = advisory ( ) ;
1123+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1124+ advisory : adv ,
1125+ repoFullName : "acme/widgets" ,
1126+ pullNumber : 7 ,
1127+ screenshotTableGateConfig : gateConfig ( "close" ) ,
1128+ prBody : "no table here" ,
1129+ prLabels : [ ] ,
1130+ botCaptureSatisfied : false ,
1131+ captureUnobtainable : false ,
1132+ files : NO_TABLE_FILES ,
1133+ } ) ;
1134+ expect ( adv . findings ) . toEqual ( [ ] ) ;
1135+ } ) ;
1136+
1137+ it ( "action: advisory, violated, captureUnobtainable: false — byte-identical finding to today" , async ( ) => {
1138+ const env = createTestEnv ( ) ;
1139+ const adv = advisory ( ) ;
1140+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1141+ advisory : adv ,
1142+ repoFullName : "acme/widgets" ,
1143+ pullNumber : 7 ,
1144+ screenshotTableGateConfig : gateConfig ( "advisory" ) ,
1145+ prBody : "no table here" ,
1146+ prLabels : [ ] ,
1147+ botCaptureSatisfied : false ,
1148+ captureUnobtainable : false ,
1149+ files : NO_TABLE_FILES ,
1150+ } ) ;
1151+ expect ( adv . findings ) . toHaveLength ( 1 ) ;
1152+ expect ( adv . findings [ 0 ] ) . toMatchObject ( {
1153+ code : "screenshot_table_missing" ,
1154+ severity : "warning" ,
1155+ title : "Missing before/after screenshot table" ,
1156+ action : "Add a before/after screenshot table to the pull request description (advisory only — this does not block merge)." ,
1157+ } ) ;
1158+ expect ( adv . findings [ 0 ] ?. detail ) . not . toContain ( CAPTURE_UNOBTAINABLE_REASON ) ;
1159+ } ) ;
1160+
1161+ it ( "REGRESSION (#10060): a degraded action: close evaluation never produces a completely silent pass" , async ( ) => {
1162+ const env = createTestEnv ( ) ;
1163+ const adv = advisory ( ) ;
1164+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1165+ advisory : adv ,
1166+ repoFullName : "acme/widgets" ,
1167+ pullNumber : 7 ,
1168+ screenshotTableGateConfig : gateConfig ( "close" ) ,
1169+ prBody : "no table here" ,
1170+ prLabels : [ ] ,
1171+ botCaptureSatisfied : false ,
1172+ captureUnobtainable : true ,
1173+ files : NO_TABLE_FILES ,
1174+ } ) ;
1175+ expect ( adv . findings . length ) . toBeGreaterThan ( 0 ) ;
1176+ } ) ;
1177+
1178+ it ( "not enabled: does not scan, no finding appended even when captureUnobtainable is true" , async ( ) => {
1179+ const env = createTestEnv ( ) ;
1180+ const adv = advisory ( ) ;
1181+ await maybeAddScreenshotTableAdvisoryFinding ( env , {
1182+ advisory : adv ,
1183+ repoFullName : "acme/widgets" ,
1184+ pullNumber : 7 ,
1185+ screenshotTableGateConfig : { ...gateConfig ( "close" ) , enabled : false } ,
1186+ prBody : "no table here" ,
1187+ prLabels : [ ] ,
1188+ botCaptureSatisfied : false ,
1189+ captureUnobtainable : true ,
1190+ files : NO_TABLE_FILES ,
1191+ } ) ;
1192+ expect ( adv . findings ) . toEqual ( [ ] ) ;
1193+ } ) ;
1194+
1195+ it ( "fail-safe: a thrown error while loading files never propagates and appends no finding" , async ( ) => {
1196+ const env = createTestEnv ( ) ;
1197+ const adv = advisory ( ) ;
1198+ const throwingEnv = {
1199+ ...env ,
1200+ DB : {
1201+ ...env . DB ,
1202+ prepare : ( ) => {
1203+ throw new Error ( "boom" ) ;
1204+ } ,
1205+ } ,
1206+ } as unknown as typeof env ;
1207+ await expect (
1208+ maybeAddScreenshotTableAdvisoryFinding ( throwingEnv , {
1209+ advisory : adv ,
1210+ repoFullName : "acme/widgets" ,
1211+ pullNumber : 7 ,
1212+ screenshotTableGateConfig : gateConfig ( "close" ) ,
1213+ prBody : "no table here" ,
1214+ prLabels : [ ] ,
1215+ botCaptureSatisfied : false ,
1216+ captureUnobtainable : true ,
1217+ files : null ,
1218+ } ) ,
1219+ ) . resolves . toBeUndefined ( ) ;
1220+ expect ( adv . findings ) . toEqual ( [ ] ) ;
1221+ } ) ;
1222+ } ) ;
0 commit comments