11import { describe , it , expect , vi } from 'vitest' ;
2- import { HttpDispatcher } from './http-dispatcher.js' ;
2+ import { HttpDispatcher , type HttpDispatcherResult } from './http-dispatcher.js' ;
33
44function kernel ( state : string , dataService ?: unknown ) : any {
55 return {
@@ -10,6 +10,34 @@ function kernel(state: string, dataService?: unknown): any {
1010}
1111const ctx : any = { } ;
1212
13+ /**
14+ * `HttpDispatcherResult.response` is OPTIONAL, so every read of it is a
15+ * `possibly undefined` in a type-checked program — and this package's test
16+ * layer IS type-checked, by `check:type-check-debt --re-measure` against a
17+ * shrink-only ledger, even though `pnpm test` never sees it. That asymmetry is
18+ * exactly how 19 fresh errors got in here behind a green `pnpm --filter
19+ * @objectstack /runtime typecheck`: the package's own tsconfig excludes every
20+ * `.test.ts` file, so the program that reported zero had never read this one.
21+ *
22+ * Narrow once, and narrow LOUDLY — the shape is lifted from the #8287 suite in
23+ * `http-dispatcher.keys.test.ts`, deliberately rather than invented again.
24+ * `expect(res.response).toBeDefined()` would satisfy a reader and narrow
25+ * nothing (vitest's matchers are not assertion signatures), and a `!` would
26+ * silence the compiler while leaving the failure to surface as `undefined is
27+ * not an object` three lines later. A probe that answered no response at all is
28+ * a different defect from one that answered the wrong status; this keeps them
29+ * distinguishable.
30+ *
31+ * Applied to the WHOLE file, not just the #13408 suite: the reads are identical
32+ * in kind, the repair is one call each, and leaving the older ones would bank a
33+ * green while knowingly holding fixable errors in a file already open.
34+ */
35+ function responseOf ( res : HttpDispatcherResult ) : NonNullable < HttpDispatcherResult [ 'response' ] > {
36+ const { response } = res ;
37+ if ( ! response ) throw new Error ( 'GET /ready answered no response at all' ) ;
38+ return response ;
39+ }
40+
1341/** An engine whose `checkDriversHealth` reports the given verdicts. */
1442function engine ( results : Array < { driverName : string ; healthy : boolean } > ) {
1543 return { checkDriversHealth : vi . fn ( async ( ) => results ) } ;
@@ -38,14 +66,14 @@ describe('HttpDispatcher — GET /ready readiness probe', () => {
3866 it ( 'returns 200 when the kernel is running' , async ( ) => {
3967 const res = await new HttpDispatcher ( kernel ( 'running' ) ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
4068 expect ( res . handled ) . toBe ( true ) ;
41- expect ( res . response . status ) . toBe ( 200 ) ;
42- expect ( res . response . body . data . state ) . toBe ( 'running' ) ;
69+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
70+ expect ( responseOf ( res ) . body . data . state ) . toBe ( 'running' ) ;
4371 } ) ;
4472
4573 it ( 'returns 503 while booting or shutting down' , async ( ) => {
4674 for ( const state of [ 'idle' , 'initializing' , 'stopping' , 'stopped' ] ) {
4775 const res = await new HttpDispatcher ( kernel ( state ) ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
48- expect ( res . response . status ) . toBe ( 503 ) ;
76+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
4977 }
5078 } ) ;
5179
@@ -57,7 +85,7 @@ describe('HttpDispatcher — GET /ready readiness probe', () => {
5785 kernel ( 'running' , engine ( [ { driverName : 'sql' , healthy : true } ] ) ) ,
5886 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
5987
60- expect ( res . response . status ) . toBe ( 200 ) ;
88+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
6189 } ) ;
6290
6391 it ( 'returns 503 naming the driver when one is down, even though the kernel runs' , async ( ) => {
@@ -68,9 +96,9 @@ describe('HttpDispatcher — GET /ready readiness probe', () => {
6896 ] ) ) ,
6997 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
7098
71- expect ( res . response . status ) . toBe ( 503 ) ;
72- expect ( res . response . body . error . message ) . toBe ( 'Data driver unavailable' ) ;
73- expect ( res . response . body . error . details ) . toEqual ( { state : 'running' , drivers : [ 'sql' ] } ) ;
99+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
100+ expect ( responseOf ( res ) . body . error . message ) . toBe ( 'Data driver unavailable' ) ;
101+ expect ( responseOf ( res ) . body . error . details ) . toEqual ( { state : 'running' , drivers : [ 'sql' ] } ) ;
74102 } ) ;
75103
76104 it ( 'does not re-probe within the memo TTL — k8s polls every few seconds' , async ( ) => {
@@ -91,14 +119,14 @@ describe('HttpDispatcher — GET /ready readiness probe', () => {
91119 } ) ,
92120 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
93121
94- expect ( res . response . status ) . toBe ( 200 ) ;
122+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
95123 } ) ;
96124
97125 it ( 'stays ready on an engine predating checkDriversHealth' , async ( ) => {
98126 const res = await new HttpDispatcher ( kernel ( 'running' , { find : async ( ) => [ ] } ) )
99127 . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
100128
101- expect ( res . response . status ) . toBe ( 200 ) ;
129+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
102130 } ) ;
103131 } ) ;
104132} ) ;
@@ -122,21 +150,21 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
122150 kernel ( 'running' , engineWithPrimary ( [ { driverName : 'pg' , healthy : false } ] , primary ( 'pg' ) ) ) ,
123151 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
124152
125- expect ( res . response . status ) . toBe ( 503 ) ;
126- expect ( res . response . body . error . message ) . toBe ( 'Data driver unavailable' ) ;
153+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
154+ expect ( responseOf ( res ) . body . error . message ) . toBe ( 'Data driver unavailable' ) ;
127155 // Byte-identical to the shape #3756 shipped — an operator's alerting on
128156 // this body must not be able to tell that the handler changed.
129- expect ( res . response . body . error . details ) . toEqual ( { state : 'running' , drivers : [ 'pg' ] } ) ;
157+ expect ( responseOf ( res ) . body . error . details ) . toEqual ( { state : 'running' , drivers : [ 'pg' ] } ) ;
130158 } ) ;
131159
132160 it ( 'the all-healthy 200 body carries NO degraded key' , async ( ) => {
133161 const res = await new HttpDispatcher (
134162 kernel ( 'running' , engineWithPrimary ( [ { driverName : 'pg' , healthy : true } ] , primary ( 'pg' ) ) ) ,
135163 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
136164
137- expect ( res . response . status ) . toBe ( 200 ) ;
138- expect ( res . response . body . data ) . toEqual ( { status : 'ready' , state : 'running' } ) ;
139- expect ( res . response . body . data ) . not . toHaveProperty ( 'degraded' ) ;
165+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
166+ expect ( responseOf ( res ) . body . data ) . toEqual ( { status : 'ready' , state : 'running' } ) ;
167+ expect ( responseOf ( res ) . body . data ) . not . toHaveProperty ( 'degraded' ) ;
140168 } ) ;
141169
142170 it ( 'does not even ASK which datasource is primary while everything is healthy' , ( ) => {
@@ -164,11 +192,11 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
164192 ) ) ,
165193 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
166194
167- expect ( res . response . status ) . toBe ( 200 ) ;
168- expect ( res . response . body . data . status ) . toBe ( 'ready' ) ;
195+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
196+ expect ( responseOf ( res ) . body . data . status ) . toBe ( 'ready' ) ;
169197 // ⛔ The rejected fourth option — filtering the bad driver out so it
170198 // becomes invisible — would show an EMPTY degraded list here.
171- expect ( res . response . body . data . degraded ) . toEqual ( {
199+ expect ( responseOf ( res ) . body . data . degraded ) . toEqual ( {
172200 drivers : [ 'tenant_mongo' ] ,
173201 primaryDatasource : 'pg' ,
174202 } ) ;
@@ -182,8 +210,8 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
182210 ) ) ,
183211 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
184212
185- expect ( res . response . status ) . toBe ( 503 ) ;
186- expect ( res . response . body . error . details . drivers ) . toEqual ( [ 'pg' ] ) ;
213+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
214+ expect ( responseOf ( res ) . body . error . details . drivers ) . toEqual ( [ 'pg' ] ) ;
187215 } ) ;
188216
189217 it ( 'drains when BOTH are down — the primary is in the unhealthy set' , async ( ) => {
@@ -194,8 +222,8 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
194222 ) ) ,
195223 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
196224
197- expect ( res . response . status ) . toBe ( 503 ) ;
198- expect ( res . response . body . error . details . drivers ) . toEqual ( [ 'pg' , 'tenant_mongo' ] ) ;
225+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
226+ expect ( responseOf ( res ) . body . error . details . drivers ) . toEqual ( [ 'pg' , 'tenant_mongo' ] ) ;
199227 } ) ;
200228 } ) ;
201229
@@ -215,8 +243,8 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
215243 const res = await new HttpDispatcher ( kernel ( 'running' , engine ( secondaryDown ) ) )
216244 . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
217245
218- expect ( res . response . status ) . toBe ( 503 ) ;
219- expect ( res . response . body . error . details ) . toEqual ( {
246+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
247+ expect ( responseOf ( res ) . body . error . details ) . toEqual ( {
220248 state : 'running' ,
221249 drivers : [ 'tenant_mongo' ] ,
222250 } ) ;
@@ -228,7 +256,7 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
228256 resolvePrimaryDatasource : ( ) => { throw new Error ( 'registry exploded' ) ; } ,
229257 } ) ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
230258
231- expect ( res . response . status ) . toBe ( 503 ) ;
259+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
232260 } ) ;
233261
234262 it . each ( [
@@ -241,7 +269,7 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
241269 kernel ( 'running' , engineWithPrimary ( secondaryDown , verdict ) ) ,
242270 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
243271
244- expect ( res . response . status ) . toBe ( 503 ) ;
272+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
245273 } ) ;
246274
247275 it . each ( [
@@ -255,7 +283,7 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
255283 kernel ( 'running' , engineWithPrimary ( secondaryDown , verdict ) ) ,
256284 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
257285
258- expect ( res . response . status ) . toBe ( 503 ) ;
286+ expect ( responseOf ( res ) . status ) . toBe ( 503 ) ;
259287 } ) ;
260288
261289 it ( 'NON-VACUITY: the same fixture returns 200 the moment the criterion resolves' , async ( ) => {
@@ -265,7 +293,7 @@ describe('HttpDispatcher — GET /ready primary-vs-secondary drain (#13408)', ()
265293 kernel ( 'running' , engineWithPrimary ( secondaryDown , primary ( 'pg' ) ) ) ,
266294 ) . dispatch ( 'GET' , '/ready' , undefined , undefined , ctx ) ;
267295
268- expect ( res . response . status ) . toBe ( 200 ) ;
296+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
269297 } ) ;
270298 } ) ;
271299
@@ -294,8 +322,8 @@ describe('HttpDispatcher — GET /health liveness probe', () => {
294322 const res = await new HttpDispatcher ( kernel ( 'running' , e ) )
295323 . dispatch ( 'GET' , '/health' , undefined , undefined , ctx ) ;
296324
297- expect ( res . response . status ) . toBe ( 200 ) ;
298- expect ( res . response . body . data . status ) . toBe ( 'ok' ) ;
325+ expect ( responseOf ( res ) . status ) . toBe ( 200 ) ;
326+ expect ( responseOf ( res ) . body . data . status ) . toBe ( 'ok' ) ;
299327 expect ( e . checkDriversHealth ) . not . toHaveBeenCalled ( ) ;
300328 } ) ;
301329} ) ;
0 commit comments