@soleri/forge 5.14.0 → 5.14.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.
Files changed (102) hide show
  1. package/dist/index.js +0 -0
  2. package/dist/lib.d.ts +1 -0
  3. package/dist/lib.js +1 -0
  4. package/dist/lib.js.map +1 -1
  5. package/dist/skills/brain-debrief.md +47 -19
  6. package/dist/skills/brainstorming.md +19 -9
  7. package/dist/skills/code-patrol.md +21 -19
  8. package/dist/skills/context-resume.md +14 -11
  9. package/dist/skills/executing-plans.md +30 -15
  10. package/dist/skills/fix-and-learn.md +17 -14
  11. package/dist/skills/health-check.md +29 -23
  12. package/dist/skills/knowledge-harvest.md +27 -20
  13. package/dist/skills/onboard-me.md +16 -15
  14. package/dist/skills/retrospective.md +34 -18
  15. package/dist/skills/second-opinion.md +16 -9
  16. package/dist/skills/systematic-debugging.md +40 -29
  17. package/dist/skills/test-driven-development.md +45 -30
  18. package/dist/skills/vault-capture.md +31 -15
  19. package/dist/skills/vault-navigator.md +24 -13
  20. package/dist/skills/verification-before-completion.md +38 -26
  21. package/dist/skills/writing-plans.md +21 -13
  22. package/dist/templates/claude-md-template.d.ts +9 -8
  23. package/dist/templates/claude-md-template.js +29 -11
  24. package/dist/templates/claude-md-template.js.map +1 -1
  25. package/dist/templates/inject-claude-md.js +65 -25
  26. package/dist/templates/inject-claude-md.js.map +1 -1
  27. package/dist/templates/shared-rules.d.ts +10 -6
  28. package/dist/templates/shared-rules.js +242 -199
  29. package/dist/templates/shared-rules.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/lib.ts +1 -0
  32. package/src/templates/claude-md-template.ts +30 -12
  33. package/src/templates/inject-claude-md.ts +65 -25
  34. package/src/templates/shared-rules.ts +259 -210
  35. package/dist/skills/skills/brain-debrief.md +0 -214
  36. package/dist/skills/skills/brainstorming.md +0 -180
  37. package/dist/skills/skills/code-patrol.md +0 -178
  38. package/dist/skills/skills/context-resume.md +0 -146
  39. package/dist/skills/skills/executing-plans.md +0 -216
  40. package/dist/skills/skills/fix-and-learn.md +0 -167
  41. package/dist/skills/skills/health-check.md +0 -231
  42. package/dist/skills/skills/knowledge-harvest.md +0 -185
  43. package/dist/skills/skills/onboard-me.md +0 -198
  44. package/dist/skills/skills/retrospective.md +0 -205
  45. package/dist/skills/skills/second-opinion.md +0 -149
  46. package/dist/skills/skills/systematic-debugging.md +0 -241
  47. package/dist/skills/skills/test-driven-development.md +0 -281
  48. package/dist/skills/skills/vault-capture.md +0 -170
  49. package/dist/skills/skills/vault-navigator.md +0 -140
  50. package/dist/skills/skills/verification-before-completion.md +0 -182
  51. package/dist/skills/skills/writing-plans.md +0 -215
  52. package/dist/templates/brain.d.ts +0 -6
  53. package/dist/templates/brain.js +0 -478
  54. package/dist/templates/brain.js.map +0 -1
  55. package/dist/templates/core-facade.d.ts +0 -6
  56. package/dist/templates/core-facade.js +0 -564
  57. package/dist/templates/core-facade.js.map +0 -1
  58. package/dist/templates/facade-factory.d.ts +0 -1
  59. package/dist/templates/facade-factory.js +0 -63
  60. package/dist/templates/facade-factory.js.map +0 -1
  61. package/dist/templates/facade-types.d.ts +0 -1
  62. package/dist/templates/facade-types.js +0 -46
  63. package/dist/templates/facade-types.js.map +0 -1
  64. package/dist/templates/intelligence-loader.d.ts +0 -1
  65. package/dist/templates/intelligence-loader.js +0 -43
  66. package/dist/templates/intelligence-loader.js.map +0 -1
  67. package/dist/templates/intelligence-types.d.ts +0 -1
  68. package/dist/templates/intelligence-types.js +0 -24
  69. package/dist/templates/intelligence-types.js.map +0 -1
  70. package/dist/templates/llm-client.d.ts +0 -7
  71. package/dist/templates/llm-client.js +0 -300
  72. package/dist/templates/llm-client.js.map +0 -1
  73. package/dist/templates/llm-key-pool.d.ts +0 -7
  74. package/dist/templates/llm-key-pool.js +0 -211
  75. package/dist/templates/llm-key-pool.js.map +0 -1
  76. package/dist/templates/llm-types.d.ts +0 -5
  77. package/dist/templates/llm-types.js +0 -161
  78. package/dist/templates/llm-types.js.map +0 -1
  79. package/dist/templates/llm-utils.d.ts +0 -5
  80. package/dist/templates/llm-utils.js +0 -260
  81. package/dist/templates/llm-utils.js.map +0 -1
  82. package/dist/templates/planner.d.ts +0 -5
  83. package/dist/templates/planner.js +0 -150
  84. package/dist/templates/planner.js.map +0 -1
  85. package/dist/templates/test-brain.d.ts +0 -6
  86. package/dist/templates/test-brain.js +0 -474
  87. package/dist/templates/test-brain.js.map +0 -1
  88. package/dist/templates/test-llm.d.ts +0 -7
  89. package/dist/templates/test-llm.js +0 -574
  90. package/dist/templates/test-llm.js.map +0 -1
  91. package/dist/templates/test-loader.d.ts +0 -5
  92. package/dist/templates/test-loader.js +0 -146
  93. package/dist/templates/test-loader.js.map +0 -1
  94. package/dist/templates/test-planner.d.ts +0 -5
  95. package/dist/templates/test-planner.js +0 -271
  96. package/dist/templates/test-planner.js.map +0 -1
  97. package/dist/templates/test-vault.d.ts +0 -5
  98. package/dist/templates/test-vault.js +0 -380
  99. package/dist/templates/test-vault.js.map +0 -1
  100. package/dist/templates/vault.d.ts +0 -5
  101. package/dist/templates/vault.js +0 -263
  102. package/dist/templates/vault.js.map +0 -1
@@ -1,574 +0,0 @@
1
- /**
2
- * Generate the LLM test suite for a new agent.
3
- * Tests: SecretString, CircuitBreaker, retry, rate-limit parsing,
4
- * KeyPool rotation, ModelRouter resolution, LLMClient availability.
5
- */
6
- export function generateLLMTest(_config) {
7
- return `import { describe, it, expect, vi, beforeEach } from 'vitest';
8
- import {
9
- SecretString,
10
- LLMError,
11
- } from '../llm/types.js';
12
- import {
13
- CircuitBreaker,
14
- CircuitOpenError,
15
- computeDelay,
16
- retry,
17
- parseRateLimitHeaders,
18
- } from '../llm/utils.js';
19
- import {
20
- KeyPool,
21
- loadKeyPoolConfig,
22
- } from '../llm/key-pool.js';
23
- import {
24
- LLMClient,
25
- } from '../llm/llm-client.js';
26
-
27
- // =============================================================================
28
- // SecretString
29
- // =============================================================================
30
-
31
- describe('SecretString', () => {
32
- it('should expose the original value', () => {
33
- const s = new SecretString('my-secret-key');
34
- expect(s.expose()).toBe('my-secret-key');
35
- });
36
-
37
- it('should redact on toString', () => {
38
- const s = new SecretString('sk-123456');
39
- expect(String(s)).toBe('[REDACTED]');
40
- expect(\`\${s}\`).toBe('[REDACTED]');
41
- });
42
-
43
- it('should redact on JSON.stringify', () => {
44
- const s = new SecretString('sk-123456');
45
- const json = JSON.stringify({ key: s });
46
- expect(json).toContain('[REDACTED]');
47
- expect(json).not.toContain('sk-123456');
48
- });
49
-
50
- it('should report isSet correctly', () => {
51
- expect(new SecretString('value').isSet).toBe(true);
52
- expect(new SecretString('').isSet).toBe(false);
53
- });
54
-
55
- it('should redact in console.log via inspect', () => {
56
- const s = new SecretString('sk-secret');
57
- const inspectKey = Symbol.for('nodejs.util.inspect.custom');
58
- expect((s as any)[inspectKey]()).toBe('[REDACTED]');
59
- });
60
- });
61
-
62
- // =============================================================================
63
- // LLMError
64
- // =============================================================================
65
-
66
- describe('LLMError', () => {
67
- it('should create with default non-retryable', () => {
68
- const err = new LLMError('test error');
69
- expect(err.message).toBe('test error');
70
- expect(err.retryable).toBe(false);
71
- expect(err.name).toBe('LLMError');
72
- });
73
-
74
- it('should create retryable error', () => {
75
- const err = new LLMError('rate limited', { retryable: true, statusCode: 429 });
76
- expect(err.retryable).toBe(true);
77
- expect(err.statusCode).toBe(429);
78
- });
79
-
80
- it('should be instanceof Error', () => {
81
- const err = new LLMError('test');
82
- expect(err).toBeInstanceOf(Error);
83
- expect(err).toBeInstanceOf(LLMError);
84
- });
85
- });
86
-
87
- // =============================================================================
88
- // CircuitBreaker
89
- // =============================================================================
90
-
91
- describe('CircuitBreaker', () => {
92
- it('should start in closed state', () => {
93
- const cb = new CircuitBreaker({ name: 'test' });
94
- const state = cb.getState();
95
- expect(state.state).toBe('closed');
96
- expect(state.failureCount).toBe(0);
97
- });
98
-
99
- it('should pass through successful calls', async () => {
100
- const cb = new CircuitBreaker({ name: 'test' });
101
- const result = await cb.call(async () => 'ok');
102
- expect(result).toBe('ok');
103
- expect(cb.getState().state).toBe('closed');
104
- });
105
-
106
- it('should count retryable failures', async () => {
107
- const cb = new CircuitBreaker({ name: 'test', failureThreshold: 3 });
108
- const retryableError = new LLMError('fail', { retryable: true });
109
-
110
- for (let i = 0; i < 2; i++) {
111
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
112
- }
113
- expect(cb.getState().failureCount).toBe(2);
114
- expect(cb.getState().state).toBe('closed');
115
- });
116
-
117
- it('should open after reaching failure threshold', async () => {
118
- const cb = new CircuitBreaker({ name: 'test', failureThreshold: 3, resetTimeoutMs: 60_000 });
119
- const retryableError = new LLMError('fail', { retryable: true });
120
-
121
- for (let i = 0; i < 3; i++) {
122
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
123
- }
124
- expect(cb.getState().state).toBe('open');
125
- });
126
-
127
- it('should reject calls when open', async () => {
128
- const cb = new CircuitBreaker({ name: 'test-reject', failureThreshold: 1, resetTimeoutMs: 60_000 });
129
- const retryableError = new LLMError('fail', { retryable: true });
130
-
131
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
132
- await expect(cb.call(async () => 'ok')).rejects.toThrow(CircuitOpenError);
133
- });
134
-
135
- it('should not count non-retryable failures', async () => {
136
- const cb = new CircuitBreaker({ name: 'test', failureThreshold: 3 });
137
- const nonRetryable = new LLMError('auth fail', { retryable: false });
138
-
139
- for (let i = 0; i < 5; i++) {
140
- await expect(cb.call(async () => { throw nonRetryable; })).rejects.toThrow();
141
- }
142
- expect(cb.getState().state).toBe('closed');
143
- expect(cb.getState().failureCount).toBe(0);
144
- });
145
-
146
- it('should reset to closed on success', async () => {
147
- const cb = new CircuitBreaker({ name: 'test', failureThreshold: 3 });
148
- const retryableError = new LLMError('fail', { retryable: true });
149
-
150
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
151
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
152
- expect(cb.getState().failureCount).toBe(2);
153
-
154
- await cb.call(async () => 'recovered');
155
- expect(cb.getState().failureCount).toBe(0);
156
- expect(cb.getState().state).toBe('closed');
157
- });
158
-
159
- it('should trip synchronously via recordFailure', () => {
160
- const cb = new CircuitBreaker({ name: 'sync-trip', failureThreshold: 2 });
161
- cb.recordFailure();
162
- expect(cb.getState().state).toBe('closed');
163
- expect(cb.getState().failureCount).toBe(1);
164
- cb.recordFailure();
165
- expect(cb.getState().state).toBe('open');
166
- expect(cb.getState().failureCount).toBe(2);
167
- });
168
-
169
- it('should transition to half-open after timeout', async () => {
170
- const cb = new CircuitBreaker({ name: 'test-halfopen', failureThreshold: 1, resetTimeoutMs: 10 });
171
- const retryableError = new LLMError('fail', { retryable: true });
172
-
173
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
174
- expect(cb.getState().state).toBe('open');
175
-
176
- // Wait for reset timeout
177
- await new Promise((r) => setTimeout(r, 20));
178
-
179
- // getState auto-transitions to half-open
180
- expect(cb.getState().state).toBe('half-open');
181
- });
182
-
183
- it('should close on successful probe in half-open', async () => {
184
- const cb = new CircuitBreaker({ name: 'test-probe', failureThreshold: 1, resetTimeoutMs: 10 });
185
- const retryableError = new LLMError('fail', { retryable: true });
186
-
187
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
188
- await new Promise((r) => setTimeout(r, 20));
189
-
190
- const result = await cb.call(async () => 'probe-success');
191
- expect(result).toBe('probe-success');
192
- expect(cb.getState().state).toBe('closed');
193
- });
194
-
195
- it('should re-open on failed probe in half-open', async () => {
196
- const cb = new CircuitBreaker({ name: 'test-reprobe', failureThreshold: 1, resetTimeoutMs: 10 });
197
- const retryableError = new LLMError('fail', { retryable: true });
198
-
199
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
200
- await new Promise((r) => setTimeout(r, 20));
201
-
202
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
203
- expect(cb.getState().state).toBe('open');
204
- });
205
-
206
- it('should support manual reset', async () => {
207
- const cb = new CircuitBreaker({ name: 'test-reset', failureThreshold: 1, resetTimeoutMs: 60_000 });
208
- const retryableError = new LLMError('fail', { retryable: true });
209
-
210
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
211
- expect(cb.getState().state).toBe('open');
212
-
213
- cb.reset();
214
- expect(cb.getState().state).toBe('closed');
215
- expect(cb.getState().failureCount).toBe(0);
216
- });
217
-
218
- it('isOpen should report correctly', async () => {
219
- const cb = new CircuitBreaker({ name: 'test-isopen', failureThreshold: 1, resetTimeoutMs: 60_000 });
220
- expect(cb.isOpen()).toBe(false);
221
-
222
- const retryableError = new LLMError('fail', { retryable: true });
223
- await expect(cb.call(async () => { throw retryableError; })).rejects.toThrow();
224
- expect(cb.isOpen()).toBe(true);
225
- });
226
- });
227
-
228
- // =============================================================================
229
- // Retry
230
- // =============================================================================
231
-
232
- describe('retry', () => {
233
- it('should return on first success', async () => {
234
- const result = await retry(async () => 'ok', { maxAttempts: 3 });
235
- expect(result).toBe('ok');
236
- });
237
-
238
- it('should retry on retryable errors', async () => {
239
- let attempts = 0;
240
- const result = await retry(
241
- async () => {
242
- attempts++;
243
- if (attempts < 3) {
244
- const err = new LLMError('fail', { retryable: true });
245
- throw err;
246
- }
247
- return 'ok';
248
- },
249
- { maxAttempts: 3, baseDelayMs: 1, maxDelayMs: 10, shouldRetry: (e) => (e as any).retryable === true },
250
- );
251
- expect(result).toBe('ok');
252
- expect(attempts).toBe(3);
253
- });
254
-
255
- it('should not retry non-retryable errors', async () => {
256
- let attempts = 0;
257
- await expect(
258
- retry(
259
- async () => {
260
- attempts++;
261
- throw new LLMError('auth fail', { retryable: false });
262
- },
263
- { maxAttempts: 3, baseDelayMs: 1 },
264
- ),
265
- ).rejects.toThrow('auth fail');
266
- expect(attempts).toBe(1);
267
- });
268
-
269
- it('should throw last error after exhausting attempts', async () => {
270
- let attempts = 0;
271
- await expect(
272
- retry(
273
- async () => {
274
- attempts++;
275
- throw new LLMError(\`fail \${attempts}\`, { retryable: true });
276
- },
277
- { maxAttempts: 2, baseDelayMs: 1, maxDelayMs: 5, shouldRetry: () => true },
278
- ),
279
- ).rejects.toThrow('fail 2');
280
- expect(attempts).toBe(2);
281
- });
282
-
283
- it('should call onRetry callback', async () => {
284
- const retries: number[] = [];
285
- let attempts = 0;
286
- await retry(
287
- async () => {
288
- attempts++;
289
- if (attempts < 2) throw new LLMError('fail', { retryable: true });
290
- return 'ok';
291
- },
292
- {
293
- maxAttempts: 3,
294
- baseDelayMs: 1,
295
- maxDelayMs: 5,
296
- shouldRetry: () => true,
297
- onRetry: (_err, attempt) => retries.push(attempt),
298
- },
299
- );
300
- expect(retries).toEqual([1]);
301
- });
302
- });
303
-
304
- // =============================================================================
305
- // computeDelay
306
- // =============================================================================
307
-
308
- describe('computeDelay', () => {
309
- it('should compute exponential backoff', () => {
310
- const config = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, jitter: 0 };
311
- expect(computeDelay(null, 0, config)).toBe(1000);
312
- expect(computeDelay(null, 1, config)).toBe(2000);
313
- expect(computeDelay(null, 2, config)).toBe(4000);
314
- });
315
-
316
- it('should cap at maxDelayMs', () => {
317
- const config = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 5000, jitter: 0 };
318
- expect(computeDelay(null, 10, config)).toBe(5000);
319
- });
320
-
321
- it('should respect retryAfterMs from error', () => {
322
- const config = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, jitter: 0 };
323
- const error = { retryAfterMs: 2500 };
324
- expect(computeDelay(error, 0, config)).toBe(2500);
325
- });
326
-
327
- it('should apply jitter within range', () => {
328
- const config = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, jitter: 0.1 };
329
- const delays = new Set<number>();
330
- for (let i = 0; i < 20; i++) {
331
- delays.add(computeDelay(null, 0, config));
332
- }
333
- // With jitter, we should get some variation
334
- for (const d of delays) {
335
- expect(d).toBeGreaterThanOrEqual(900);
336
- expect(d).toBeLessThanOrEqual(1100);
337
- }
338
- });
339
- });
340
-
341
- // =============================================================================
342
- // parseRateLimitHeaders
343
- // =============================================================================
344
-
345
- describe('parseRateLimitHeaders', () => {
346
- it('should parse remaining requests header', () => {
347
- const headers = new Headers({
348
- 'x-ratelimit-remaining-requests': '42',
349
- });
350
- const info = parseRateLimitHeaders(headers);
351
- expect(info.remaining).toBe(42);
352
- });
353
-
354
- it('should parse reset duration (6m0s)', () => {
355
- const headers = new Headers({
356
- 'x-ratelimit-reset-requests': '6m0s',
357
- });
358
- const info = parseRateLimitHeaders(headers);
359
- expect(info.resetMs).toBe(360000);
360
- });
361
-
362
- it('should parse reset duration (200ms)', () => {
363
- const headers = new Headers({
364
- 'x-ratelimit-reset-requests': '200ms',
365
- });
366
- const info = parseRateLimitHeaders(headers);
367
- expect(info.resetMs).toBe(200);
368
- });
369
-
370
- it('should parse reset duration (1s)', () => {
371
- const headers = new Headers({
372
- 'x-ratelimit-reset-requests': '1s',
373
- });
374
- const info = parseRateLimitHeaders(headers);
375
- expect(info.resetMs).toBe(1000);
376
- });
377
-
378
- it('should parse retry-after header (seconds)', () => {
379
- const headers = new Headers({
380
- 'retry-after': '2.5',
381
- });
382
- const info = parseRateLimitHeaders(headers);
383
- expect(info.retryAfterMs).toBe(2500); // ceil(2.5 * 1000) = 2500
384
- });
385
-
386
- it('should return nulls for missing headers', () => {
387
- const headers = new Headers({});
388
- const info = parseRateLimitHeaders(headers);
389
- expect(info.remaining).toBeNull();
390
- expect(info.resetMs).toBeNull();
391
- expect(info.retryAfterMs).toBeNull();
392
- });
393
- });
394
-
395
- // =============================================================================
396
- // KeyPool
397
- // =============================================================================
398
-
399
- describe('KeyPool', () => {
400
- it('should initialize with keys', () => {
401
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2'] });
402
- expect(pool.hasKeys).toBe(true);
403
- expect(pool.poolSize).toBe(2);
404
- expect(pool.activeKeyIndex).toBe(0);
405
- });
406
-
407
- it('should filter empty keys', () => {
408
- const pool = new KeyPool({ keys: ['sk-1', '', 'sk-2'] });
409
- expect(pool.poolSize).toBe(2);
410
- });
411
-
412
- it('should report no keys when empty', () => {
413
- const pool = new KeyPool({ keys: [] });
414
- expect(pool.hasKeys).toBe(false);
415
- expect(pool.exhausted).toBe(true);
416
- });
417
-
418
- it('should return active key as SecretString', () => {
419
- const pool = new KeyPool({ keys: ['sk-test-key'] });
420
- const key = pool.getActiveKey();
421
- expect(key.expose()).toBe('sk-test-key');
422
- expect(String(key)).toBe('[REDACTED]');
423
- });
424
-
425
- it('should rotate on error', () => {
426
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2', 'sk-3'] });
427
- expect(pool.activeKeyIndex).toBe(0);
428
-
429
- const newKey = pool.rotateOnError();
430
- expect(newKey).not.toBeNull();
431
- expect(pool.activeKeyIndex).toBe(1);
432
- });
433
-
434
- it('should preemptively rotate when quota low', () => {
435
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2'], preemptiveThreshold: 50 });
436
- pool.updateQuota(0, 10); // below threshold
437
- const rotated = pool.rotatePreemptive();
438
- expect(rotated).toBe(true);
439
- expect(pool.activeKeyIndex).toBe(1);
440
- });
441
-
442
- it('should not rotate when quota above threshold', () => {
443
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2'], preemptiveThreshold: 50 });
444
- pool.updateQuota(0, 100);
445
- const rotated = pool.rotatePreemptive();
446
- expect(rotated).toBe(false);
447
- expect(pool.activeKeyIndex).toBe(0);
448
- });
449
-
450
- it('should report exhausted when all keys breakers are open', () => {
451
- const pool = new KeyPool({ keys: ['sk-1'] });
452
- expect(pool.exhausted).toBe(false);
453
-
454
- // Force 3 errors to trip the per-key breaker (threshold=3)
455
- pool.rotateOnError();
456
- pool.rotateOnError();
457
- pool.rotateOnError();
458
- expect(pool.exhausted).toBe(true);
459
- });
460
-
461
- it('should return status with per-key info', () => {
462
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2'] });
463
- pool.updateQuota(0, 100);
464
- const status = pool.getStatus();
465
- expect(status.poolSize).toBe(2);
466
- expect(status.activeKeyIndex).toBe(0);
467
- expect(status.exhausted).toBe(false);
468
- expect(status.perKeyStatus).toHaveLength(2);
469
- expect(status.perKeyStatus[0].remainingQuota).toBe(100);
470
- expect(status.perKeyStatus[1].remainingQuota).toBeNull();
471
- });
472
-
473
- it('should wrap around when rotating past last key', () => {
474
- const pool = new KeyPool({ keys: ['sk-1', 'sk-2'] });
475
- pool.rotateOnError(); // 0 → 1
476
- expect(pool.activeKeyIndex).toBe(1);
477
- pool.rotateOnError(); // 1 → 0
478
- expect(pool.activeKeyIndex).toBe(0);
479
- });
480
- });
481
-
482
- // =============================================================================
483
- // loadKeyPoolConfig
484
- // =============================================================================
485
-
486
- describe('loadKeyPoolConfig', () => {
487
- it('should return empty pools when no keys configured', () => {
488
- // Remove env vars temporarily
489
- const origOpenai = process.env.OPENAI_API_KEY;
490
- const origAnthropic = process.env.ANTHROPIC_API_KEY;
491
- delete process.env.OPENAI_API_KEY;
492
- delete process.env.ANTHROPIC_API_KEY;
493
-
494
- try {
495
- const config = loadKeyPoolConfig();
496
- // keys.json likely doesn't exist in test env, so falls back to empty
497
- expect(config.openai.keys).toBeDefined();
498
- expect(config.anthropic.keys).toBeDefined();
499
- } finally {
500
- if (origOpenai) process.env.OPENAI_API_KEY = origOpenai;
501
- if (origAnthropic) process.env.ANTHROPIC_API_KEY = origAnthropic;
502
- }
503
- });
504
- });
505
-
506
- // =============================================================================
507
- // LLMClient
508
- // =============================================================================
509
-
510
- describe('LLMClient', () => {
511
- it('should report availability correctly when no keys', () => {
512
- const openaiPool = new KeyPool({ keys: [] });
513
- const anthropicPool = new KeyPool({ keys: [] });
514
- const client = new LLMClient(openaiPool, anthropicPool);
515
-
516
- const status = client.isAvailable();
517
- expect(status.openai).toBe(false);
518
- expect(status.anthropic).toBe(false);
519
- });
520
-
521
- it('should report availability correctly when keys present', () => {
522
- const openaiPool = new KeyPool({ keys: ['sk-test'] });
523
- const anthropicPool = new KeyPool({ keys: ['sk-ant-test'] });
524
- const client = new LLMClient(openaiPool, anthropicPool);
525
-
526
- const status = client.isAvailable();
527
- expect(status.openai).toBe(true);
528
- expect(status.anthropic).toBe(true);
529
- });
530
-
531
- it('should return routes from router', () => {
532
- const openaiPool = new KeyPool({ keys: [] });
533
- const anthropicPool = new KeyPool({ keys: [] });
534
- const client = new LLMClient(openaiPool, anthropicPool);
535
-
536
- const routes = client.getRoutes();
537
- expect(Array.isArray(routes)).toBe(true);
538
- });
539
-
540
- it('should throw LLMError when no OpenAI key configured', async () => {
541
- const openaiPool = new KeyPool({ keys: [] });
542
- const anthropicPool = new KeyPool({ keys: [] });
543
- const client = new LLMClient(openaiPool, anthropicPool);
544
-
545
- await expect(
546
- client.complete({
547
- provider: 'openai',
548
- model: 'gpt-4o-mini',
549
- systemPrompt: 'test',
550
- userPrompt: 'test',
551
- caller: 'test',
552
- }),
553
- ).rejects.toThrow('OpenAI API key not configured');
554
- });
555
-
556
- it('should throw LLMError when no Anthropic key configured', async () => {
557
- const openaiPool = new KeyPool({ keys: [] });
558
- const anthropicPool = new KeyPool({ keys: [] });
559
- const client = new LLMClient(openaiPool, anthropicPool);
560
-
561
- await expect(
562
- client.complete({
563
- provider: 'anthropic',
564
- model: 'claude-sonnet-4-20250514',
565
- systemPrompt: 'test',
566
- userPrompt: 'test',
567
- caller: 'test',
568
- }),
569
- ).rejects.toThrow('Anthropic API key not configured');
570
- });
571
- });
572
- `;
573
- }
574
- //# sourceMappingURL=test-llm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-llm.js","sourceRoot":"","sources":["../../src/templates/test-llm.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAoB;IAClD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqjBR,CAAC;AACF,CAAC"}
@@ -1,5 +0,0 @@
1
- /**
2
- * Generates intelligence loader test file for a new agent.
3
- * Tests cover: load valid JSON bundles, skip invalid entries, missing dir, malformed JSON.
4
- */
5
- export declare function generateLoaderTest(): string;