2929 * red on the twin.
3030 */
3131
32- import { describe , it , expect , vi } from 'vitest' ;
32+ import { describe , it , expect , vi , beforeAll , afterAll } from 'vitest' ;
3333import type { Logger } from '@objectstack/spec/contracts' ;
3434import { INTERNAL_ERROR_MESSAGE } from '@objectstack/types' ;
3535import { AnalyticsService , type AnalyticsServiceConfig } from '@objectstack/service-analytics' ;
@@ -69,14 +69,25 @@ function buildRoute(analyticsProvider: () => Promise<AnalyticsService>) {
6969 return rest . getRoutes ( ) . find ( ( r ) => r . method === 'POST' && r . path . endsWith ( '/analytics/dataset/query' ) ) ! ;
7070}
7171
72- const silent : Logger = { debug ( ) { } , info ( ) { } , warn ( ) { } , error ( ) { } } ;
72+ // The route's fault line is asserted below, so the level this file reads is the loud one.
73+ beforeAll ( ( ) => { vi . stubEnv ( 'OS_REST_LOG' , 'info' ) ; } ) ;
74+ afterAll ( ( ) => { vi . unstubAllEnvs ( ) ; } ) ;
75+
76+ /** The service's log sink, recording every line it is handed — where the cause must land. */
77+ function recordingLogger ( ) : { logger : Logger ; lines : string [ ] } {
78+ const lines : string [ ] = [ ] ;
79+ const record = ( message : string , err ?: unknown ) => {
80+ lines . push ( `${ message } ${ err instanceof Error ? err . message : '' } ` ) ;
81+ } ;
82+ return { lines, logger : { debug ( ) { } , info ( ) { } , warn : record , error : record } as Logger } ;
83+ }
7384
7485type Gates = Pick < AnalyticsServiceConfig , 'admitObjectRead' | 'getReadableFields' | 'getObjectDeclaration' > ;
7586
7687/** A real `AnalyticsService` on the ObjectQL aggregate path, with the gates under test. */
77- function analytics ( gates : Gates , executed : string [ ] ) : AnalyticsService {
88+ function analytics ( gates : Gates , executed : string [ ] , logger : Logger ) : AnalyticsService {
7889 return new AnalyticsService ( {
79- logger : silent ,
90+ logger,
8091 queryCapabilities : ( ) => ( { nativeSql : false , objectqlAggregate : true , inMemory : false } ) ,
8192 executeAggregate : async ( object : string ) => {
8293 executed . push ( object ) ;
@@ -101,16 +112,20 @@ const body = {
101112
102113async function post ( gates : Gates ) {
103114 const executed : string [ ] = [ ] ;
104- const service = analytics ( gates , executed ) ;
115+ const { logger, lines } = recordingLogger ( ) ;
116+ const service = analytics ( gates , executed , logger ) ;
105117 const route = buildRoute ( async ( ) => service ) ;
106118 const res = mockRes ( ) ;
107119 const logSpy = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
120+ let routeLog : string ;
108121 try {
109122 await route . handler ( { method : 'POST' , params : { } , headers : { } , body } as any , res ) ;
123+ // Read the calls BEFORE restoring: `mockRestore` clears them.
124+ routeLog = logSpy . mock . calls . map ( ( args ) => args . map ( String ) . join ( ' ' ) ) . join ( '\n' ) ;
110125 } finally {
111126 logSpy . mockRestore ( ) ;
112127 }
113- return { status : res . statusCode as number , body : res . body , executed } ;
128+ return { status : res . statusCode as number , body : res . body , executed, serviceLog : lines . join ( '\n' ) , routeLog } ;
114129}
115130
116131const exploded = ( ) => {
@@ -164,6 +179,9 @@ describe('[#25561] a read gate that could not decide answers 503 SERVICE_UNAVAIL
164179 expect ( res . body . error ) . toBe ( INTERNAL_ERROR_MESSAGE ) ;
165180 expect ( JSON . stringify ( res . body ) ) . not . toContain ( 'exploded' ) ;
166181 expect ( res . executed ) . toEqual ( [ ] ) ;
182+ // …and the operator keeps it: the cause at the site, the fault at the route.
183+ expect ( res . serviceLog ) . toContain ( 'exploded' ) ;
184+ expect ( res . routeLog ) . toContain ( 'Analytics dataset query error' ) ;
167185 } ) ;
168186
169187 it ( `its verdict twin — the same gate answering "no" — still answers ${ verdictStatus } ${ verdictCode } ` , async ( ) => {
0 commit comments