@@ -108,7 +108,25 @@ function isConcurrencyRejection(error: unknown): boolean {
108108/** rows + the actual (clip-aware) time window the query service resolved for this run. */
109109type QueryResult = { rows : Row [ ] ; timeRange : { from : Date ; to : Date } } ;
110110
111- async function runQuery (
111+ /** Runs one (TRQL) report query. Injectable so tests can drive the loader with canned results. */
112+ export type HealthQueryRunner = (
113+ env : AuthenticatedEnvironment ,
114+ query : string ,
115+ period : string
116+ ) => Promise < QueryResult > ;
117+
118+ /**
119+ * The loader's IO boundary (§7 Seam A): ClickHouse via the query service, Redis via the engine.
120+ * Defaults wire the real singletons; `loadHealthInput` accepts an override so its orchestration
121+ * (source selection, snapshot fallback on empty/throw, dlq parse, window math) is testable without
122+ * booting the env-bound query-service client.
123+ */
124+ export type HealthDeps = {
125+ runQuery : HealthQueryRunner ;
126+ lengthOfEnvQueue : ( env : AuthenticatedEnvironment ) => Promise < number | undefined > ;
127+ } ;
128+
129+ async function executeReportQuery (
112130 env : AuthenticatedEnvironment ,
113131 query : string ,
114132 period : string
@@ -226,14 +244,21 @@ ORDER BY fails DESC
226244LIMIT 10` ;
227245}
228246
247+ /** Default IO wiring — the real query-service runner + the engine's env-queue length. */
248+ const defaultHealthDeps : HealthDeps = {
249+ runQuery : executeReportQuery ,
250+ lengthOfEnvQueue : ( env ) => engine . lengthOfEnvQueue ( env ) ,
251+ } ;
252+
229253/** Run a query that may reference not-yet-available columns; never break the report. */
230254async function tryQuery (
255+ deps : HealthDeps ,
231256 env : AuthenticatedEnvironment ,
232257 query : string ,
233258 period : string
234259) : Promise < Row [ ] > {
235260 try {
236- return ( await runQuery ( env , query , period ) ) . rows ;
261+ return ( await deps . runQuery ( env , query , period ) ) . rows ;
237262 } catch {
238263 return [ ] ;
239264 }
@@ -266,7 +291,8 @@ export interface FlowSource {
266291 loadFlow (
267292 env : AuthenticatedEnvironment ,
268293 period : string ,
269- ctx : RunsContext
294+ ctx : RunsContext ,
295+ deps : HealthDeps
270296 ) : Promise < FlowData | null > ;
271297}
272298
@@ -276,20 +302,20 @@ export interface FlowSource {
276302 * caller can fall back to the snapshot.
277303 */
278304export const QueueMetricsSource : FlowSource = {
279- async loadFlow ( env , period ) {
305+ async loadFlow ( env , period , _ctx , deps ) {
280306 try {
281307 // Redis depth is not a ClickHouse query, so it runs alongside (doesn't count toward the cap).
282- const pendingNowPromise = engine . lengthOfEnvQueue ( env ) ;
308+ const pendingNowPromise = deps . lengthOfEnvQueue ( env ) ;
283309
284310 // Bug 1 fix — route all CH queries through the concurrency cap (max 2 in flight) instead
285311 // of firing 4 at once via Promise.all.
286312 const [ series , liveScalar , baselineScalar , worstRows , dlqRows ] = await mapWithConcurrency (
287313 [
288- ( ) => runQuery ( env , envSeriesQuery ( ) , period ) . then ( ( r ) => r . rows ) ,
289- ( ) => runQuery ( env , envScalarQuery ( ) , period ) . then ( ( r ) => r . rows [ 0 ] ?? { } ) ,
290- ( ) => runQuery ( env , envScalarQuery ( ) , BASELINE_PERIOD ) . then ( ( r ) => r . rows [ 0 ] ?? { } ) ,
291- ( ) => tryQuery ( env , queueWorstQuery ( ) , period ) ,
292- ( ) => tryQuery ( env , dlqTotalQuery ( ) , period ) ,
314+ ( ) => deps . runQuery ( env , envSeriesQuery ( ) , period ) . then ( ( r ) => r . rows ) ,
315+ ( ) => deps . runQuery ( env , envScalarQuery ( ) , period ) . then ( ( r ) => r . rows [ 0 ] ?? { } ) ,
316+ ( ) => deps . runQuery ( env , envScalarQuery ( ) , BASELINE_PERIOD ) . then ( ( r ) => r . rows [ 0 ] ?? { } ) ,
317+ ( ) => tryQuery ( deps , env , queueWorstQuery ( ) , period ) ,
318+ ( ) => tryQuery ( deps , env , dlqTotalQuery ( ) , period ) ,
293319 ] ,
294320 CH_CONCURRENCY ,
295321 ( task ) => task ( )
@@ -379,8 +405,8 @@ function buildQueueMetricsFlow(
379405 * series is shape-only (`estimated: true`).
380406 */
381407export const SnapshotFlowSource : FlowSource = {
382- async loadFlow ( env , _period , ctx ) {
383- const pendingNow = ( await engine . lengthOfEnvQueue ( env ) ) ?? 0 ;
408+ async loadFlow ( env , _period , ctx , deps ) {
409+ const pendingNow = ( await deps . lengthOfEnvQueue ( env ) ) ?? 0 ;
384410
385411 let backlog = 0 ;
386412 const proxy = ctx . liveSeries . map ( ( r ) => {
@@ -415,15 +441,16 @@ export const SnapshotFlowSource: FlowSource = {
415441export async function loadHealthInput (
416442 env : AuthenticatedEnvironment ,
417443 period : string ,
418- now : Date = new Date ( )
444+ now : Date = new Date ( ) ,
445+ deps : HealthDeps = defaultHealthDeps
419446) : Promise < HealthInput > {
420447 // Bug 1 fix — route the runs-phase CH queries through the concurrency cap (max 2 in flight)
421448 // instead of firing all 3 at once, so we never exceed the query service's per-project limit.
422449 const [ liveScalarRes , liveSeriesRes , baselineScalarRes ] = await mapWithConcurrency (
423450 [
424- ( ) => runQuery ( env , runsScalarQuery ( ) , period ) ,
425- ( ) => runQuery ( env , runsSeriesQuery ( ) , period ) ,
426- ( ) => runQuery ( env , runsScalarQuery ( ) , BASELINE_PERIOD ) ,
451+ ( ) => deps . runQuery ( env , runsScalarQuery ( ) , period ) ,
452+ ( ) => deps . runQuery ( env , runsSeriesQuery ( ) , period ) ,
453+ ( ) => deps . runQuery ( env , runsScalarQuery ( ) , BASELINE_PERIOD ) ,
427454 ] ,
428455 CH_CONCURRENCY ,
429456 ( task ) => task ( )
@@ -444,8 +471,8 @@ export async function loadHealthInput(
444471
445472 // Prefer measured queue metrics; fall back to the runs snapshot when unavailable.
446473 const flow =
447- ( await QueueMetricsSource . loadFlow ( env , period , ctx ) ) ??
448- ( await SnapshotFlowSource . loadFlow ( env , period , ctx ) ) ! ;
474+ ( await QueueMetricsSource . loadFlow ( env , period , ctx , deps ) ) ??
475+ ( await SnapshotFlowSource . loadFlow ( env , period , ctx , deps ) ) ! ;
449476
450477 const failuresSeries = resampleSeries (
451478 ctx . liveSeries . map ( ( r ) => failureRate ( num ( r . failures ) , num ( r . completed ) ) )
@@ -470,7 +497,7 @@ export async function loadHealthInput(
470497 normalRate > 0 &&
471498 rate / normalRate >= HEALTH_THRESHOLDS . failures . warnMult ;
472499 const failureBreakdown = failureDegraded
473- ? await loadFailureBreakdown ( env , period , num ( ctx . liveScalar . failures ) )
500+ ? await loadFailureBreakdown ( deps , env , period , num ( ctx . liveScalar . failures ) )
474501 : undefined ;
475502
476503 const lastCompletion = parseTimestamp ( ctx . liveScalar . last_completion ) ;
@@ -498,12 +525,13 @@ export async function loadHealthInput(
498525}
499526
500527async function loadFailureBreakdown (
528+ deps : HealthDeps ,
501529 env : AuthenticatedEnvironment ,
502530 period : string ,
503531 totalFails : number
504532) : Promise < HealthInput [ "failureBreakdown" ] > {
505533 if ( totalFails <= 0 ) return undefined ;
506- const rows = await tryQuery ( env , failureBreakdownQuery ( ) , period ) ;
534+ const rows = await tryQuery ( deps , env , failureBreakdownQuery ( ) , period ) ;
507535 if ( rows . length === 0 ) return undefined ;
508536 const top = rows [ 0 ] ;
509537 return { task : String ( top . task ?? "unknown" ) , share : num ( top . fails ) / totalFails } ;
0 commit comments