@terreno/api 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  5. package/dist/actions.openApi.test.js +13 -11
  6. package/dist/api.js +98 -11
  7. package/dist/api.query.test.js +31 -1
  8. package/dist/api.test.js +211 -0
  9. package/dist/auth.test.js +10 -10
  10. package/dist/betterAuth.d.ts +1 -1
  11. package/dist/consentApp.test.js +1 -0
  12. package/dist/example.js +4 -4
  13. package/dist/expressServer.d.ts +0 -22
  14. package/dist/expressServer.js +1 -125
  15. package/dist/expressServer.test.js +90 -91
  16. package/dist/githubAuth.test.js +22 -22
  17. package/dist/logger.d.ts +154 -0
  18. package/dist/logger.js +445 -26
  19. package/dist/logger.test.js +435 -0
  20. package/dist/middleware.d.ts +7 -0
  21. package/dist/middleware.js +58 -1
  22. package/dist/middleware.test.js +159 -0
  23. package/dist/openApi.test.js +10 -17
  24. package/dist/openApiBuilder.test.js +18 -10
  25. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  26. package/dist/realtime/changeStreamWatcher.js +2 -4
  27. package/dist/realtime/queryMatcher.d.ts +1 -1
  28. package/dist/realtime/queryMatcher.js +39 -14
  29. package/dist/realtime/types.d.ts +3 -3
  30. package/dist/requestContext.d.ts +61 -0
  31. package/dist/requestContext.js +74 -0
  32. package/dist/secretProviders.test.js +335 -0
  33. package/dist/terrenoApp.d.ts +27 -15
  34. package/dist/terrenoApp.js +24 -14
  35. package/dist/terrenoApp.test.js +52 -0
  36. package/dist/tests/bunSetup.js +61 -7
  37. package/dist/tests.js +27 -4
  38. package/package.json +1 -1
  39. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  40. package/src/actions.openApi.test.ts +12 -10
  41. package/src/api.query.test.ts +24 -1
  42. package/src/api.test.ts +169 -0
  43. package/src/api.ts +71 -0
  44. package/src/auth.test.ts +10 -10
  45. package/src/betterAuth.ts +1 -1
  46. package/src/consentApp.test.ts +1 -0
  47. package/src/example.ts +4 -4
  48. package/src/expressServer.test.ts +82 -85
  49. package/src/expressServer.ts +1 -213
  50. package/src/githubAuth.test.ts +22 -22
  51. package/src/logger.test.ts +466 -1
  52. package/src/logger.ts +477 -14
  53. package/src/middleware.test.ts +74 -2
  54. package/src/middleware.ts +57 -0
  55. package/src/openApi.test.ts +10 -17
  56. package/src/openApiBuilder.test.ts +18 -10
  57. package/src/realtime/changeStreamWatcher.ts +15 -10
  58. package/src/realtime/queryMatcher.ts +54 -27
  59. package/src/realtime/types.ts +4 -4
  60. package/src/requestContext.ts +86 -0
  61. package/src/secretProviders.test.ts +219 -1
  62. package/src/terrenoApp.test.ts +38 -0
  63. package/src/terrenoApp.ts +37 -15
  64. package/src/tests/bunSetup.ts +16 -3
  65. package/src/tests.ts +17 -4
@@ -8,10 +8,10 @@ import supertest from "supertest";
8
8
  import type TestAgent from "supertest/lib/agent";
9
9
 
10
10
  import {generateTokens, type UserModel} from "./auth";
11
- import {setupServer} from "./expressServer";
12
11
  import {type GitHubUserFields, githubUserPlugin, setupGitHubAuth} from "./githubAuth";
13
12
  import {logger} from "./logger";
14
13
  import {createdUpdatedPlugin, isDisabledPlugin} from "./plugins";
14
+ import {TerrenoApp} from "./terrenoApp";
15
15
 
16
16
  interface FakeStrategyOutcome {
17
17
  type: "success" | "redirect" | "fail";
@@ -142,8 +142,8 @@ describe("GitHub auth routes", () => {
142
142
  router.get("/test", (_req, res) => res.json({ok: true}));
143
143
  }
144
144
 
145
- app = setupServer({
146
- addRoutes,
145
+ app = new TerrenoApp({
146
+ configureApp: addRoutes,
147
147
  githubAuth: {
148
148
  allowAccountLinking: true,
149
149
  callbackURL: "http://localhost:9000/auth/github/callback",
@@ -152,7 +152,7 @@ describe("GitHub auth routes", () => {
152
152
  },
153
153
  skipListen: true,
154
154
  userModel: GitHubTestUserModel as any,
155
- });
155
+ }).build();
156
156
  agent = supertest.agent(app);
157
157
  });
158
158
 
@@ -249,11 +249,11 @@ describe("GitHub auth disabled", () => {
249
249
  }
250
250
 
251
251
  // Setup server WITHOUT GitHub auth
252
- app = setupServer({
253
- addRoutes,
252
+ app = new TerrenoApp({
253
+ configureApp: addRoutes,
254
254
  skipListen: true,
255
255
  userModel: GitHubTestUserModel as any,
256
- });
256
+ }).build();
257
257
  agent = supertest.agent(app);
258
258
  });
259
259
 
@@ -517,8 +517,8 @@ describe("addGitHubAuthRoutes link endpoints", () => {
517
517
  router.get("/test", (_req, res) => res.json({ok: true}));
518
518
  }
519
519
 
520
- app = setupServer({
521
- addRoutes,
520
+ app = new TerrenoApp({
521
+ configureApp: addRoutes,
522
522
  githubAuth: {
523
523
  allowAccountLinking: true,
524
524
  callbackURL: "http://localhost:9000/auth/github/callback",
@@ -527,7 +527,7 @@ describe("addGitHubAuthRoutes link endpoints", () => {
527
527
  },
528
528
  skipListen: true,
529
529
  userModel: GitHubTestUserModel as any,
530
- });
530
+ }).build();
531
531
  agent = supertest.agent(app);
532
532
  });
533
533
 
@@ -586,10 +586,10 @@ describe("GitHub callback handler (fake strategy)", () => {
586
586
  router.get("/test", (_req, res) => res.json({ok: true}));
587
587
  }
588
588
 
589
- app = setupServer({
590
- addMiddleware: (a) => {
589
+ app = new TerrenoApp({
590
+ beforeJsonSetup: (a) => {
591
591
  // The handler reads (req as unknown as {session?: {returnTo?: string}}).session?.returnTo.
592
- // setupServer does not install express-session, so prime a fake session from a request
592
+ // TerrenoApp does not install express-session, so prime a fake session from a request
593
593
  // header for tests.
594
594
  a.use((req, _res, next) => {
595
595
  const headerReturnTo = req.headers["x-mock-return-to"];
@@ -599,7 +599,7 @@ describe("GitHub callback handler (fake strategy)", () => {
599
599
  next();
600
600
  });
601
601
  },
602
- addRoutes,
602
+ configureApp: addRoutes,
603
603
  githubAuth: {
604
604
  allowAccountLinking: true,
605
605
  callbackURL: "http://localhost:9000/auth/github/callback",
@@ -608,8 +608,8 @@ describe("GitHub callback handler (fake strategy)", () => {
608
608
  },
609
609
  skipListen: true,
610
610
  userModel: GitHubTestUserModel as unknown as UserModel,
611
- });
612
- // Swap the github strategy with our fake after setupServer registered it.
611
+ }).build();
612
+ // Swap the github strategy with our fake after TerrenoApp registered it.
613
613
  installFakeGithubStrategy();
614
614
  agent = supertest.agent(app);
615
615
  });
@@ -691,8 +691,8 @@ describe("GET /auth/github/link with JWT (fake strategy)", () => {
691
691
  router.get("/test", (_req, res) => res.json({ok: true}));
692
692
  }
693
693
 
694
- app = setupServer({
695
- addRoutes,
694
+ app = new TerrenoApp({
695
+ configureApp: addRoutes,
696
696
  githubAuth: {
697
697
  allowAccountLinking: true,
698
698
  callbackURL: "http://localhost:9000/auth/github/callback",
@@ -701,7 +701,7 @@ describe("GET /auth/github/link with JWT (fake strategy)", () => {
701
701
  },
702
702
  skipListen: true,
703
703
  userModel: GitHubTestUserModel as unknown as UserModel,
704
- });
704
+ }).build();
705
705
  installFakeGithubStrategy();
706
706
  agent = supertest.agent(app);
707
707
  });
@@ -746,8 +746,8 @@ describe("DELETE /auth/github/unlink edge cases", () => {
746
746
  router.get("/test", (_req, res) => res.json({ok: true}));
747
747
  }
748
748
 
749
- app = setupServer({
750
- addRoutes,
749
+ app = new TerrenoApp({
750
+ configureApp: addRoutes,
751
751
  githubAuth: {
752
752
  allowAccountLinking: true,
753
753
  callbackURL: "http://localhost:9000/auth/github/callback",
@@ -756,7 +756,7 @@ describe("DELETE /auth/github/unlink edge cases", () => {
756
756
  },
757
757
  skipListen: true,
758
758
  userModel: GitHubTestUserModel as unknown as UserModel,
759
- });
759
+ }).build();
760
760
  installFakeGithubStrategy();
761
761
  agent = supertest.agent(app);
762
762
  });
@@ -2,9 +2,19 @@ import {afterEach, beforeEach, describe, expect, it} from "bun:test";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import {Writable} from "node:stream";
5
6
  import winston from "winston";
6
7
 
7
- import {logger, setupLogging} from "./logger";
8
+ import {
9
+ createFeatureFlaggedLogger,
10
+ createScopedLogger,
11
+ formatLogContextSuffix,
12
+ logger,
13
+ setupLogging,
14
+ type TerrenoRequestLogEntry,
15
+ winstonLogger,
16
+ } from "./logger";
17
+ import {runWithRequestContext} from "./requestContext";
8
18
 
9
19
  describe("logger", () => {
10
20
  const OLD_ENV = process.env;
@@ -47,6 +57,420 @@ describe("logger", () => {
47
57
  });
48
58
  });
49
59
 
60
+ describe("formatLogContextSuffix", () => {
61
+ it("includes request ids and sorts terrenoLabels for stable output", () => {
62
+ const suffix = formatLogContextSuffix({
63
+ requestId: "r1",
64
+ terrenoLabels: {alpha: "a", zebra: "z"},
65
+ });
66
+ expect(suffix).toContain("requestId=r1");
67
+ expect(suffix.indexOf("alpha=a")).toBeLessThan(suffix.indexOf("zebra=z"));
68
+ });
69
+
70
+ it("returns empty string when no fields are set", () => {
71
+ expect(formatLogContextSuffix({})).toBe("");
72
+ });
73
+
74
+ it("includes terrenoLogPrefix in suffix", () => {
75
+ const suffix = formatLogContextSuffix({
76
+ requestId: "r1",
77
+ terrenoLogPrefix: "[Job]",
78
+ });
79
+ expect(suffix).toContain("logPrefix=[Job]");
80
+ expect(suffix).toContain("requestId=r1");
81
+ });
82
+ });
83
+
84
+ describe("createScopedLogger", () => {
85
+ afterEach(() => {
86
+ setupLogging({disableFileLogging: true});
87
+ });
88
+
89
+ it("returns the global logger when prefix and labels are empty", () => {
90
+ expect(createScopedLogger({})).toBe(logger);
91
+ expect(createScopedLogger({labels: {skipped: undefined}})).toBe(logger);
92
+ });
93
+
94
+ it("prefixes messages when only prefix is set", () => {
95
+ const lines: string[] = [];
96
+ const snapshots: Array<{terrenoLogPrefix?: unknown}> = [];
97
+ const captureTransport = new winston.transports.Stream({
98
+ format: winston.format.combine(
99
+ winston.format((info) => {
100
+ snapshots.push({
101
+ terrenoLogPrefix: (info as {terrenoLogPrefix?: unknown}).terrenoLogPrefix,
102
+ });
103
+ return info;
104
+ })(),
105
+ winston.format.printf((info) => {
106
+ const msg = typeof info.message === "string" ? info.message : String(info.message);
107
+ return `${info.level}: ${msg}`;
108
+ })
109
+ ),
110
+ stream: new Writable({
111
+ write(chunk, _encoding, callback): void {
112
+ lines.push(chunk.toString().trim());
113
+ callback();
114
+ },
115
+ }),
116
+ });
117
+ winstonLogger.add(captureTransport);
118
+ try {
119
+ createScopedLogger({prefix: "[ScopedTest]"}).info("hello");
120
+ } finally {
121
+ winstonLogger.remove(captureTransport);
122
+ }
123
+ expect(lines.some((l) => l.includes("[ScopedTest]") && l.includes("hello"))).toBe(true);
124
+ expect(snapshots.some((s) => s.terrenoLogPrefix === "[ScopedTest]")).toBe(true);
125
+ });
126
+
127
+ it("attaches terrenoLabels to winston metadata for structured transports", () => {
128
+ const snapshots: Array<{level?: string; message?: unknown; terrenoLabels?: unknown}> = [];
129
+ const captureTransport = new winston.transports.Stream({
130
+ format: winston.format((info) => {
131
+ snapshots.push({
132
+ level: info.level,
133
+ message: info.message,
134
+ terrenoLabels: (info as {terrenoLabels?: unknown}).terrenoLabels,
135
+ });
136
+ return info;
137
+ })(),
138
+ stream: new Writable({
139
+ write(_chunk, _encoding, callback): void {
140
+ callback();
141
+ },
142
+ }),
143
+ });
144
+ winstonLogger.add(captureTransport);
145
+ try {
146
+ createScopedLogger({labels: {billingId: "b1"}}).warn("charged");
147
+ } finally {
148
+ winstonLogger.remove(captureTransport);
149
+ }
150
+ expect(
151
+ snapshots.some(
152
+ (s) => (s.terrenoLabels as {billingId?: string} | undefined)?.billingId === "b1"
153
+ )
154
+ ).toBe(true);
155
+ });
156
+
157
+ it("includes terrenoLogPrefix alongside terrenoLabels in metadata", () => {
158
+ const snapshots: Array<{terrenoLogPrefix?: unknown; terrenoLabels?: unknown}> = [];
159
+ const captureTransport = new winston.transports.Stream({
160
+ format: winston.format((info) => {
161
+ snapshots.push({
162
+ terrenoLabels: (info as {terrenoLabels?: unknown}).terrenoLabels,
163
+ terrenoLogPrefix: (info as {terrenoLogPrefix?: unknown}).terrenoLogPrefix,
164
+ });
165
+ return info;
166
+ })(),
167
+ stream: new Writable({
168
+ write(_chunk, _encoding, callback): void {
169
+ callback();
170
+ },
171
+ }),
172
+ });
173
+ winstonLogger.add(captureTransport);
174
+ try {
175
+ createScopedLogger({labels: {x: "1"}, prefix: "[Both]"}).info("m");
176
+ } finally {
177
+ winstonLogger.remove(captureTransport);
178
+ }
179
+ expect(
180
+ snapshots.some(
181
+ (s) =>
182
+ (s.terrenoLabels as {x?: string} | undefined)?.x === "1" &&
183
+ (s.terrenoLogPrefix as string | undefined) === "[Both]"
184
+ )
185
+ ).toBe(true);
186
+ });
187
+
188
+ it("merges all request context fields including jobId, sessionId, spanId, traceId, traceSampled", () => {
189
+ const snapshots: Array<Record<string, unknown>> = [];
190
+ const captureTransport = new winston.transports.Stream({
191
+ format: winston.format((info) => {
192
+ snapshots.push({
193
+ jobId: info.jobId,
194
+ requestId: info.requestId,
195
+ sessionId: info.sessionId,
196
+ spanId: info.spanId,
197
+ traceId: info.traceId,
198
+ traceSampled: info.traceSampled,
199
+ userId: info.userId,
200
+ });
201
+ return info;
202
+ })(),
203
+ stream: new Writable({
204
+ write(_chunk, _encoding, callback): void {
205
+ callback();
206
+ },
207
+ }),
208
+ });
209
+ winstonLogger.add(captureTransport);
210
+ try {
211
+ runWithRequestContext(
212
+ {
213
+ jobId: "job-42",
214
+ requestId: "req-full",
215
+ sessionId: "sess-7",
216
+ spanId: "span-abc",
217
+ traceId: "trace-xyz",
218
+ traceSampled: true,
219
+ userId: "user-1",
220
+ },
221
+ () => {
222
+ logger.info("full context");
223
+ }
224
+ );
225
+ } finally {
226
+ winstonLogger.remove(captureTransport);
227
+ }
228
+ const entry = snapshots.find((s) => s.requestId === "req-full");
229
+ expect(entry).toBeDefined();
230
+ expect(entry?.jobId).toBe("job-42");
231
+ expect(entry?.sessionId).toBe("sess-7");
232
+ expect(entry?.spanId).toBe("span-abc");
233
+ expect(entry?.traceId).toBe("trace-xyz");
234
+ expect(entry?.traceSampled).toBe(true);
235
+ expect(entry?.userId).toBe("user-1");
236
+ });
237
+
238
+ it("exercises all scoped logger methods (debug, error, catch)", () => {
239
+ const lines: string[] = [];
240
+ const captureTransport = new winston.transports.Stream({
241
+ format: winston.format.printf((info) => {
242
+ const msg = typeof info.message === "string" ? info.message : String(info.message);
243
+ return `${info.level}: ${msg}`;
244
+ }),
245
+ stream: new Writable({
246
+ write(chunk, _encoding, callback): void {
247
+ lines.push(chunk.toString().trim());
248
+ callback();
249
+ },
250
+ }),
251
+ });
252
+ winstonLogger.add(captureTransport);
253
+ try {
254
+ const scoped = createScopedLogger({prefix: "[Methods]"});
255
+ scoped.debug("d-msg");
256
+ scoped.error("e-msg");
257
+ scoped.catch(new Error("caught-msg"));
258
+ } finally {
259
+ winstonLogger.remove(captureTransport);
260
+ }
261
+ expect(lines.some((l) => l.includes("[Methods]") && l.includes("d-msg"))).toBe(true);
262
+ expect(lines.some((l) => l.includes("[Methods]") && l.includes("e-msg"))).toBe(true);
263
+ expect(lines.some((l) => l.includes("[Methods]") && l.includes("caught-msg"))).toBe(true);
264
+ });
265
+
266
+ it("scoped logger catch with Sentry enabled and Error instance", () => {
267
+ const OLD_ENV = process.env;
268
+ process.env = {...OLD_ENV, USE_SENTRY_LOGGING: "true"};
269
+ try {
270
+ const scoped = createScopedLogger({prefix: "[Sentry]"});
271
+ expect(() => scoped.catch(new Error("sentry scoped error"))).not.toThrow();
272
+ } finally {
273
+ process.env = OLD_ENV;
274
+ }
275
+ });
276
+
277
+ it("attaches terrenoRequestLog while request ALS scope is active", () => {
278
+ const snapshots: Array<{terrenoRequestLog?: TerrenoRequestLogEntry}> = [];
279
+ const captureTransport = new winston.transports.Stream({
280
+ format: winston.format((info) => {
281
+ snapshots.push({
282
+ terrenoRequestLog: (info as {terrenoRequestLog?: TerrenoRequestLogEntry})
283
+ .terrenoRequestLog,
284
+ });
285
+ return info;
286
+ })(),
287
+ stream: new Writable({
288
+ write(_chunk, _encoding, callback): void {
289
+ callback();
290
+ },
291
+ }),
292
+ });
293
+ winstonLogger.add(captureTransport);
294
+ try {
295
+ runWithRequestContext({requestId: "req-als-1", userId: "user-99"}, () => {
296
+ logger.info("in scope");
297
+ });
298
+ runWithRequestContext({requestId: "req-als-2"}, () => {
299
+ logger.info("anon");
300
+ });
301
+ } finally {
302
+ winstonLogger.remove(captureTransport);
303
+ }
304
+ const withUser = snapshots.find((s) => s.terrenoRequestLog?.requestId === "req-als-1");
305
+ expect(withUser?.terrenoRequestLog?.userId).toBe("user-99");
306
+ const anon = snapshots.find((s) => s.terrenoRequestLog?.requestId === "req-als-2");
307
+ expect(anon?.terrenoRequestLog?.userId).toBeNull();
308
+ });
309
+ });
310
+
311
+ describe("createFeatureFlaggedLogger", () => {
312
+ afterEach(() => {
313
+ setupLogging({disableFileLogging: true});
314
+ });
315
+
316
+ it("drops info lines while disabled", () => {
317
+ let hits = 0;
318
+ const captureTransport = new winston.transports.Stream({
319
+ format: winston.format.printf(() => {
320
+ hits += 1;
321
+ return "";
322
+ }),
323
+ stream: new Writable({
324
+ write(_chunk, _encoding, callback): void {
325
+ callback();
326
+ },
327
+ }),
328
+ });
329
+ winstonLogger.add(captureTransport);
330
+ try {
331
+ const log = createFeatureFlaggedLogger({isEnabled: () => false, target: logger});
332
+ log.info("hidden");
333
+ } finally {
334
+ winstonLogger.remove(captureTransport);
335
+ }
336
+ expect(hits).toBe(0);
337
+ });
338
+
339
+ it("forwards lines when enabled", () => {
340
+ let hits = 0;
341
+ const captureTransport = new winston.transports.Stream({
342
+ format: winston.format.printf(() => {
343
+ hits += 1;
344
+ return "";
345
+ }),
346
+ stream: new Writable({
347
+ write(_chunk, _encoding, callback): void {
348
+ callback();
349
+ },
350
+ }),
351
+ });
352
+ winstonLogger.add(captureTransport);
353
+ try {
354
+ const log = createFeatureFlaggedLogger({isEnabled: () => true, target: logger});
355
+ log.info("visible");
356
+ } finally {
357
+ winstonLogger.remove(captureTransport);
358
+ }
359
+ expect(hits).toBeGreaterThan(0);
360
+ });
361
+
362
+ it("forwards catch while disabled when gateCatch is false", () => {
363
+ let hits = 0;
364
+ const captureTransport = new winston.transports.Stream({
365
+ format: winston.format.printf(() => {
366
+ hits += 1;
367
+ return "";
368
+ }),
369
+ stream: new Writable({
370
+ write(_chunk, _encoding, callback): void {
371
+ callback();
372
+ },
373
+ }),
374
+ });
375
+ winstonLogger.add(captureTransport);
376
+ try {
377
+ const log = createFeatureFlaggedLogger({
378
+ gateCatch: false,
379
+ isEnabled: () => false,
380
+ target: logger,
381
+ });
382
+ log.catch(new Error("still-logged"));
383
+ } finally {
384
+ winstonLogger.remove(captureTransport);
385
+ }
386
+ expect(hits).toBeGreaterThan(0);
387
+ });
388
+
389
+ it("forwards all methods when enabled (debug, warn, error)", () => {
390
+ const levels: string[] = [];
391
+ const captureTransport = new winston.transports.Stream({
392
+ format: winston.format((info) => {
393
+ levels.push(info.level);
394
+ return info;
395
+ })(),
396
+ stream: new Writable({
397
+ write(_chunk, _encoding, callback): void {
398
+ callback();
399
+ },
400
+ }),
401
+ });
402
+ winstonLogger.add(captureTransport);
403
+ try {
404
+ const log = createFeatureFlaggedLogger({isEnabled: () => true, target: logger});
405
+ log.debug("d");
406
+ log.warn("w");
407
+ log.error("e");
408
+ } finally {
409
+ winstonLogger.remove(captureTransport);
410
+ }
411
+ expect(levels.some((l) => l === "debug")).toBe(true);
412
+ expect(levels.some((l) => l === "warn")).toBe(true);
413
+ expect(levels.some((l) => l === "error")).toBe(true);
414
+ });
415
+
416
+ it("drops debug, warn, error lines when disabled", () => {
417
+ let hits = 0;
418
+ const captureTransport = new winston.transports.Stream({
419
+ format: winston.format.printf(() => {
420
+ hits += 1;
421
+ return "";
422
+ }),
423
+ stream: new Writable({
424
+ write(_chunk, _encoding, callback): void {
425
+ callback();
426
+ },
427
+ }),
428
+ });
429
+ winstonLogger.add(captureTransport);
430
+ try {
431
+ const log = createFeatureFlaggedLogger({isEnabled: () => false, target: logger});
432
+ log.debug("hidden-d");
433
+ log.warn("hidden-w");
434
+ log.error("hidden-e");
435
+ } finally {
436
+ winstonLogger.remove(captureTransport);
437
+ }
438
+ expect(hits).toBe(0);
439
+ });
440
+
441
+ it("uses the global logger as default target when target is not provided", () => {
442
+ const log = createFeatureFlaggedLogger({isEnabled: () => true});
443
+ expect(() => log.info("default target")).not.toThrow();
444
+ });
445
+
446
+ it("drops catch while disabled when gateCatch is true", () => {
447
+ let hits = 0;
448
+ const captureTransport = new winston.transports.Stream({
449
+ format: winston.format.printf(() => {
450
+ hits += 1;
451
+ return "";
452
+ }),
453
+ stream: new Writable({
454
+ write(_chunk, _encoding, callback): void {
455
+ callback();
456
+ },
457
+ }),
458
+ });
459
+ winstonLogger.add(captureTransport);
460
+ try {
461
+ const log = createFeatureFlaggedLogger({
462
+ gateCatch: true,
463
+ isEnabled: () => false,
464
+ target: logger,
465
+ });
466
+ log.catch(new Error("suppressed"));
467
+ } finally {
468
+ winstonLogger.remove(captureTransport);
469
+ }
470
+ expect(hits).toBe(0);
471
+ });
472
+ });
473
+
50
474
  describe("setupLogging", () => {
51
475
  let tempDir: string;
52
476
 
@@ -146,4 +570,45 @@ describe("setupLogging", () => {
146
570
  });
147
571
  expect(true).toBe(true);
148
572
  });
573
+
574
+ it("console format includes timestamps when showConsoleTimestamps is true", () => {
575
+ const lines: string[] = [];
576
+ setupLogging({
577
+ disableFileLogging: true,
578
+ showConsoleTimestamps: true,
579
+ });
580
+ const captureTransport = new winston.transports.Stream({
581
+ format: winston.format.combine(
582
+ winston.format.timestamp(),
583
+ winston.format.printf((info) => {
584
+ if (info.timestamp) {
585
+ return `${info.timestamp} - ${info.level}: ${info.message}`;
586
+ }
587
+ return `${info.level}: ${info.message}`;
588
+ })
589
+ ),
590
+ stream: new Writable({
591
+ write(chunk, _encoding, callback): void {
592
+ lines.push(chunk.toString().trim());
593
+ callback();
594
+ },
595
+ }),
596
+ });
597
+ winstonLogger.add(captureTransport);
598
+ try {
599
+ logger.info("timestamp-test");
600
+ } finally {
601
+ winstonLogger.remove(captureTransport);
602
+ }
603
+ expect(lines.some((l) => l.includes("timestamp-test"))).toBe(true);
604
+ });
605
+
606
+ it("disableTerrenoDevJsonlLog skips the dev JSONL transport", () => {
607
+ expect(() =>
608
+ setupLogging({
609
+ disableFileLogging: true,
610
+ disableTerrenoDevJsonlLog: true,
611
+ })
612
+ ).not.toThrow();
613
+ });
149
614
  });