@@ -1095,6 +1095,220 @@ describe('LifecycleService.sweep — governance (P4)', () => {
10951095 } ) ;
10961096} ) ;
10971097
1098+ // #8906 — the governance row-count probe used to fail into `catch { continue }`,
1099+ // which made "this object has nothing worth alerting on" and "the driver could
1100+ // not answer" the same observable event. The damage outlived the sweep: no
1101+ // `quota-exceeded`, no `growth`, AND the object dropped out of `nextCounts`, so
1102+ // the next sweep had no baseline to diff against either.
1103+ //
1104+ // The repair discriminates by error TYPE through the declared
1105+ // `isMissingTableError` predicate and surfaces everything else through the
1106+ // channels that already exist — `report.errors` plus a `warn` — with no new
1107+ // report field (the maintainer's 2026-08-15 disposition for this family:
1108+ // unprovisioned is truthful emptiness, everything else must surface).
1109+ //
1110+ // Every expectation below is written as a LITERAL, never derived from the code
1111+ // under test, and the benign case asserts that the injected throw really fired —
1112+ // otherwise "the sweep carried on" would also be satisfied by a harness that
1113+ // never probed at all.
1114+ describe ( 'LifecycleService.sweep — governance row-count probe failure (#8906)' , ( ) => {
1115+ const PROBED : LifecycleObjectLike = {
1116+ name : 'sys_job_run' ,
1117+ lifecycle : { class : 'telemetry' , retention : { maxAge : '30d' } } as any ,
1118+ } ;
1119+ /** A second declared object, so "one probe failed" can be told apart from
1120+ * "the governance leg stopped". */
1121+ const SIBLING : LifecycleObjectLike = {
1122+ name : 'sys_audit_log' ,
1123+ lifecycle : { class : 'telemetry' , retention : { maxAge : '30d' } } as any ,
1124+ } ;
1125+
1126+ function fakeSettings ( values : Record < string , unknown > ) {
1127+ return {
1128+ async get ( _ns : string , key : string ) {
1129+ if ( key in values ) return { value : values [ key ] , source : 'global' } ;
1130+ return { value : undefined , source : 'default' } ;
1131+ } ,
1132+ } ;
1133+ }
1134+
1135+ /** A driver whose `count` answers per object — throwing for the objects named
1136+ * in `throwsFor`, and returning `rows` for every other one. */
1137+ function countingDriver ( throwsFor : Record < string , unknown > , rows = 1_500 ) {
1138+ const count = vi . fn ( async ( object : string ) => {
1139+ if ( object in throwsFor ) throw throwsFor [ object ] ;
1140+ return rows ;
1141+ } ) ;
1142+ return { driver : { name : 'default' , count } , count } ;
1143+ }
1144+
1145+ /** POSITIVE CONTROL — the same harness, nothing injected. Without this, every
1146+ * refusal below is also consistent with a fixture that never alerts at all. */
1147+ it ( 'a healthy probe alerts on the quota and reports no error' , async ( ) => {
1148+ const warn = vi . fn ( ) ;
1149+ const { driver, count } = countingDriver ( { } ) ;
1150+ const { engine } = captureEngine ( [ PROBED ] , { driver } ) ;
1151+ const settings = fakeSettings ( { quotas : { sys_job_run : 1_000 } } ) ;
1152+
1153+ const report = await service ( engine , {
1154+ getSettings : ( ) => settings ,
1155+ logger : { ...silentLogger ( ) , warn } ,
1156+ // Alerts go to the sink, so `warn` carries only degradation reports —
1157+ // an alert of its own logs at `warn` when no sink is registered.
1158+ onAlert : ( ) => { } ,
1159+ } ) . sweep ( ) ;
1160+
1161+ expect ( count ) . toHaveBeenCalledWith ( 'sys_job_run' ) ;
1162+ expect ( report . alerts ) . toEqual ( [
1163+ { type : 'quota-exceeded' , object : 'sys_job_run' , rowCount : 1_500 , quota : 1_000 } ,
1164+ ] ) ;
1165+ expect ( report . errors ) . toEqual ( [ ] ) ;
1166+ expect ( warn ) . not . toHaveBeenCalled ( ) ;
1167+ } ) ;
1168+
1169+ it ( 'a non-benign probe failure is reported per object, at warn, naming the lost baseline' , async ( ) => {
1170+ const warn = vi . fn ( ) ;
1171+ const error = vi . fn ( ) ;
1172+ const { driver } = countingDriver ( { sys_job_run : new Error ( 'connection reset by peer' ) } ) ;
1173+ const { engine } = captureEngine ( [ PROBED ] , { driver } ) ;
1174+ const settings = fakeSettings ( { quotas : { sys_job_run : 1 } } ) ;
1175+
1176+ const report = await service ( engine , {
1177+ getSettings : ( ) => settings ,
1178+ logger : { ...silentLogger ( ) , warn, error } ,
1179+ } ) . sweep ( ) ;
1180+
1181+ // The report carries the incomplete fact in the field it already has —
1182+ // `errors` is the sweep's declared per-object failure channel, and the
1183+ // sweep's own summary line counts it. No new field was added.
1184+ expect ( report . errors ) . toEqual ( [
1185+ {
1186+ object : 'sys_job_run' ,
1187+ error :
1188+ 'governance row-count probe failed (connection reset by peer) — quota and growth alerting ' +
1189+ 'skipped for this object this sweep, and its growth baseline for the next sweep is lost' ,
1190+ } ,
1191+ ] ) ;
1192+ // The damage is still real and still visible: quota 1 would have breached
1193+ // on any count at all, and no alert could be raised for an object nobody
1194+ // could count.
1195+ expect ( report . alerts ) . toEqual ( [ ] ) ;
1196+ expect ( warn ) . toHaveBeenCalledWith (
1197+ '[lifecycle] governance row-count probe on sys_job_run failed (connection reset by peer); ' +
1198+ 'quota/growth alerting skipped this sweep and the next sweep has no growth baseline for it' ,
1199+ ) ;
1200+ // Reduced ALERTING, not a write that claimed to persist and did not —
1201+ // `warn`, deliberately, per AGENTS.md "Degradation log levels".
1202+ expect ( error ) . not . toHaveBeenCalled ( ) ;
1203+ } ) ;
1204+
1205+ it ( 'one failed probe never costs the other objects their governance' , async ( ) => {
1206+ const { driver } = countingDriver ( { sys_job_run : new Error ( 'connection reset by peer' ) } , 2_000 ) ;
1207+ const { engine } = captureEngine ( [ PROBED , SIBLING ] , { driver } ) ;
1208+ const settings = fakeSettings ( { quotas : { sys_job_run : 1 , sys_audit_log : 1_000 } } ) ;
1209+
1210+ const report = await service ( engine , { getSettings : ( ) => settings } ) . sweep ( ) ;
1211+
1212+ expect ( report . alerts ) . toEqual ( [
1213+ { type : 'quota-exceeded' , object : 'sys_audit_log' , rowCount : 2_000 , quota : 1_000 } ,
1214+ ] ) ;
1215+ expect ( report . errors ) . toEqual ( [
1216+ {
1217+ object : 'sys_job_run' ,
1218+ error :
1219+ 'governance row-count probe failed (connection reset by peer) — quota and growth alerting ' +
1220+ 'skipped for this object this sweep, and its growth baseline for the next sweep is lost' ,
1221+ } ,
1222+ ] ) ;
1223+ } ) ;
1224+
1225+ // The report is where an operator learns that the degradation OUTLIVES the
1226+ // sweep it happened in. This pins that honestly: the fix makes the loss
1227+ // visible, it does not invent a baseline to replace it.
1228+ it ( 'reports the lost baseline rather than repairing it — the next sweep still has no growth delta' , async ( ) => {
1229+ const outage = new Error ( 'connection reset by peer' ) ;
1230+ const throwsFor : Record < string , unknown > = { sys_job_run : outage } ;
1231+ const { driver } = countingDriver ( throwsFor , 5_000 ) ;
1232+ const { engine } = captureEngine ( [ PROBED ] , { driver } ) ;
1233+ const settings = fakeSettings ( { quotas : { sys_job_run : 100_000 } , growth_alert_rows : 100 } ) ;
1234+ const svc = service ( engine , { getSettings : ( ) => settings } ) ;
1235+
1236+ const first = await svc . sweep ( ) ;
1237+ expect ( first . errors ) . toHaveLength ( 1 ) ;
1238+ expect ( first . errors [ 0 ] . error ) . toContain ( 'growth baseline for the next sweep is lost' ) ;
1239+
1240+ // The driver recovers. There is still no `last` to diff 5_000 against, so
1241+ // no growth alert can be raised — exactly what the first report said.
1242+ delete throwsFor . sys_job_run ;
1243+ const second = await svc . sweep ( ) ;
1244+ expect ( second . errors ) . toEqual ( [ ] ) ;
1245+ expect ( second . alerts ) . toEqual ( [ ] ) ;
1246+
1247+ // And the baseline is rebuilt from here: the sweep after that CAN alert.
1248+ const { driver : grown } = countingDriver ( { } , 6_000 ) ;
1249+ ( engine as any ) . getDriverForObject = ( ) => grown ;
1250+ const third = await svc . sweep ( ) ;
1251+ expect ( third . alerts ) . toEqual ( [
1252+ { type : 'growth' , object : 'sys_job_run' , rowCount : 6_000 , delta : 1_000 } ,
1253+ ] ) ;
1254+ } ) ;
1255+
1256+ it ( 'an unprovisioned table stays silent — truthful emptiness, not a swallowed outage' , async ( ) => {
1257+ const warn = vi . fn ( ) ;
1258+ const missingTable = new Error ( 'no such table: sys_job_run' ) ;
1259+ const { driver, count } = countingDriver ( { sys_job_run : missingTable } , 2_000 ) ;
1260+ const { engine } = captureEngine ( [ PROBED , SIBLING ] , { driver } ) ;
1261+ const settings = fakeSettings ( { quotas : { sys_job_run : 1 , sys_audit_log : 1_000 } } ) ;
1262+
1263+ const report = await service ( engine , {
1264+ getSettings : ( ) => settings ,
1265+ logger : { ...silentLogger ( ) , warn } ,
1266+ onAlert : ( ) => { } ,
1267+ } ) . sweep ( ) ;
1268+
1269+ // The injected throw ACTUALLY FIRED — without this, "the sweep continued"
1270+ // is also what a harness that never probed would produce.
1271+ expect ( count ) . toHaveBeenCalledWith ( 'sys_job_run' ) ;
1272+ await expect ( count . mock . results [ 0 ] ! . value ) . rejects . toThrow ( 'no such table: sys_job_run' ) ;
1273+
1274+ // A table that does not exist holds no rows: no quota to breach, no growth
1275+ // to measure, nothing to report and nothing to log.
1276+ expect ( report . errors ) . toEqual ( [ ] ) ;
1277+ expect ( warn ) . not . toHaveBeenCalled ( ) ;
1278+ // …and the sweep carried on, proved by the sibling's alert.
1279+ expect ( report . alerts ) . toEqual ( [
1280+ { type : 'quota-exceeded' , object : 'sys_audit_log' , rowCount : 2_000 , quota : 1_000 } ,
1281+ ] ) ;
1282+ } ) ;
1283+
1284+ // The benign verdict comes from the declared predicate, not from a substring
1285+ // guess: Postgres phrases a missing COLUMN on an existing relation as
1286+ // `column "x" of relation "y" does not exist`, which CONTAINS a legal
1287+ // missing-table phrase. The table is there and its rows were not counted, so
1288+ // this must surface.
1289+ it ( 'a missing column on a provisioned table is not benign, though its message contains a table phrase' , async ( ) => {
1290+ const columnGone : Error & { code ?: string } = new Error (
1291+ 'column "status" of relation "sys_job_run" does not exist' ,
1292+ ) ;
1293+ columnGone . code = '42703' ;
1294+ const { driver } = countingDriver ( { sys_job_run : columnGone } ) ;
1295+ const { engine } = captureEngine ( [ PROBED ] , { driver } ) ;
1296+ const settings = fakeSettings ( { quotas : { sys_job_run : 1 } } ) ;
1297+
1298+ const report = await service ( engine , { getSettings : ( ) => settings } ) . sweep ( ) ;
1299+
1300+ expect ( report . errors ) . toEqual ( [
1301+ {
1302+ object : 'sys_job_run' ,
1303+ error :
1304+ 'governance row-count probe failed (column "status" of relation "sys_job_run" does not exist) — ' +
1305+ 'quota and growth alerting skipped for this object this sweep, and its growth baseline for the ' +
1306+ 'next sweep is lost' ,
1307+ } ,
1308+ ] ) ;
1309+ } ) ;
1310+ } ) ;
1311+
10981312// #5195 — ADR-0057 P4 lets an operator override any object's window through the
10991313// `lifecycle` settings namespace, and until this the only validation on that
11001314// override was "does it parse". That is a side door around #5179's invariant:
0 commit comments