Skip to content

Commit 18b8b6f

Browse files
committed
test(rest): the read-gate fault seam declares its log level and asserts where the cause lands
Claude-Session: https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea8c425 commit 18b8b6f

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

‎packages/rest/src/analytics-read-gate-fault-envelope.test.ts‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
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';
3333
import type { Logger } from '@objectstack/spec/contracts';
3434
import { INTERNAL_ERROR_MESSAGE } from '@objectstack/types';
3535
import { 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

7485
type 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

102113
async 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

116131
const 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

Comments
 (0)