@@ -174,13 +174,75 @@ export function buildDataset({ perSummary, fileCounts, provenance }) {
174174 } ;
175175}
176176
177+ // -- The self-test's own battery roster and floor (#13489) ------------------
178+ //
179+ // `--self-test` reaching its verdict used to be this self-test's ONLY success
180+ // condition, so "every case held" and "the cases never ran" printed the same
181+ // line. Closed the way PR #13487 validated on check-doc-authoring: what is
182+ // pinned is the registered NAMES, not a number.
183+ //
184+ // This file's assertions are bare `throw`s rather than calls to an assertion
185+ // helper, so they are counted by the `check(() => { ... })` THUNK PR #15198
186+ // measured: the existing `if (...) throw ...` is carried into the thunk
187+ // VERBATIM and the condition is never touched. Routing these through a boolean
188+ // helper instead would mean inverting 22 failure conditions by hand, and a
189+ // dropped `!` yields an assertion that still registers its case and still
190+ // passes -- invisible to the very floor being installed here. The throw still
191+ // propagates: this file fails fast on the FIRST broken assertion, as it always
192+ // has.
193+ //
194+ // This file declares ONE battery, opened at the top of the self-test body. Its
195+ // blocks are headed by unmarked prose comments -- it carries ZERO named section
196+ // banners, fewer than the two the sectioning criterion needs, and a comment is
197+ // NOT promoted to a section head (that is a judgement per comment this
198+ // transplant does not make). The hoisted single battery is the shape PR #14896,
199+ // PR #15003 and PR #15217 landed for exactly this case.
200+ //
201+ // The count is a FLOOR, not an equality -- adding cases is ordinary work and
202+ // must not red. A battery BELOW its floor means cases stopped running; the
203+ // remedy is to find what stopped registering, never to lower the number.
204+ const SELF_TEST_BATTERIES = Object . freeze ( {
205+ 'measure-test-shard-timings self-test' : 22 ,
206+ } ) ;
207+
208+ // DELETING an entry silences that battery's floor exactly as effectively as
209+ // zeroing it, so the roster's own size is pinned too.
210+ const SELF_TEST_BATTERY_FLOOR = 1 ;
211+
212+ // The key an assertion is filed under when no battery is open. It is not a
213+ // declared battery, so it reds by the same set difference rather than silently
214+ // inflating whichever battery happened to run last.
215+ const UNATTRIBUTED_BATTERY = '(no battery open)' ;
216+
177217// Returned by `selfTest()` only after its verdict is printed. The dispatch
178218// refuses anything else: a `return` that leaves the function above that line
179219// prints nothing and still exits 0 — a self-test that never finished, reported
180220// as one that passed (#13798).
181221const SELF_TEST_VERDICT = 'measure-test-shard-timings self-test reached its verdict' ;
182222
183223function selfTest ( ) {
224+ // The battery ledger this self-test's floor is evaluated against (#13489).
225+ // `battery()` opens a battery; every assertion below is attributed to the one
226+ // most recently opened, so a section that stops running stops registering and
227+ // names ITSELF at the floor rather than going quiet.
228+ const batterySeen = new Map ( ) ;
229+ let openBattery = null ;
230+ const battery = ( name ) => {
231+ openBattery = name ;
232+ } ;
233+ const registerCase = ( ) => {
234+ const b = openBattery ?? UNATTRIBUTED_BATTERY ;
235+ batterySeen . set ( b , ( batterySeen . get ( b ) ?? 0 ) + 1 ) ;
236+ } ;
237+ battery ( 'measure-test-shard-timings self-test' ) ;
238+ // The thunk: it registers the case and then runs the existing site VERBATIM,
239+ // so no assertion condition is inverted or rewritten and the sink keeps its
240+ // own semantics. Registration happens whether or not the site fires, which is
241+ // what makes the count a floor on cases RUN rather than a count of failures.
242+ const check = ( fn ) => {
243+ registerCase ( ) ;
244+ fn ( ) ;
245+ } ;
184246 const summary = ( tasks ) => ( { tasks } ) ;
185247 const testTask = ( pkg , start , end , status = 'MISS' , exitCode = 0 ) => ( {
186248 taskId : `${ pkg } #test` ,
@@ -190,9 +252,15 @@ function selfTest() {
190252 execution : { startTime : start , endTime : end , exitCode } ,
191253 } ) ;
192254
193- if ( median ( [ 3 ] ) !== 3 ) throw new Error ( 'median: single value' ) ;
194- if ( median ( [ 5 , 1 , 3 ] ) !== 3 ) throw new Error ( 'median: odd length is not order-dependent' ) ;
195- if ( median ( [ 1 , 2 , 3 , 4 ] ) !== 2.5 ) throw new Error ( 'median: even length averages the middle pair' ) ;
255+ check ( ( ) => {
256+ if ( median ( [ 3 ] ) !== 3 ) throw new Error ( 'median: single value' ) ;
257+ } ) ;
258+ check ( ( ) => {
259+ if ( median ( [ 5 , 1 , 3 ] ) !== 3 ) throw new Error ( 'median: odd length is not order-dependent' ) ;
260+ } ) ;
261+ check ( ( ) => {
262+ if ( median ( [ 1 , 2 , 3 , 4 ] ) !== 2.5 ) throw new Error ( 'median: even length averages the middle pair' ) ;
263+ } ) ;
196264
197265 // A build task in the same summary must not be read as a test duration.
198266 const mixed = samplesFromSummary (
@@ -202,23 +270,37 @@ function selfTest() {
202270 ] ) ,
203271 'f'
204272 ) ;
205- if ( mixed . samples . get ( 'a' ) !== 2 ) throw new Error ( `task filter: got ${ mixed . samples . get ( 'a' ) } ` ) ;
206- if ( mixed . samples . size !== 1 ) throw new Error ( 'task filter: a non-test task was sampled' ) ;
273+ check ( ( ) => {
274+ if ( mixed . samples . get ( 'a' ) !== 2 ) throw new Error ( `task filter: got ${ mixed . samples . get ( 'a' ) } ` ) ;
275+ } ) ;
276+ check ( ( ) => {
277+ if ( mixed . samples . size !== 1 ) throw new Error ( 'task filter: a non-test task was sampled' ) ;
278+ } ) ;
207279
208280 // THE ONE THAT MATTERS: a cache HIT is skipped, never recorded as ~0s.
209281 // The control leg first -- a genuine 40ms MISS is a legitimate measurement,
210282 // so only the HIT/MISS pair below proves the skip is about the cache status
211283 // and not about the window being short.
212284 const shortMiss = samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 40 ) ] ) , 'f' ) ;
213- if ( shortMiss . samples . get ( 'a' ) !== 0.04 ) throw new Error ( 'cache: a short MISS was not recorded' ) ;
285+ check ( ( ) => {
286+ if ( shortMiss . samples . get ( 'a' ) !== 0.04 ) throw new Error ( 'cache: a short MISS was not recorded' ) ;
287+ } ) ;
214288 const withHit = samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 40 , 'HIT' ) , testTask ( 'b' , 0 , 60_000 ) ] ) , 'f' ) ;
215- if ( withHit . samples . has ( 'a' ) ) throw new Error ( 'cache: a HIT was recorded as a measurement' ) ;
216- if ( ! withHit . skippedCached . includes ( 'a' ) ) throw new Error ( 'cache: a HIT was not reported as skipped' ) ;
217- if ( withHit . samples . get ( 'b' ) !== 60 ) throw new Error ( 'cache: the MISS beside it was lost' ) ;
289+ check ( ( ) => {
290+ if ( withHit . samples . has ( 'a' ) ) throw new Error ( 'cache: a HIT was recorded as a measurement' ) ;
291+ } ) ;
292+ check ( ( ) => {
293+ if ( ! withHit . skippedCached . includes ( 'a' ) ) throw new Error ( 'cache: a HIT was not reported as skipped' ) ;
294+ } ) ;
295+ check ( ( ) => {
296+ if ( withHit . samples . get ( 'b' ) !== 60 ) throw new Error ( 'cache: the MISS beside it was lost' ) ;
297+ } ) ;
218298
219299 // A failed suite stopped early; its window is not the package's cost.
220300 const failed = samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 500 , 'MISS' , 1 ) ] ) , 'f' ) ;
221- if ( failed . samples . has ( 'a' ) ) throw new Error ( 'exit: a failed suite was recorded as a duration' ) ;
301+ check ( ( ) => {
302+ if ( failed . samples . has ( 'a' ) ) throw new Error ( 'exit: a failed suite was recorded as a duration' ) ;
303+ } ) ;
222304
223305 const threw = ( fn ) => {
224306 try {
@@ -228,13 +310,19 @@ function selfTest() {
228310 return true ;
229311 }
230312 } ;
231- if ( ! threw ( ( ) => samplesFromSummary ( { } , 'f' ) ) ) throw new Error ( 'shape: a non-summary was accepted' ) ;
232- if ( ! threw ( ( ) => samplesFromSummary ( summary ( [ { task : 'test' , package : '' , cache : { status : 'MISS' } } ] ) , 'f' ) ) ) {
233- throw new Error ( 'shape: a nameless test task was accepted' ) ;
234- }
235- if ( ! threw ( ( ) => samplesFromSummary ( summary ( [ { task : 'test' , package : 'a' , cache : { status : 'MISS' } } ] ) , 'f' ) ) ) {
236- throw new Error ( 'shape: a test task with no execution window was accepted' ) ;
237- }
313+ check ( ( ) => {
314+ if ( ! threw ( ( ) => samplesFromSummary ( { } , 'f' ) ) ) throw new Error ( 'shape: a non-summary was accepted' ) ;
315+ } ) ;
316+ check ( ( ) => {
317+ if ( ! threw ( ( ) => samplesFromSummary ( summary ( [ { task : 'test' , package : '' , cache : { status : 'MISS' } } ] ) , 'f' ) ) ) {
318+ throw new Error ( 'shape: a nameless test task was accepted' ) ;
319+ }
320+ } ) ;
321+ check ( ( ) => {
322+ if ( ! threw ( ( ) => samplesFromSummary ( summary ( [ { task : 'test' , package : 'a' , cache : { status : 'MISS' } } ] ) , 'f' ) ) ) {
323+ throw new Error ( 'shape: a test task with no execution window was accepted' ) ;
324+ }
325+ } ) ;
238326
239327 // Merging: the same package sampled by several shards collapses to its median.
240328 const merged = buildDataset ( {
@@ -246,43 +334,108 @@ function selfTest() {
246334 fileCounts : new Map ( [ [ 'a' , 10 ] , [ 'big' , 50 ] ] ) ,
247335 provenance : { measuredAt : 'test' } ,
248336 } ) ;
249- if ( merged . packages . a !== 20 ) throw new Error ( `merge: expected the median 20, got ${ merged . packages . a } ` ) ;
337+ check ( ( ) => {
338+ if ( merged . packages . a !== 20 ) throw new Error ( `merge: expected the median 20, got ${ merged . packages . a } ` ) ;
339+ } ) ;
250340 // rates: a -> 20/10 = 2, big -> 100/50 = 2 => 2
251- if ( merged . secondsPerTestFileFallback !== 2 ) {
252- throw new Error ( `fallback rate: got ${ merged . secondsPerTestFileFallback } ` ) ;
253- }
341+ check ( ( ) => {
342+ if ( merged . secondsPerTestFileFallback !== 2 ) {
343+ throw new Error ( `fallback rate: got ${ merged . secondsPerTestFileFallback } ` ) ;
344+ }
345+ } ) ;
254346 // Packages too small to vote on the rate are excluded from it but still kept.
255347 const tiny = buildDataset ( {
256348 perSummary : [ samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 10_000 ) , testTask ( 't' , 0 , 300 ) ] ) , 'f' ) ] ,
257349 fileCounts : new Map ( [ [ 'a' , 5 ] , [ 't' , 1 ] ] ) ,
258350 provenance : { } ,
259351 } ) ;
260- if ( tiny . secondsPerTestFileFallback !== 2 ) throw new Error ( `rate: a 0.3s/1-file package voted (${ tiny . secondsPerTestFileFallback } )` ) ;
261- if ( tiny . packages . t !== 0.3 ) throw new Error ( 'rate: the small package was dropped from the dataset' ) ;
352+ check ( ( ) => {
353+ if ( tiny . secondsPerTestFileFallback !== 2 ) throw new Error ( `rate: a 0.3s/1-file package voted (${ tiny . secondsPerTestFileFallback } )` ) ;
354+ } ) ;
355+ check ( ( ) => {
356+ if ( tiny . packages . t !== 0.3 ) throw new Error ( 'rate: the small package was dropped from the dataset' ) ;
357+ } ) ;
262358
263- if ( ! threw ( ( ) =>
264- buildDataset ( {
265- perSummary : [ samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 40 , 'HIT' ) ] ) , 'f' ) ] ,
266- fileCounts : new Map ( ) ,
267- provenance : { } ,
268- } )
269- ) ) {
270- throw new Error ( 'an all-cached run produced a dataset instead of refusing' ) ;
271- }
359+ check ( ( ) => {
360+ if ( ! threw ( ( ) =>
361+ buildDataset ( {
362+ perSummary : [ samplesFromSummary ( summary ( [ testTask ( 'a' , 0 , 40 , 'HIT' ) ] ) , 'f' ) ] ,
363+ fileCounts : new Map ( ) ,
364+ provenance : { } ,
365+ } )
366+ ) ) {
367+ throw new Error ( 'an all-cached run produced a dataset instead of refusing' ) ;
368+ }
369+ } ) ;
272370
273371 // Workspace resolution, at the depth that actually caught a defect. A
274372 // one-level scan resolves `packages/*` and returns null for the ~60% of the
275373 // workspace that lives under `packages/drivers/*`, `packages/services/*` and
276374 // seven more roots -- silently, as "this package has no test files", which
277375 // then skews the fallback rate toward whichever half sits at depth 1.
278376 const nested = packageDirForName ( '@objectstack/driver-turso' ) ;
279- if ( nested === null ) throw new Error ( 'workspace: a package nested under packages/drivers/ did not resolve' ) ;
280- if ( path . relative ( REPO_ROOT , nested ) . split ( path . sep ) . length < 3 ) {
281- throw new Error ( `workspace: expected a nested path, got ${ nested } ` ) ;
282- }
283- if ( countTestFiles ( nested ) === 0 ) throw new Error ( 'workspace: the resolved nested package reports no test files' ) ;
377+ check ( ( ) => {
378+ if ( nested === null ) throw new Error ( 'workspace: a package nested under packages/drivers/ did not resolve' ) ;
379+ } ) ;
380+ check ( ( ) => {
381+ if ( path . relative ( REPO_ROOT , nested ) . split ( path . sep ) . length < 3 ) {
382+ throw new Error ( `workspace: expected a nested path, got ${ nested } ` ) ;
383+ }
384+ } ) ;
385+ check ( ( ) => {
386+ if ( countTestFiles ( nested ) === 0 ) throw new Error ( 'workspace: the resolved nested package reports no test files' ) ;
387+ } ) ;
284388 const flat = packageDirForName ( '@objectstack/spec' ) ;
285- if ( flat === null || path . basename ( flat ) !== 'spec' ) throw new Error ( 'workspace: a depth-1 package stopped resolving' ) ;
389+ check ( ( ) => {
390+ if ( flat === null || path . basename ( flat ) !== 'spec' ) throw new Error ( 'workspace: a depth-1 package stopped resolving' ) ;
391+ } ) ;
392+
393+ // -- The floor: every declared battery RAN, and ran its cases (#13489) ----
394+ //
395+ // Evaluated after every battery has had its chance and BEFORE the verdict, so
396+ // the success line below can only be printed by a run in which the set of
397+ // batteries that registered cases EQUALS the set declared. A set difference
398+ // names WHICH battery stopped; a count says only that something did.
399+ //
400+ // It THROWS rather than collecting into a `failures` array because that is
401+ // how every other assertion in this file reports: the dispatch below turns an
402+ // unfinished self-test into a non-zero exit, and a floor breach is exactly
403+ // that -- a self-test that did not run what it claims to run.
404+ const floorFailures = [ ] ;
405+ const declaredBatteries = Object . keys ( SELF_TEST_BATTERIES ) ;
406+ if ( declaredBatteries . length < SELF_TEST_BATTERY_FLOOR ) {
407+ floorFailures . push (
408+ `SELF_TEST_BATTERIES declares ${ declaredBatteries . length } batteries, below the pinned ` +
409+ `${ SELF_TEST_BATTERY_FLOOR } -- a battery deleted from the roster takes its own floor with it.`
410+ ) ;
411+ }
412+ for ( const [ name , count ] of batterySeen ) {
413+ if ( declaredBatteries . includes ( name ) ) continue ;
414+ floorFailures . push (
415+ `self-test battery "${ name } " registered ${ count } case(s) but is not declared in ` +
416+ 'SELF_TEST_BATTERIES -- a case attributed to no declared battery is one nothing floors.'
417+ ) ;
418+ }
419+ for ( const name of declaredBatteries ) {
420+ const count = batterySeen . get ( name ) ?? 0 ;
421+ if ( count >= SELF_TEST_BATTERIES [ name ] ) continue ;
422+ floorFailures . push (
423+ count === 0
424+ ? `self-test battery "${ name } " DID NOT RUN -- 0 cases registered, ${ SELF_TEST_BATTERIES [ name ] } pinned. ` +
425+ 'The verdict below would have claimed those cases hold.'
426+ : `self-test battery "${ name } " registered ${ count } case(s), below its pinned floor of ` +
427+ `${ SELF_TEST_BATTERIES [ name ] } -- cases that used to run no longer do.`
428+ ) ;
429+ }
430+ if ( floorFailures . length > 0 ) {
431+ throw new Error (
432+ `measure-test-shard-timings self-test floor (${ floorFailures . length } breach(es)):\n` +
433+ floorFailures . map ( ( f ) => ` - ${ f } ` ) . join ( '\n' ) +
434+ '\n A battery at or below its floor means cases STOPPED RUNNING -- the battery is the bug, ' +
435+ 'not the number. Find what stopped registering (an early return, a deleted block, a guard ' +
436+ 'that now skips) and restore it.'
437+ ) ;
438+ }
286439
287440 console . log ( 'measure-test-shard-timings: self-test OK' ) ;
288441
0 commit comments