te.js 2.1.6 → 2.2.1
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.
- package/README.md +1 -12
- package/auto-docs/analysis/handler-analyzer.test.js +106 -0
- package/auto-docs/analysis/source-resolver.test.js +58 -0
- package/auto-docs/constants.js +13 -2
- package/auto-docs/openapi/generator.js +7 -5
- package/auto-docs/openapi/generator.test.js +132 -0
- package/auto-docs/openapi/spec-builders.js +39 -19
- package/cli/docs-command.js +44 -36
- package/cors/index.test.js +82 -0
- package/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +7 -2
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +382 -0
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +19 -22
- package/rate-limit/index.test.js +93 -0
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- package/server/ammo/body-parser.js +156 -152
- package/server/ammo/body-parser.test.js +79 -0
- package/server/ammo/enhancer.js +8 -4
- package/server/ammo.js +138 -12
- package/server/context/request-context.js +51 -0
- package/server/context/request-context.test.js +53 -0
- package/server/endpoint.js +15 -0
- package/server/error.js +56 -3
- package/server/error.test.js +45 -0
- package/server/errors/channels/channels.test.js +148 -0
- package/server/errors/channels/index.js +1 -1
- package/server/errors/llm-cache.js +1 -1
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +1 -1
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +1 -1
- package/server/targets/registry.js +3 -3
- package/server/targets/registry.test.js +108 -0
- package/te.js +233 -183
- package/utils/auto-register.js +1 -1
- package/utils/configuration.js +23 -9
- package/utils/configuration.test.js +58 -0
- package/utils/errors-llm-config.js +74 -8
- package/utils/request-logger.js +49 -3
- package/utils/startup.js +80 -0
- package/database/index.js +0 -165
- package/database/mongodb.js +0 -146
- package/database/redis.js +0 -201
- package/docs/database.md +0 -390
package/server/ammo.js
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
buildPayload,
|
|
16
16
|
dispatchToChannels,
|
|
17
17
|
} from './errors/channels/index.js';
|
|
18
|
+
import TejLogger from 'tej-logger';
|
|
19
|
+
|
|
20
|
+
const logger = new TejLogger('Tejas.Ammo');
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Detect if the value is a throw() options object (per-call overrides).
|
|
@@ -85,6 +88,13 @@ class Ammo {
|
|
|
85
88
|
|
|
86
89
|
// Response related data
|
|
87
90
|
this.dispatchedData = undefined;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolved error info stashed after ammo.throw() completes.
|
|
94
|
+
* Read by the radar middleware on res.finish to populate error tracking.
|
|
95
|
+
* @type {{ message: string, type: string|null, devInsight: string|null, stack: string|null, codeContext: object|null } | null}
|
|
96
|
+
*/
|
|
97
|
+
this._errorInfo = null;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/**
|
|
@@ -371,7 +381,11 @@ class Ammo {
|
|
|
371
381
|
// Per-call options: last arg can be { useLlm?, messageType? } when call is LLM-eligible (no explicit code).
|
|
372
382
|
const llmEligible =
|
|
373
383
|
args.length === 0 ||
|
|
374
|
-
(!isStatusCode(args[0]) &&
|
|
384
|
+
(!isStatusCode(args[0]) &&
|
|
385
|
+
!(
|
|
386
|
+
typeof args[0]?.statusCode === 'number' &&
|
|
387
|
+
typeof args[0]?.code === 'string'
|
|
388
|
+
));
|
|
375
389
|
let throwOpts =
|
|
376
390
|
/** @type {{ useLlm?: boolean, messageType?: 'endUser'|'developer' } | null} */ (
|
|
377
391
|
null
|
|
@@ -393,7 +407,7 @@ class Ammo {
|
|
|
393
407
|
// Capture the stack string SYNCHRONOUSLY before any async work or fire() call,
|
|
394
408
|
// because the call stack unwinds as soon as we await or respond.
|
|
395
409
|
const stack =
|
|
396
|
-
args[0]
|
|
410
|
+
args[0] != null && typeof args[0].stack === 'string'
|
|
397
411
|
? args[0].stack
|
|
398
412
|
: new Error().stack;
|
|
399
413
|
const originalError =
|
|
@@ -405,11 +419,32 @@ class Ammo {
|
|
|
405
419
|
// Respond immediately with a generic 500, then run LLM in the background.
|
|
406
420
|
this.fire(500, 'Internal Server Error');
|
|
407
421
|
|
|
408
|
-
//
|
|
422
|
+
// Stash basic error info synchronously so radar can read it on res.finish
|
|
423
|
+
// even before LLM completes. LLM result will update _errorInfo when ready.
|
|
424
|
+
const errorType =
|
|
425
|
+
originalError != null &&
|
|
426
|
+
typeof originalError.constructor?.name === 'string'
|
|
427
|
+
? originalError.constructor.name
|
|
428
|
+
: originalError !== undefined
|
|
429
|
+
? typeof originalError
|
|
430
|
+
: null;
|
|
431
|
+
this._errorInfo = {
|
|
432
|
+
message: 'Internal Server Error',
|
|
433
|
+
type: errorType,
|
|
434
|
+
devInsight: null,
|
|
435
|
+
stack: stack ?? null,
|
|
436
|
+
codeContext: null,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Run LLM in the background; expose the promise so the Radar middleware
|
|
440
|
+
// can await it before flushing events (ensures LLM data is captured).
|
|
409
441
|
const method = this.method;
|
|
410
442
|
const path = this.path;
|
|
411
|
-
|
|
443
|
+
const self = this;
|
|
444
|
+
this._llmPromise = captureCodeContext(stack)
|
|
412
445
|
.then((codeContext) => {
|
|
446
|
+
// Update _errorInfo with captured code context
|
|
447
|
+
if (self._errorInfo) self._errorInfo.codeContext = codeContext;
|
|
413
448
|
const context = {
|
|
414
449
|
codeContext,
|
|
415
450
|
method,
|
|
@@ -428,6 +463,11 @@ class Ammo {
|
|
|
428
463
|
}));
|
|
429
464
|
})
|
|
430
465
|
.then(({ result, codeContext }) => {
|
|
466
|
+
// Update _errorInfo with full LLM result
|
|
467
|
+
if (self._errorInfo) {
|
|
468
|
+
self._errorInfo.message = result.message;
|
|
469
|
+
self._errorInfo.devInsight = result.devInsight ?? null;
|
|
470
|
+
}
|
|
431
471
|
const channels = getChannels(channel, logFile);
|
|
432
472
|
const payload = buildPayload({
|
|
433
473
|
method,
|
|
@@ -442,8 +482,12 @@ class Ammo {
|
|
|
442
482
|
});
|
|
443
483
|
return dispatchToChannels(channels, payload);
|
|
444
484
|
})
|
|
445
|
-
.catch(() => {
|
|
446
|
-
//
|
|
485
|
+
.catch((err) => {
|
|
486
|
+
// Background LLM failed after HTTP response already sent — log the failure
|
|
487
|
+
// but do not attempt to respond again.
|
|
488
|
+
logger.warn(
|
|
489
|
+
`Background LLM dispatch failed: ${err?.message ?? err}`,
|
|
490
|
+
);
|
|
447
491
|
});
|
|
448
492
|
|
|
449
493
|
return;
|
|
@@ -462,9 +506,27 @@ class Ammo {
|
|
|
462
506
|
}),
|
|
463
507
|
};
|
|
464
508
|
if (originalError !== undefined) context.error = originalError;
|
|
465
|
-
return inferErrorFromContext(context)
|
|
509
|
+
return inferErrorFromContext(context).then((result) => ({
|
|
510
|
+
result,
|
|
511
|
+
codeContext,
|
|
512
|
+
}));
|
|
466
513
|
})
|
|
467
|
-
.then(({
|
|
514
|
+
.then(({ result, codeContext }) => {
|
|
515
|
+
const { statusCode, message, devInsight } = result;
|
|
516
|
+
const errorType =
|
|
517
|
+
originalError != null &&
|
|
518
|
+
typeof originalError.constructor?.name === 'string'
|
|
519
|
+
? originalError.constructor.name
|
|
520
|
+
: originalError !== undefined
|
|
521
|
+
? typeof originalError
|
|
522
|
+
: null;
|
|
523
|
+
this._errorInfo = {
|
|
524
|
+
message,
|
|
525
|
+
type: errorType,
|
|
526
|
+
devInsight: devInsight ?? null,
|
|
527
|
+
stack: stack ?? null,
|
|
528
|
+
codeContext: codeContext ?? null,
|
|
529
|
+
};
|
|
468
530
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
469
531
|
const data =
|
|
470
532
|
!isProduction && devInsight
|
|
@@ -472,15 +534,23 @@ class Ammo {
|
|
|
472
534
|
: message;
|
|
473
535
|
this.fire(statusCode, data);
|
|
474
536
|
})
|
|
475
|
-
.catch(() => {
|
|
537
|
+
.catch((err) => {
|
|
476
538
|
// LLM call failed (network error, timeout, etc.) — fall back to generic 500
|
|
477
539
|
// so the client always gets a response and we don't trigger an infinite retry loop.
|
|
540
|
+
logger.warn(`LLM error inference failed: ${err?.message ?? err}`);
|
|
478
541
|
this.fire(500, 'Internal Server Error');
|
|
479
542
|
});
|
|
480
543
|
}
|
|
481
544
|
|
|
482
545
|
// Sync path: explicit code/message or useLlm: false
|
|
483
546
|
if (args.length === 0) {
|
|
547
|
+
this._errorInfo = {
|
|
548
|
+
message: 'Internal Server Error',
|
|
549
|
+
type: null,
|
|
550
|
+
devInsight: null,
|
|
551
|
+
stack: null,
|
|
552
|
+
codeContext: null,
|
|
553
|
+
};
|
|
484
554
|
this.fire(500, 'Internal Server Error');
|
|
485
555
|
return;
|
|
486
556
|
}
|
|
@@ -488,29 +558,71 @@ class Ammo {
|
|
|
488
558
|
if (isStatusCode(args[0])) {
|
|
489
559
|
const statusCode = args[0];
|
|
490
560
|
const message = args[1] || toStatusMessage(statusCode);
|
|
561
|
+
this._errorInfo = {
|
|
562
|
+
message,
|
|
563
|
+
type: null,
|
|
564
|
+
devInsight: null,
|
|
565
|
+
stack: null,
|
|
566
|
+
codeContext: null,
|
|
567
|
+
};
|
|
491
568
|
this.fire(statusCode, message);
|
|
492
569
|
return;
|
|
493
570
|
}
|
|
494
571
|
|
|
495
|
-
if (
|
|
572
|
+
if (
|
|
573
|
+
typeof args[0]?.statusCode === 'number' &&
|
|
574
|
+
typeof args[0]?.code === 'string'
|
|
575
|
+
) {
|
|
496
576
|
const error = args[0];
|
|
497
|
-
this.
|
|
577
|
+
this._errorInfo = {
|
|
578
|
+
message: error.message,
|
|
579
|
+
type: error.constructor?.name ?? 'TejError',
|
|
580
|
+
devInsight: null,
|
|
581
|
+
stack: error.stack ?? null,
|
|
582
|
+
codeContext: null,
|
|
583
|
+
};
|
|
584
|
+
this.fire(error.statusCode, error.message);
|
|
498
585
|
return;
|
|
499
586
|
}
|
|
500
587
|
|
|
501
|
-
if (
|
|
588
|
+
if (
|
|
589
|
+
args[0] != null &&
|
|
590
|
+
typeof args[0].message === 'string' &&
|
|
591
|
+
typeof args[0].stack === 'string'
|
|
592
|
+
) {
|
|
502
593
|
const error = args[0];
|
|
503
594
|
if (!isNaN(parseInt(error.message))) {
|
|
504
595
|
const statusCode = parseInt(error.message);
|
|
505
596
|
const message = toStatusMessage(statusCode) || toStatusMessage(500);
|
|
597
|
+
this._errorInfo = {
|
|
598
|
+
message,
|
|
599
|
+
type: error.constructor.name,
|
|
600
|
+
devInsight: null,
|
|
601
|
+
stack: error.stack ?? null,
|
|
602
|
+
codeContext: null,
|
|
603
|
+
};
|
|
506
604
|
this.fire(statusCode, message);
|
|
507
605
|
return;
|
|
508
606
|
}
|
|
509
607
|
const statusCode = toStatusCode(error.message);
|
|
510
608
|
if (statusCode) {
|
|
609
|
+
this._errorInfo = {
|
|
610
|
+
message: error.message,
|
|
611
|
+
type: error.constructor.name,
|
|
612
|
+
devInsight: null,
|
|
613
|
+
stack: error.stack ?? null,
|
|
614
|
+
codeContext: null,
|
|
615
|
+
};
|
|
511
616
|
this.fire(statusCode, error.message);
|
|
512
617
|
return;
|
|
513
618
|
}
|
|
619
|
+
this._errorInfo = {
|
|
620
|
+
message: error.message,
|
|
621
|
+
type: error.constructor.name,
|
|
622
|
+
devInsight: null,
|
|
623
|
+
stack: error.stack ?? null,
|
|
624
|
+
codeContext: null,
|
|
625
|
+
};
|
|
514
626
|
this.fire(500, error.message);
|
|
515
627
|
return;
|
|
516
628
|
}
|
|
@@ -518,9 +630,23 @@ class Ammo {
|
|
|
518
630
|
const errorValue = args[0];
|
|
519
631
|
const statusCode = toStatusCode(errorValue);
|
|
520
632
|
if (statusCode) {
|
|
633
|
+
this._errorInfo = {
|
|
634
|
+
message: toStatusMessage(statusCode),
|
|
635
|
+
type: null,
|
|
636
|
+
devInsight: null,
|
|
637
|
+
stack: null,
|
|
638
|
+
codeContext: null,
|
|
639
|
+
};
|
|
521
640
|
this.fire(statusCode, toStatusMessage(statusCode));
|
|
522
641
|
return;
|
|
523
642
|
}
|
|
643
|
+
this._errorInfo = {
|
|
644
|
+
message: errorValue.toString(),
|
|
645
|
+
type: null,
|
|
646
|
+
devInsight: null,
|
|
647
|
+
stack: null,
|
|
648
|
+
codeContext: null,
|
|
649
|
+
};
|
|
524
650
|
this.fire(500, errorValue.toString());
|
|
525
651
|
}
|
|
526
652
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Request-scoped context using AsyncLocalStorage.
|
|
3
|
+
*
|
|
4
|
+
* Provides a safe, mutation-resistant store for per-request data.
|
|
5
|
+
* The store is initialized by the handler and is accessible from any
|
|
6
|
+
* code running within the same async execution context (middleware, handlers,
|
|
7
|
+
* service functions, etc.) without needing to thread parameters through.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // In a middleware or handler:
|
|
11
|
+
* import { getRequestId, getRequestStore } from '../server/context/request-context.js';
|
|
12
|
+
* const requestId = getRequestId(); // 'abc-123...' or 'no-context'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
/** @type {AsyncLocalStorage<{ requestId: string, startTime: number }>} */
|
|
19
|
+
export const requestContext = new AsyncLocalStorage();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the current request ID from async context, or 'no-context' if called
|
|
23
|
+
* outside of an active request (e.g., during startup).
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function getRequestId() {
|
|
27
|
+
return requestContext.getStore()?.requestId ?? 'no-context';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the full request store, or null if not in a request context.
|
|
32
|
+
* @returns {{ requestId: string, startTime: number } | undefined}
|
|
33
|
+
*/
|
|
34
|
+
export function getRequestStore() {
|
|
35
|
+
return requestContext.getStore();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Middleware that initializes the per-request AsyncLocalStorage store.
|
|
40
|
+
* Must be the first global middleware registered via app.midair().
|
|
41
|
+
*
|
|
42
|
+
* @param {import('../ammo.js').default} ammo
|
|
43
|
+
* @param {function(): Promise<void>} next
|
|
44
|
+
*/
|
|
45
|
+
export function contextMiddleware(ammo, next) {
|
|
46
|
+
const store = {
|
|
47
|
+
requestId: randomUUID(),
|
|
48
|
+
startTime: Date.now(),
|
|
49
|
+
};
|
|
50
|
+
return requestContext.run(store, next);
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for request-context (AsyncLocalStorage).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
requestContext,
|
|
7
|
+
getRequestId,
|
|
8
|
+
getRequestStore,
|
|
9
|
+
contextMiddleware,
|
|
10
|
+
} from './request-context.js';
|
|
11
|
+
|
|
12
|
+
describe('requestContext', () => {
|
|
13
|
+
it('should return "no-context" when called outside a request', () => {
|
|
14
|
+
expect(getRequestId()).toBe('no-context');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return undefined store outside a request', () => {
|
|
18
|
+
expect(getRequestStore()).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should provide requestId within contextMiddleware', async () => {
|
|
22
|
+
const results = [];
|
|
23
|
+
const next = async () => {
|
|
24
|
+
results.push(getRequestId());
|
|
25
|
+
};
|
|
26
|
+
await contextMiddleware({}, next);
|
|
27
|
+
expect(results).toHaveLength(1);
|
|
28
|
+
expect(typeof results[0]).toBe('string');
|
|
29
|
+
expect(results[0]).toMatch(
|
|
30
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should provide startTime within contextMiddleware', async () => {
|
|
35
|
+
const before = Date.now();
|
|
36
|
+
let store;
|
|
37
|
+
await contextMiddleware({}, async () => {
|
|
38
|
+
store = getRequestStore();
|
|
39
|
+
});
|
|
40
|
+
expect(store.startTime).toBeGreaterThanOrEqual(before);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('each request should have unique requestId', async () => {
|
|
44
|
+
const ids = [];
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
await contextMiddleware({}, async () => {
|
|
47
|
+
ids.push(getRequestId());
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const unique = new Set(ids);
|
|
51
|
+
expect(unique.size).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/server/endpoint.js
CHANGED
|
@@ -2,7 +2,16 @@ import isMiddlewareValid from './targets/middleware-validator.js';
|
|
|
2
2
|
import { isPathValid, standardizePath } from './targets/path-validator.js';
|
|
3
3
|
import isShootValid from './targets/shoot-validator.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Represents a single route endpoint: a path, handler, optional middlewares,
|
|
7
|
+
* allowed HTTP methods, and documentation metadata.
|
|
8
|
+
*
|
|
9
|
+
* Use the fluent builder methods to configure before registering with a Target.
|
|
10
|
+
*/
|
|
5
11
|
class Endpoint {
|
|
12
|
+
/**
|
|
13
|
+
* Create a new Endpoint with empty defaults.
|
|
14
|
+
*/
|
|
6
15
|
constructor() {
|
|
7
16
|
this.path = '';
|
|
8
17
|
this.middlewares = [];
|
|
@@ -14,6 +23,12 @@ class Endpoint {
|
|
|
14
23
|
this.group = null;
|
|
15
24
|
}
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Set the full path by combining a base path and a path segment.
|
|
28
|
+
* @param {string} base - Base path (e.g. '/api')
|
|
29
|
+
* @param {string} path - Route path segment (e.g. '/users')
|
|
30
|
+
* @returns {Endpoint}
|
|
31
|
+
*/
|
|
17
32
|
setPath(base, path) {
|
|
18
33
|
const standardizedBase = standardizePath(base);
|
|
19
34
|
const standardizedPath = standardizePath(path);
|
package/server/error.js
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base error class for the Tejas framework.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework extend TejError. The constructor
|
|
5
|
+
* accepts the HTTP status code as the first argument (matching the existing
|
|
6
|
+
* call signature `new TejError(statusCode, message)`) and derives a
|
|
7
|
+
* machine-readable `code` string of the form `ERR_HTTP_<statusCode>`.
|
|
8
|
+
*
|
|
9
|
+
* Well-known codes are available as named constants on `TejError` for
|
|
10
|
+
* callers that want expressive error codes without hardcoding numbers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base framework error class.
|
|
15
|
+
*
|
|
16
|
+
* @extends {Error}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* throw new TejError(404, 'Not Found');
|
|
20
|
+
* // error.statusCode === 404
|
|
21
|
+
* // error.code === 'ERR_HTTP_404'
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // With cause chaining
|
|
25
|
+
* throw new TejError(500, 'Database failure', { cause: originalError });
|
|
26
|
+
*/
|
|
1
27
|
class TejError extends Error {
|
|
2
|
-
|
|
3
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {number} statusCode - HTTP status code (e.g. 404, 500)
|
|
30
|
+
* @param {string} message - Human-readable description
|
|
31
|
+
* @param {{ cause?: Error }} [options] - Optional native cause for chaining
|
|
32
|
+
*/
|
|
33
|
+
constructor(statusCode, message, options) {
|
|
34
|
+
super(message, options);
|
|
4
35
|
this.name = this.constructor.name;
|
|
5
|
-
|
|
36
|
+
/** @type {number} HTTP status code */
|
|
37
|
+
this.statusCode = statusCode;
|
|
38
|
+
/** @type {string} Machine-readable error code derived from status */
|
|
39
|
+
this.code = `ERR_HTTP_${statusCode}`;
|
|
40
|
+
Error.captureStackTrace(this, this.constructor);
|
|
6
41
|
}
|
|
7
42
|
}
|
|
8
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Named error codes for common framework scenarios.
|
|
46
|
+
* Use these instead of hardcoding numeric status codes at call sites.
|
|
47
|
+
*/
|
|
48
|
+
TejError.CODES = Object.freeze({
|
|
49
|
+
ERR_ROUTING_FAILED: 'ERR_ROUTING_FAILED',
|
|
50
|
+
ERR_INVALID_DEPENDENCY: 'ERR_INVALID_DEPENDENCY',
|
|
51
|
+
ERR_PLUGIN_LOAD_FAILED: 'ERR_PLUGIN_LOAD_FAILED',
|
|
52
|
+
ERR_CONFIG_INVALID: 'ERR_CONFIG_INVALID',
|
|
53
|
+
ERR_STREAM_OVERFLOW: 'ERR_STREAM_OVERFLOW',
|
|
54
|
+
ERR_AUTH_FAILED: 'ERR_AUTH_FAILED',
|
|
55
|
+
ERR_NOT_FOUND: 'ERR_HTTP_404',
|
|
56
|
+
ERR_METHOD_NOT_ALLOWED: 'ERR_HTTP_405',
|
|
57
|
+
ERR_UNAUTHORIZED: 'ERR_HTTP_401',
|
|
58
|
+
ERR_BAD_REQUEST: 'ERR_HTTP_400',
|
|
59
|
+
ERR_INTERNAL: 'ERR_HTTP_500',
|
|
60
|
+
});
|
|
61
|
+
|
|
9
62
|
export default TejError;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for TejError base class.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import TejError from './error.js';
|
|
6
|
+
|
|
7
|
+
describe('TejError', () => {
|
|
8
|
+
it('should set statusCode and derive ERR_HTTP_* code', () => {
|
|
9
|
+
const err = new TejError(404, 'Not Found');
|
|
10
|
+
expect(err.statusCode).toBe(404);
|
|
11
|
+
expect(err.code).toBe('ERR_HTTP_404');
|
|
12
|
+
expect(err.message).toBe('Not Found');
|
|
13
|
+
expect(err.name).toBe('TejError');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should capture a stack trace', () => {
|
|
17
|
+
const err = new TejError(500, 'Internal Server Error');
|
|
18
|
+
expect(typeof err.stack).toBe('string');
|
|
19
|
+
expect(err.stack).toContain('TejError');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should support native cause chaining', () => {
|
|
23
|
+
const cause = new Error('original');
|
|
24
|
+
const err = new TejError(500, 'Wrapped', { cause });
|
|
25
|
+
expect(err.cause).toBe(cause);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should extend Error', () => {
|
|
29
|
+
const err = new TejError(400, 'Bad Request');
|
|
30
|
+
expect(err).toBeInstanceOf(Error);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have frozen CODES constant', () => {
|
|
34
|
+
expect(Object.isFrozen(TejError.CODES)).toBe(true);
|
|
35
|
+
expect(TejError.CODES.ERR_ROUTING_FAILED).toBe('ERR_ROUTING_FAILED');
|
|
36
|
+
expect(TejError.CODES.ERR_NOT_FOUND).toBe('ERR_HTTP_404');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should derive correct codes for common statuses', () => {
|
|
40
|
+
expect(new TejError(400, '').code).toBe('ERR_HTTP_400');
|
|
41
|
+
expect(new TejError(401, '').code).toBe('ERR_HTTP_401');
|
|
42
|
+
expect(new TejError(403, '').code).toBe('ERR_HTTP_403');
|
|
43
|
+
expect(new TejError(500, '').code).toBe('ERR_HTTP_500');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { getChannels, buildPayload } from './index.js';
|
|
3
|
+
import { ConsoleChannel } from './console.js';
|
|
4
|
+
import { LogChannel } from './log.js';
|
|
5
|
+
|
|
6
|
+
describe('getChannels()', () => {
|
|
7
|
+
it('returns a ConsoleChannel for "console"', () => {
|
|
8
|
+
const channels = getChannels('console', './test.log');
|
|
9
|
+
expect(channels).toHaveLength(1);
|
|
10
|
+
expect(channels[0]).toBeInstanceOf(ConsoleChannel);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns a LogChannel for "log"', () => {
|
|
14
|
+
const channels = getChannels('log', './test.log');
|
|
15
|
+
expect(channels).toHaveLength(1);
|
|
16
|
+
expect(channels[0]).toBeInstanceOf(LogChannel);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns both channels for "both"', () => {
|
|
20
|
+
const channels = getChannels('both', './test.log');
|
|
21
|
+
expect(channels).toHaveLength(2);
|
|
22
|
+
const types = channels.map((c) => c.constructor.name);
|
|
23
|
+
expect(types).toContain('ConsoleChannel');
|
|
24
|
+
expect(types).toContain('LogChannel');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('defaults to ConsoleChannel for unknown values', () => {
|
|
28
|
+
const channels = getChannels('unknown', './test.log');
|
|
29
|
+
expect(channels).toHaveLength(1);
|
|
30
|
+
expect(channels[0]).toBeInstanceOf(ConsoleChannel);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns same ConsoleChannel singleton across calls', () => {
|
|
34
|
+
const a = getChannels('console', './test.log')[0];
|
|
35
|
+
const b = getChannels('console', './test.log')[0];
|
|
36
|
+
expect(a).toBe(b);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('LogChannel uses the provided logFile path', () => {
|
|
40
|
+
const channels = getChannels('log', './my-errors.log');
|
|
41
|
+
expect(channels[0].logFile).toBe('./my-errors.log');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('buildPayload()', () => {
|
|
46
|
+
const codeContext = {
|
|
47
|
+
snippets: [
|
|
48
|
+
{ file: '/app/handler.js', line: 10, snippet: '→ ammo.throw()' },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
it('builds a complete payload', () => {
|
|
53
|
+
const payload = buildPayload({
|
|
54
|
+
method: 'POST',
|
|
55
|
+
path: '/users',
|
|
56
|
+
originalError: new Error('DB error'),
|
|
57
|
+
codeContext,
|
|
58
|
+
statusCode: 500,
|
|
59
|
+
message: 'Internal Server Error',
|
|
60
|
+
devInsight: 'DB connection may be down.',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(payload.method).toBe('POST');
|
|
64
|
+
expect(payload.path).toBe('/users');
|
|
65
|
+
expect(payload.statusCode).toBe(500);
|
|
66
|
+
expect(payload.message).toBe('Internal Server Error');
|
|
67
|
+
expect(payload.devInsight).toBe('DB connection may be down.');
|
|
68
|
+
expect(payload.error).toEqual({ type: 'Error', message: 'DB error' });
|
|
69
|
+
expect(payload.codeContext).toBe(codeContext);
|
|
70
|
+
expect(typeof payload.timestamp).toBe('string');
|
|
71
|
+
expect(() => new Date(payload.timestamp)).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles string error', () => {
|
|
75
|
+
const payload = buildPayload({
|
|
76
|
+
method: 'GET',
|
|
77
|
+
path: '/items',
|
|
78
|
+
originalError: 'Not found',
|
|
79
|
+
codeContext,
|
|
80
|
+
statusCode: 404,
|
|
81
|
+
message: 'Not found',
|
|
82
|
+
});
|
|
83
|
+
expect(payload.error).toEqual({ type: 'string', message: 'Not found' });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('sets error to null when no originalError', () => {
|
|
87
|
+
const payload = buildPayload({
|
|
88
|
+
method: 'GET',
|
|
89
|
+
path: '/',
|
|
90
|
+
originalError: null,
|
|
91
|
+
codeContext,
|
|
92
|
+
statusCode: 500,
|
|
93
|
+
message: 'Error',
|
|
94
|
+
});
|
|
95
|
+
expect(payload.error).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('includes cached flag when provided', () => {
|
|
99
|
+
const payload = buildPayload({
|
|
100
|
+
method: 'GET',
|
|
101
|
+
path: '/',
|
|
102
|
+
originalError: null,
|
|
103
|
+
codeContext,
|
|
104
|
+
statusCode: 404,
|
|
105
|
+
message: 'Not found',
|
|
106
|
+
cached: true,
|
|
107
|
+
});
|
|
108
|
+
expect(payload.cached).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('includes rateLimited flag when provided', () => {
|
|
112
|
+
const payload = buildPayload({
|
|
113
|
+
method: 'GET',
|
|
114
|
+
path: '/',
|
|
115
|
+
originalError: null,
|
|
116
|
+
codeContext,
|
|
117
|
+
statusCode: 500,
|
|
118
|
+
message: 'Error',
|
|
119
|
+
rateLimited: true,
|
|
120
|
+
});
|
|
121
|
+
expect(payload.rateLimited).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('omits cached and rateLimited when not provided', () => {
|
|
125
|
+
const payload = buildPayload({
|
|
126
|
+
method: 'GET',
|
|
127
|
+
path: '/',
|
|
128
|
+
originalError: null,
|
|
129
|
+
codeContext,
|
|
130
|
+
statusCode: 200,
|
|
131
|
+
message: 'OK',
|
|
132
|
+
});
|
|
133
|
+
expect(payload).not.toHaveProperty('cached');
|
|
134
|
+
expect(payload).not.toHaveProperty('rateLimited');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('omits devInsight when not provided', () => {
|
|
138
|
+
const payload = buildPayload({
|
|
139
|
+
method: 'GET',
|
|
140
|
+
path: '/',
|
|
141
|
+
originalError: null,
|
|
142
|
+
codeContext,
|
|
143
|
+
statusCode: 500,
|
|
144
|
+
message: 'Error',
|
|
145
|
+
});
|
|
146
|
+
expect(payload).not.toHaveProperty('devInsight');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -77,7 +77,7 @@ export function buildPayload({
|
|
|
77
77
|
rateLimited,
|
|
78
78
|
}) {
|
|
79
79
|
let errorSummary = null;
|
|
80
|
-
if (originalError
|
|
80
|
+
if (originalError != null && typeof originalError.message === 'string') {
|
|
81
81
|
errorSummary = {
|
|
82
82
|
type: originalError.constructor?.name ?? 'Error',
|
|
83
83
|
message: originalError.message ?? '',
|
|
@@ -29,7 +29,7 @@ class LLMErrorCache {
|
|
|
29
29
|
const snippet = codeContext?.snippets?.[0];
|
|
30
30
|
const location = snippet ? `${snippet.file}:${snippet.line}` : 'unknown';
|
|
31
31
|
let errText = '';
|
|
32
|
-
if (error
|
|
32
|
+
if (error != null && typeof error.message === 'string') {
|
|
33
33
|
errText = error.message ?? '';
|
|
34
34
|
} else if (error != null) {
|
|
35
35
|
errText = String(error);
|