33// Every category comes from recorded run fields, so these cases pin the exact boundaries an alert
44// fires on: a lane that stopped running, one that failed twice, and one that is merely flaky.
55
6+ import { spawnSync } from 'node:child_process' ;
7+ import fs from 'node:fs' ;
8+ import os from 'node:os' ;
69import path from 'node:path' ;
710import { fileURLToPath } from 'node:url' ;
811import { describe , expect , it } from 'vitest' ;
@@ -25,8 +28,12 @@ const NIGHTLY: LaneCadence = {
2528 cadenceHours : 24 ,
2629} ;
2730
28- function known ( runs : LaneRun [ ] ) {
29- return { known : true , runs } ;
31+ function known ( runs : LaneRun [ ] , registeredHoursAgo = 1000 ) {
32+ return {
33+ known : true ,
34+ registeredAt : new Date ( NOW - registeredHoursAgo * 60 * 60 * 1000 ) . toISOString ( ) ,
35+ runs,
36+ } ;
3037}
3138
3239function run ( hoursAgo : number , conclusion : LaneRun [ 'conclusion' ] ) : LaneRun {
@@ -75,13 +82,25 @@ describe('lane health', () => {
7582 expect ( lane . reason ) . toContain ( 'over 48h of cadence' ) ;
7683 } ) ;
7784
78- it ( 'is dark when the lane has never run on schedule' , ( ) => {
85+ it ( 'is dark when a long-registered lane has never run on schedule' , ( ) => {
7986 const lane = laneHealth ( NIGHTLY , known ( [ ] ) , NOW ) ;
8087 expect ( lane . state ) . toBe ( 'dark' ) ;
8188 expect ( lane . lastRunAt ) . toBeNull ( ) ;
8289 expect ( lane . hoursSinceLastSuccess ) . toBeNull ( ) ;
8390 } ) ;
8491
92+ it ( 'gives a newborn lane the same two-cadence grace before its first run' , ( ) => {
93+ const lane = laneHealth ( NIGHTLY , known ( [ ] , 5 ) , NOW ) ;
94+ expect ( lane . state ) . toBe ( 'pending' ) ;
95+ expect ( lane . reason ) . toContain ( 'first run not yet 48h overdue' ) ;
96+ expect ( unhealthyLanes ( [ lane ] ) ) . toEqual ( [ ] ) ;
97+ } ) ;
98+
99+ it ( 'stays pending when a run-less lane has no registration date to judge' , ( ) => {
100+ const lane = laneHealth ( NIGHTLY , { known : true , registeredAt : null , runs : [ ] } , NOW ) ;
101+ expect ( lane . state ) . toBe ( 'pending' ) ;
102+ } ) ;
103+
85104 it ( 'is pending, not dark, when the run history could not be read' , ( ) => {
86105 const lane = laneHealth ( NIGHTLY , { known : false , reason : 'no token' , runs : [ ] } , NOW ) ;
87106 expect ( lane . state ) . toBe ( 'pending' ) ;
@@ -94,6 +113,44 @@ describe('lane health', () => {
94113 } ) ;
95114} ) ;
96115
116+ describe ( 'terminal failures' , ( ) => {
117+ // A monitor whose API call fails must still leave evidence behind, or the lane it watches goes
118+ // dark twice over: once in CI and once in the artifact a human would look for.
119+ it ( 'writes an error envelope and snapshot when the Actions API is unreachable' , ( ) => {
120+ const artifactDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'lane-health-' ) ) ;
121+ const result = spawnSync (
122+ process . execPath ,
123+ [
124+ '--experimental-strip-types' ,
125+ path . join ( REPO_ROOT , 'scripts/scheduled-lane/health.ts' ) ,
126+ '--artifact-dir' ,
127+ artifactDir ,
128+ '--dry-run' ,
129+ ] ,
130+ {
131+ cwd : REPO_ROOT ,
132+ encoding : 'utf8' ,
133+ env : {
134+ ...process . env ,
135+ GITHUB_TOKEN : 'not-a-real-token' ,
136+ GITHUB_API_URL : 'http://127.0.0.1:1' ,
137+ } ,
138+ } ,
139+ ) ;
140+ expect ( result . status ) . toBe ( 1 ) ;
141+ const envelope = JSON . parse (
142+ fs . readFileSync ( path . join ( artifactDir , 'run-envelope.json' ) , 'utf8' ) ,
143+ ) ;
144+ expect ( envelope . result ) . toBe ( 'error' ) ;
145+ expect ( envelope . lane ) . toBe ( 'scheduled-lane-health' ) ;
146+ expect ( String ( envelope . details . error ) . length ) . toBeGreaterThan ( 0 ) ;
147+ const snapshot = JSON . parse (
148+ fs . readFileSync ( path . join ( artifactDir , 'lane-health.json' ) , 'utf8' ) ,
149+ ) ;
150+ expect ( snapshot . error ) . not . toBeNull ( ) ;
151+ } ) ;
152+ } ) ;
153+
97154describe ( 'lane discovery' , ( ) => {
98155 // Derived from the workflow directory: a scheduled lane cannot opt out of being watched.
99156 it ( 'finds every schedule-triggered workflow in this repo' , ( ) => {
0 commit comments