@soleri/forge 0.0.1 → 4.0.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 (128) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +199 -0
  3. package/dist/facades/forge.facade.d.ts +9 -0
  4. package/dist/facades/forge.facade.js +134 -0
  5. package/dist/facades/forge.facade.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +81 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/knowledge-installer.d.ts +31 -0
  10. package/dist/knowledge-installer.js +437 -0
  11. package/dist/knowledge-installer.js.map +1 -0
  12. package/dist/scaffolder.d.ts +13 -0
  13. package/dist/scaffolder.js +330 -0
  14. package/dist/scaffolder.js.map +1 -0
  15. package/dist/templates/activate.d.ts +9 -0
  16. package/dist/templates/activate.js +139 -0
  17. package/dist/templates/activate.js.map +1 -0
  18. package/dist/templates/brain.d.ts +6 -0
  19. package/dist/templates/brain.js +478 -0
  20. package/dist/templates/brain.js.map +1 -0
  21. package/dist/templates/claude-md-template.d.ts +11 -0
  22. package/dist/templates/claude-md-template.js +73 -0
  23. package/dist/templates/claude-md-template.js.map +1 -0
  24. package/dist/templates/core-facade.d.ts +6 -0
  25. package/dist/templates/core-facade.js +456 -0
  26. package/dist/templates/core-facade.js.map +1 -0
  27. package/dist/templates/domain-facade.d.ts +7 -0
  28. package/dist/templates/domain-facade.js +119 -0
  29. package/dist/templates/domain-facade.js.map +1 -0
  30. package/dist/templates/entry-point.d.ts +5 -0
  31. package/dist/templates/entry-point.js +116 -0
  32. package/dist/templates/entry-point.js.map +1 -0
  33. package/dist/templates/facade-factory.d.ts +1 -0
  34. package/dist/templates/facade-factory.js +63 -0
  35. package/dist/templates/facade-factory.js.map +1 -0
  36. package/dist/templates/facade-types.d.ts +1 -0
  37. package/dist/templates/facade-types.js +46 -0
  38. package/dist/templates/facade-types.js.map +1 -0
  39. package/dist/templates/inject-claude-md.d.ts +11 -0
  40. package/dist/templates/inject-claude-md.js +92 -0
  41. package/dist/templates/inject-claude-md.js.map +1 -0
  42. package/dist/templates/intelligence-loader.d.ts +1 -0
  43. package/dist/templates/intelligence-loader.js +43 -0
  44. package/dist/templates/intelligence-loader.js.map +1 -0
  45. package/dist/templates/intelligence-types.d.ts +1 -0
  46. package/dist/templates/intelligence-types.js +24 -0
  47. package/dist/templates/intelligence-types.js.map +1 -0
  48. package/dist/templates/llm-client.d.ts +7 -0
  49. package/dist/templates/llm-client.js +300 -0
  50. package/dist/templates/llm-client.js.map +1 -0
  51. package/dist/templates/llm-key-pool.d.ts +7 -0
  52. package/dist/templates/llm-key-pool.js +211 -0
  53. package/dist/templates/llm-key-pool.js.map +1 -0
  54. package/dist/templates/llm-types.d.ts +5 -0
  55. package/dist/templates/llm-types.js +161 -0
  56. package/dist/templates/llm-types.js.map +1 -0
  57. package/dist/templates/llm-utils.d.ts +5 -0
  58. package/dist/templates/llm-utils.js +260 -0
  59. package/dist/templates/llm-utils.js.map +1 -0
  60. package/dist/templates/package-json.d.ts +2 -0
  61. package/dist/templates/package-json.js +37 -0
  62. package/dist/templates/package-json.js.map +1 -0
  63. package/dist/templates/persona.d.ts +2 -0
  64. package/dist/templates/persona.js +42 -0
  65. package/dist/templates/persona.js.map +1 -0
  66. package/dist/templates/planner.d.ts +5 -0
  67. package/dist/templates/planner.js +150 -0
  68. package/dist/templates/planner.js.map +1 -0
  69. package/dist/templates/readme.d.ts +5 -0
  70. package/dist/templates/readme.js +316 -0
  71. package/dist/templates/readme.js.map +1 -0
  72. package/dist/templates/setup-script.d.ts +6 -0
  73. package/dist/templates/setup-script.js +112 -0
  74. package/dist/templates/setup-script.js.map +1 -0
  75. package/dist/templates/test-brain.d.ts +6 -0
  76. package/dist/templates/test-brain.js +474 -0
  77. package/dist/templates/test-brain.js.map +1 -0
  78. package/dist/templates/test-facades.d.ts +6 -0
  79. package/dist/templates/test-facades.js +649 -0
  80. package/dist/templates/test-facades.js.map +1 -0
  81. package/dist/templates/test-llm.d.ts +7 -0
  82. package/dist/templates/test-llm.js +574 -0
  83. package/dist/templates/test-llm.js.map +1 -0
  84. package/dist/templates/test-loader.d.ts +5 -0
  85. package/dist/templates/test-loader.js +146 -0
  86. package/dist/templates/test-loader.js.map +1 -0
  87. package/dist/templates/test-planner.d.ts +5 -0
  88. package/dist/templates/test-planner.js +271 -0
  89. package/dist/templates/test-planner.js.map +1 -0
  90. package/dist/templates/test-vault.d.ts +5 -0
  91. package/dist/templates/test-vault.js +380 -0
  92. package/dist/templates/test-vault.js.map +1 -0
  93. package/dist/templates/tsconfig.d.ts +1 -0
  94. package/dist/templates/tsconfig.js +25 -0
  95. package/dist/templates/tsconfig.js.map +1 -0
  96. package/dist/templates/vault.d.ts +5 -0
  97. package/dist/templates/vault.js +263 -0
  98. package/dist/templates/vault.js.map +1 -0
  99. package/dist/templates/vitest-config.d.ts +1 -0
  100. package/dist/templates/vitest-config.js +27 -0
  101. package/dist/templates/vitest-config.js.map +1 -0
  102. package/dist/types.d.ts +89 -0
  103. package/dist/types.js +21 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +42 -4
  106. package/src/__tests__/knowledge-installer.test.ts +805 -0
  107. package/src/__tests__/scaffolder.test.ts +323 -0
  108. package/src/facades/forge.facade.ts +150 -0
  109. package/src/index.ts +101 -0
  110. package/src/knowledge-installer.ts +532 -0
  111. package/src/scaffolder.ts +386 -0
  112. package/src/templates/activate.ts +145 -0
  113. package/src/templates/claude-md-template.ts +137 -0
  114. package/src/templates/core-facade.ts +457 -0
  115. package/src/templates/domain-facade.ts +121 -0
  116. package/src/templates/entry-point.ts +120 -0
  117. package/src/templates/inject-claude-md.ts +94 -0
  118. package/src/templates/llm-client.ts +301 -0
  119. package/src/templates/package-json.ts +39 -0
  120. package/src/templates/persona.ts +45 -0
  121. package/src/templates/readme.ts +319 -0
  122. package/src/templates/setup-script.ts +113 -0
  123. package/src/templates/test-facades.ts +656 -0
  124. package/src/templates/tsconfig.ts +25 -0
  125. package/src/templates/vitest-config.ts +26 -0
  126. package/src/types.ts +68 -0
  127. package/tsconfig.json +21 -0
  128. package/vitest.config.ts +15 -0
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Generates facade integration tests for a new agent.
3
+ * Tests all domain facades and the core facade.
4
+ */
5
+ export function generateFacadesTest(config) {
6
+ const domainImports = config.domains
7
+ .map((d) => {
8
+ const fn = `create${pascalCase(d)}Facade`;
9
+ return `import { ${fn} } from '../facades/${d}.facade.js';`;
10
+ })
11
+ .join('\n');
12
+ const domainDescribes = config.domains
13
+ .map((d) => generateDomainDescribe(config.id, d))
14
+ .join('\n\n');
15
+ return `import { describe, it, expect, beforeEach, afterEach } from 'vitest';
16
+ import { Vault, Brain, Planner, KeyPool } from '@soleri/core';
17
+ import type { IntelligenceEntry } from '@soleri/core';
18
+ import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ ${domainImports}
22
+ import { createCoreFacade } from '../facades/core.facade.js';
23
+ import { LLMClient } from '../llm/llm-client.js';
24
+
25
+ function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
26
+ return {
27
+ id: overrides.id ?? 'test-1',
28
+ type: overrides.type ?? 'pattern',
29
+ domain: overrides.domain ?? 'testing',
30
+ title: overrides.title ?? 'Test Pattern',
31
+ severity: overrides.severity ?? 'warning',
32
+ description: overrides.description ?? 'A test pattern.',
33
+ tags: overrides.tags ?? ['testing'],
34
+ };
35
+ }
36
+
37
+ describe('Facades', () => {
38
+ let vault: Vault;
39
+ let brain: Brain;
40
+ let plannerDir: string;
41
+ let planner: Planner;
42
+ let openaiKeyPool: KeyPool;
43
+ let anthropicKeyPool: KeyPool;
44
+ let llmClient: LLMClient;
45
+
46
+ beforeEach(() => {
47
+ vault = new Vault(':memory:');
48
+ brain = new Brain(vault);
49
+ plannerDir = join(tmpdir(), 'forge-planner-test-' + Date.now());
50
+ mkdirSync(plannerDir, { recursive: true });
51
+ planner = new Planner(join(plannerDir, 'plans.json'));
52
+ openaiKeyPool = new KeyPool({ keys: [] });
53
+ anthropicKeyPool = new KeyPool({ keys: [] });
54
+ llmClient = new LLMClient(openaiKeyPool, anthropicKeyPool);
55
+ });
56
+
57
+ afterEach(() => {
58
+ vault.close();
59
+ rmSync(plannerDir, { recursive: true, force: true });
60
+ });
61
+
62
+ ${domainDescribes}
63
+
64
+ describe('${config.id}_core', () => {
65
+ it('should create core facade with expected ops', () => {
66
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
67
+ expect(facade.name).toBe('${config.id}_core');
68
+ const opNames = facade.ops.map((o) => o.name);
69
+ expect(opNames).toContain('search');
70
+ expect(opNames).toContain('vault_stats');
71
+ expect(opNames).toContain('list_all');
72
+ expect(opNames).toContain('health');
73
+ expect(opNames).toContain('identity');
74
+ expect(opNames).toContain('activate');
75
+ expect(opNames).toContain('inject_claude_md');
76
+ expect(opNames).toContain('setup');
77
+ expect(opNames).toContain('register');
78
+ expect(opNames).toContain('llm_status');
79
+ });
80
+
81
+ it('search should query across all domains with ranked results', async () => {
82
+ vault.seed([
83
+ makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
84
+ makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
85
+ ]);
86
+ brain = new Brain(vault);
87
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
88
+ const searchOp = facade.ops.find((o) => o.name === 'search')!;
89
+ const results = (await searchOp.handler({ query: 'pattern' })) as Array<{ entry: unknown; score: number; breakdown: unknown }>;
90
+ expect(Array.isArray(results)).toBe(true);
91
+ expect(results.length).toBe(2);
92
+ expect(results[0].score).toBeGreaterThan(0);
93
+ expect(results[0].breakdown).toBeDefined();
94
+ });
95
+
96
+ it('vault_stats should return counts', async () => {
97
+ vault.seed([
98
+ makeEntry({ id: 'vs1', domain: 'd1', tags: ['x'] }),
99
+ makeEntry({ id: 'vs2', domain: 'd2', tags: ['y'] }),
100
+ ]);
101
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
102
+ const statsOp = facade.ops.find((o) => o.name === 'vault_stats')!;
103
+ const stats = (await statsOp.handler({})) as { totalEntries: number };
104
+ expect(stats.totalEntries).toBe(2);
105
+ });
106
+
107
+ it('health should return ok status', async () => {
108
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
109
+ const healthOp = facade.ops.find((o) => o.name === 'health')!;
110
+ const health = (await healthOp.handler({})) as { status: string };
111
+ expect(health.status).toBe('ok');
112
+ });
113
+
114
+ it('identity should return persona', async () => {
115
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
116
+ const identityOp = facade.ops.find((o) => o.name === 'identity')!;
117
+ const persona = (await identityOp.handler({})) as { name: string; role: string };
118
+ expect(persona.name).toBe('${escapeQuotes(config.name)}');
119
+ expect(persona.role).toBe('${escapeQuotes(config.role)}');
120
+ });
121
+
122
+ it('list_all should support domain filter', async () => {
123
+ vault.seed([
124
+ makeEntry({ id: 'la1', domain: 'alpha', tags: ['a'] }),
125
+ makeEntry({ id: 'la2', domain: 'beta', tags: ['b'] }),
126
+ ]);
127
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
128
+ const listOp = facade.ops.find((o) => o.name === 'list_all')!;
129
+ const filtered = (await listOp.handler({ domain: 'alpha' })) as unknown[];
130
+ expect(filtered).toHaveLength(1);
131
+ });
132
+
133
+ it('activate should return persona and setup status', async () => {
134
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
135
+ const activateOp = facade.ops.find((o) => o.name === 'activate')!;
136
+ const result = (await activateOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
137
+ activated: boolean;
138
+ persona: { name: string; role: string };
139
+ guidelines: string[];
140
+ tool_recommendations: unknown[];
141
+ setup_status: { claude_md_injected: boolean; global_claude_md_injected: boolean; vault_has_entries: boolean };
142
+ };
143
+ expect(result.activated).toBe(true);
144
+ expect(result.persona.name).toBe('${escapeQuotes(config.name)}');
145
+ expect(result.persona.role).toBe('${escapeQuotes(config.role)}');
146
+ expect(result.guidelines.length).toBeGreaterThan(0);
147
+ expect(result.tool_recommendations.length).toBeGreaterThan(0);
148
+ expect(result.setup_status.claude_md_injected).toBe(false);
149
+ expect(typeof result.setup_status.global_claude_md_injected).toBe('boolean');
150
+ });
151
+
152
+ it('activate with deactivate flag should return deactivation', async () => {
153
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
154
+ const activateOp = facade.ops.find((o) => o.name === 'activate')!;
155
+ const result = (await activateOp.handler({ deactivate: true })) as { deactivated: boolean; message: string };
156
+ expect(result.deactivated).toBe(true);
157
+ expect(result.message).toBeDefined();
158
+ });
159
+
160
+ it('inject_claude_md should create CLAUDE.md in temp dir', async () => {
161
+ const tempDir = join(tmpdir(), 'forge-inject-test-' + Date.now());
162
+ mkdirSync(tempDir, { recursive: true });
163
+ try {
164
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
165
+ const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
166
+ const result = (await injectOp.handler({ projectPath: tempDir })) as {
167
+ injected: boolean;
168
+ path: string;
169
+ action: string;
170
+ };
171
+ expect(result.injected).toBe(true);
172
+ expect(result.action).toBe('created');
173
+ expect(existsSync(result.path)).toBe(true);
174
+ const content = readFileSync(result.path, 'utf-8');
175
+ expect(content).toContain('${config.id}:mode');
176
+ } finally {
177
+ rmSync(tempDir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ it('inject_claude_md with global flag should target ~/.claude/CLAUDE.md', async () => {
182
+ // We test the global path resolution by checking the returned path
183
+ // contains .claude/CLAUDE.md (actual write may or may not happen depending on env)
184
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
185
+ const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
186
+ const result = (await injectOp.handler({ global: true })) as {
187
+ injected: boolean;
188
+ path: string;
189
+ action: string;
190
+ };
191
+ expect(result.injected).toBe(true);
192
+ expect(result.path).toContain('.claude');
193
+ expect(result.path).toContain('CLAUDE.md');
194
+ });
195
+
196
+ it('setup should return project and global CLAUDE.md status', async () => {
197
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
198
+ const setupOp = facade.ops.find((o) => o.name === 'setup')!;
199
+ const result = (await setupOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
200
+ agent: { name: string };
201
+ claude_md: {
202
+ project: { exists: boolean; has_agent_section: boolean };
203
+ global: { exists: boolean; has_agent_section: boolean };
204
+ };
205
+ vault: { entries: number };
206
+ recommendations: string[];
207
+ };
208
+ expect(result.agent.name).toBe('${escapeQuotes(config.name)}');
209
+ expect(result.claude_md.project.exists).toBe(false);
210
+ expect(typeof result.claude_md.global.exists).toBe('boolean');
211
+ expect(result.vault.entries).toBe(0);
212
+ expect(result.recommendations.length).toBeGreaterThan(0);
213
+ });
214
+
215
+ it('register should track new project', async () => {
216
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
217
+ const registerOp = facade.ops.find((o) => o.name === 'register')!;
218
+ const result = (await registerOp.handler({ projectPath: '/tmp/reg-test-project', name: 'reg-test' })) as {
219
+ project: { path: string; name: string; sessionCount: number };
220
+ is_new: boolean;
221
+ message: string;
222
+ vault: { entries: number };
223
+ };
224
+ expect(result.is_new).toBe(true);
225
+ expect(result.project.name).toBe('reg-test');
226
+ expect(result.project.sessionCount).toBe(1);
227
+ expect(result.message).toContain('Welcome');
228
+ });
229
+
230
+ it('register should increment session count for returning project', async () => {
231
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
232
+ const registerOp = facade.ops.find((o) => o.name === 'register')!;
233
+ await registerOp.handler({ projectPath: '/tmp/reg-test-returning', name: 'returning' });
234
+ const result = (await registerOp.handler({ projectPath: '/tmp/reg-test-returning', name: 'returning' })) as {
235
+ project: { sessionCount: number };
236
+ is_new: boolean;
237
+ message: string;
238
+ };
239
+ expect(result.is_new).toBe(false);
240
+ expect(result.project.sessionCount).toBe(2);
241
+ expect(result.message).toContain('Welcome back');
242
+ });
243
+
244
+ it('memory_capture should store a memory', async () => {
245
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
246
+ const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
247
+ const result = (await captureOp.handler({
248
+ projectPath: '/test',
249
+ type: 'lesson',
250
+ context: 'Testing facades',
251
+ summary: 'Facade tests work great',
252
+ topics: ['testing'],
253
+ filesModified: [],
254
+ toolsUsed: [],
255
+ })) as { captured: boolean; memory: { id: string; type: string } };
256
+ expect(result.captured).toBe(true);
257
+ expect(result.memory.id).toMatch(/^mem-/);
258
+ expect(result.memory.type).toBe('lesson');
259
+ });
260
+
261
+ it('memory_search should find captured memories', async () => {
262
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
263
+ const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
264
+ await captureOp.handler({
265
+ projectPath: '/test',
266
+ type: 'lesson',
267
+ context: 'Database optimization',
268
+ summary: 'Learned about index strategies for PostgreSQL',
269
+ topics: ['database'],
270
+ filesModified: [],
271
+ toolsUsed: [],
272
+ });
273
+ const searchOp = facade.ops.find((o) => o.name === 'memory_search')!;
274
+ const results = (await searchOp.handler({ query: 'index strategies' })) as Array<{ summary: string }>;
275
+ expect(results.length).toBeGreaterThan(0);
276
+ expect(results[0].summary).toContain('index strategies');
277
+ });
278
+
279
+ it('memory_list should return all memories with stats', async () => {
280
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
281
+ const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
282
+ await captureOp.handler({
283
+ projectPath: '/test',
284
+ type: 'session',
285
+ context: 'ctx',
286
+ summary: 'Session summary',
287
+ topics: [],
288
+ filesModified: [],
289
+ toolsUsed: [],
290
+ });
291
+ const listOp = facade.ops.find((o) => o.name === 'memory_list')!;
292
+ const result = (await listOp.handler({})) as {
293
+ memories: unknown[];
294
+ stats: { total: number };
295
+ };
296
+ expect(result.memories).toHaveLength(1);
297
+ expect(result.stats.total).toBe(1);
298
+ });
299
+
300
+ it('session_capture should store a session memory', async () => {
301
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
302
+ const sessionOp = facade.ops.find((o) => o.name === 'session_capture')!;
303
+ const result = (await sessionOp.handler({
304
+ projectPath: '/tmp/test-session',
305
+ summary: 'Worked on vault memory system',
306
+ topics: ['vault', 'memory'],
307
+ filesModified: ['vault.ts'],
308
+ toolsUsed: ['Edit', 'Bash'],
309
+ })) as { captured: boolean; memory: { type: string; summary: string }; message: string };
310
+ expect(result.captured).toBe(true);
311
+ expect(result.memory.type).toBe('session');
312
+ expect(result.memory.summary).toBe('Worked on vault memory system');
313
+ expect(result.message).toContain('saved');
314
+ });
315
+
316
+ it('export should return vault entries as bundles', async () => {
317
+ vault.seed([
318
+ makeEntry({ id: 'exp1', domain: 'security', tags: ['auth'] }),
319
+ makeEntry({ id: 'exp2', domain: 'security', tags: ['xss'] }),
320
+ makeEntry({ id: 'exp3', domain: 'api-design', tags: ['rest'] }),
321
+ ]);
322
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
323
+ const exportOp = facade.ops.find((o) => o.name === 'export')!;
324
+ const result = (await exportOp.handler({})) as {
325
+ exported: boolean;
326
+ bundles: Array<{ domain: string; entries: unknown[] }>;
327
+ totalEntries: number;
328
+ domains: string[];
329
+ };
330
+ expect(result.exported).toBe(true);
331
+ expect(result.totalEntries).toBe(3);
332
+ expect(result.domains).toContain('security');
333
+ expect(result.domains).toContain('api-design');
334
+ expect(result.bundles.find((b) => b.domain === 'security')!.entries).toHaveLength(2);
335
+ });
336
+
337
+ it('export should filter by domain', async () => {
338
+ vault.seed([
339
+ makeEntry({ id: 'exp-d1', domain: 'security', tags: ['auth'] }),
340
+ makeEntry({ id: 'exp-d2', domain: 'api-design', tags: ['rest'] }),
341
+ ]);
342
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
343
+ const exportOp = facade.ops.find((o) => o.name === 'export')!;
344
+ const result = (await exportOp.handler({ domain: 'security' })) as {
345
+ bundles: Array<{ domain: string; entries: unknown[] }>;
346
+ totalEntries: number;
347
+ };
348
+ expect(result.totalEntries).toBe(1);
349
+ expect(result.bundles).toHaveLength(1);
350
+ expect(result.bundles[0].domain).toBe('security');
351
+ });
352
+
353
+ it('create_plan should create a draft plan', async () => {
354
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
355
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
356
+ const result = (await createOp.handler({
357
+ objective: 'Add caching',
358
+ scope: 'api layer',
359
+ tasks: [{ title: 'Add Redis', description: 'Set up Redis client' }],
360
+ })) as { created: boolean; plan: { id: string; status: string; tasks: unknown[] } };
361
+ expect(result.created).toBe(true);
362
+ expect(result.plan.status).toBe('draft');
363
+ expect(result.plan.tasks).toHaveLength(1);
364
+ });
365
+
366
+ it('get_plan should return a plan by id', async () => {
367
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
368
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
369
+ const created = (await createOp.handler({ objective: 'Test', scope: 'test' })) as { plan: { id: string } };
370
+ const getOp = facade.ops.find((o) => o.name === 'get_plan')!;
371
+ const result = (await getOp.handler({ planId: created.plan.id })) as { id: string; objective: string };
372
+ expect(result.id).toBe(created.plan.id);
373
+ expect(result.objective).toBe('Test');
374
+ });
375
+
376
+ it('get_plan without id should return active plans', async () => {
377
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
378
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
379
+ await createOp.handler({ objective: 'Plan A', scope: 'a' });
380
+ const getOp = facade.ops.find((o) => o.name === 'get_plan')!;
381
+ const result = (await getOp.handler({})) as { active: unknown[]; executing: unknown[] };
382
+ expect(result.active).toHaveLength(1);
383
+ expect(result.executing).toHaveLength(0);
384
+ });
385
+
386
+ it('approve_plan should approve and optionally start execution', async () => {
387
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
388
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
389
+ const created = (await createOp.handler({ objective: 'Approve', scope: 'test' })) as { plan: { id: string } };
390
+ const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
391
+ const result = (await approveOp.handler({ planId: created.plan.id, startExecution: true })) as {
392
+ approved: boolean;
393
+ executing: boolean;
394
+ plan: { status: string };
395
+ };
396
+ expect(result.approved).toBe(true);
397
+ expect(result.executing).toBe(true);
398
+ expect(result.plan.status).toBe('executing');
399
+ });
400
+
401
+ it('update_task should update task status', async () => {
402
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
403
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
404
+ const created = (await createOp.handler({
405
+ objective: 'Task update',
406
+ scope: 'test',
407
+ tasks: [{ title: 'T1', description: 'Do thing' }],
408
+ })) as { plan: { id: string } };
409
+ const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
410
+ await approveOp.handler({ planId: created.plan.id, startExecution: true });
411
+ const updateOp = facade.ops.find((o) => o.name === 'update_task')!;
412
+ const result = (await updateOp.handler({
413
+ planId: created.plan.id,
414
+ taskId: 'task-1',
415
+ status: 'completed',
416
+ })) as { updated: boolean; task: { status: string } };
417
+ expect(result.updated).toBe(true);
418
+ expect(result.task!.status).toBe('completed');
419
+ });
420
+
421
+ it('complete_plan should complete with task summary', async () => {
422
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
423
+ const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
424
+ const created = (await createOp.handler({
425
+ objective: 'Complete me',
426
+ scope: 'test',
427
+ tasks: [
428
+ { title: 'T1', description: 'Do thing' },
429
+ { title: 'T2', description: 'Another' },
430
+ ],
431
+ })) as { plan: { id: string } };
432
+ const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
433
+ await approveOp.handler({ planId: created.plan.id, startExecution: true });
434
+ const updateOp = facade.ops.find((o) => o.name === 'update_task')!;
435
+ await updateOp.handler({ planId: created.plan.id, taskId: 'task-1', status: 'completed' });
436
+ await updateOp.handler({ planId: created.plan.id, taskId: 'task-2', status: 'skipped' });
437
+ const completeOp = facade.ops.find((o) => o.name === 'complete_plan')!;
438
+ const result = (await completeOp.handler({ planId: created.plan.id })) as {
439
+ completed: boolean;
440
+ taskSummary: { completed: number; skipped: number; total: number };
441
+ };
442
+ expect(result.completed).toBe(true);
443
+ expect(result.taskSummary.completed).toBe(1);
444
+ expect(result.taskSummary.skipped).toBe(1);
445
+ expect(result.taskSummary.total).toBe(2);
446
+ });
447
+
448
+ it('should have brain ops in core facade', () => {
449
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
450
+ const opNames = facade.ops.map((o) => o.name);
451
+ expect(opNames).toContain('record_feedback');
452
+ expect(opNames).toContain('rebuild_vocabulary');
453
+ expect(opNames).toContain('brain_stats');
454
+ });
455
+
456
+ it('record_feedback should record search feedback', async () => {
457
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
458
+ const feedbackOp = facade.ops.find((o) => o.name === 'record_feedback')!;
459
+ const result = (await feedbackOp.handler({
460
+ query: 'test query',
461
+ entryId: 'test-entry',
462
+ action: 'accepted',
463
+ })) as { recorded: boolean; query: string; action: string };
464
+ expect(result.recorded).toBe(true);
465
+ expect(result.query).toBe('test query');
466
+ expect(result.action).toBe('accepted');
467
+ });
468
+
469
+ it('rebuild_vocabulary should rebuild and return size', async () => {
470
+ vault.seed([
471
+ makeEntry({ id: 'rv1', title: 'Rebuild vocab test', description: 'Testing vocabulary rebuild.', tags: ['rebuild'] }),
472
+ ]);
473
+ brain = new Brain(vault);
474
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
475
+ const rebuildOp = facade.ops.find((o) => o.name === 'rebuild_vocabulary')!;
476
+ const result = (await rebuildOp.handler({})) as { rebuilt: boolean; vocabularySize: number };
477
+ expect(result.rebuilt).toBe(true);
478
+ expect(result.vocabularySize).toBeGreaterThan(0);
479
+ });
480
+
481
+ it('brain_stats should return intelligence stats', async () => {
482
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
483
+ const statsOp = facade.ops.find((o) => o.name === 'brain_stats')!;
484
+ const result = (await statsOp.handler({})) as {
485
+ vocabularySize: number;
486
+ feedbackCount: number;
487
+ weights: { semantic: number; severity: number; recency: number; tagOverlap: number; domainMatch: number };
488
+ };
489
+ expect(result.vocabularySize).toBe(0);
490
+ expect(result.feedbackCount).toBe(0);
491
+ expect(result.weights.semantic).toBeCloseTo(0.40, 2);
492
+ expect(result.weights.severity).toBeCloseTo(0.15, 2);
493
+ });
494
+
495
+ it('brain_stats should reflect feedback count after recording', async () => {
496
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
497
+ const feedbackOp = facade.ops.find((o) => o.name === 'record_feedback')!;
498
+ await feedbackOp.handler({ query: 'q1', entryId: 'e1', action: 'accepted' });
499
+ await feedbackOp.handler({ query: 'q2', entryId: 'e2', action: 'dismissed' });
500
+ const statsOp = facade.ops.find((o) => o.name === 'brain_stats')!;
501
+ const result = (await statsOp.handler({})) as { feedbackCount: number };
502
+ expect(result.feedbackCount).toBe(2);
503
+ });
504
+
505
+ it('llm_status should return provider availability', async () => {
506
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
507
+ const llmStatusOp = facade.ops.find((o) => o.name === 'llm_status')!;
508
+ const result = (await llmStatusOp.handler({})) as {
509
+ providers: {
510
+ openai: { available: boolean; keyPool: unknown };
511
+ anthropic: { available: boolean; keyPool: unknown };
512
+ };
513
+ routes: unknown[];
514
+ };
515
+ expect(result.providers.openai.available).toBe(false);
516
+ expect(result.providers.anthropic.available).toBe(false);
517
+ expect(result.providers.openai.keyPool).toBeNull();
518
+ expect(Array.isArray(result.routes)).toBe(true);
519
+ });
520
+
521
+ it('llm_status should reflect key pool when keys present', async () => {
522
+ const oPool = new KeyPool({ keys: ['sk-test'] });
523
+ const aPool = new KeyPool({ keys: ['sk-ant-test'] });
524
+ const client = new LLMClient(oPool, aPool);
525
+ const facade = createCoreFacade(vault, planner, brain, client, oPool, aPool);
526
+ const llmStatusOp = facade.ops.find((o) => o.name === 'llm_status')!;
527
+ const result = (await llmStatusOp.handler({})) as {
528
+ providers: {
529
+ openai: { available: boolean; keyPool: { poolSize: number } };
530
+ anthropic: { available: boolean; keyPool: { poolSize: number } };
531
+ };
532
+ };
533
+ expect(result.providers.openai.available).toBe(true);
534
+ expect(result.providers.openai.keyPool.poolSize).toBe(1);
535
+ expect(result.providers.anthropic.available).toBe(true);
536
+ expect(result.providers.anthropic.keyPool.poolSize).toBe(1);
537
+ });
538
+
539
+ it('search through brain should return ranked results with breakdowns', async () => {
540
+ vault.seed([
541
+ makeEntry({ id: 'bs-1', domain: 'security', title: 'Authentication token handling', severity: 'critical', description: 'Always validate JWT tokens.', tags: ['auth', 'jwt'] }),
542
+ ]);
543
+ brain = new Brain(vault);
544
+ const facade = createCoreFacade(vault, planner, brain, llmClient, openaiKeyPool, anthropicKeyPool);
545
+ const searchOp = facade.ops.find((o) => o.name === 'search')!;
546
+ const results = (await searchOp.handler({ query: 'authentication token' })) as Array<{ entry: { id: string }; score: number; breakdown: { semantic: number; total: number } }>;
547
+ expect(results.length).toBeGreaterThan(0);
548
+ expect(results[0].score).toBeGreaterThan(0);
549
+ expect(results[0].breakdown.total).toBe(results[0].score);
550
+ });
551
+ });
552
+ });
553
+ `;
554
+ }
555
+ function generateDomainDescribe(agentId, domain) {
556
+ const facadeName = `${agentId}_${domain.replace(/-/g, '_')}`;
557
+ const factoryFn = `create${pascalCase(domain)}Facade`;
558
+ return ` describe('${facadeName}', () => {
559
+ it('should create facade with expected ops', () => {
560
+ const facade = ${factoryFn}(vault, brain);
561
+ expect(facade.name).toBe('${facadeName}');
562
+ const opNames = facade.ops.map((o) => o.name);
563
+ expect(opNames).toContain('get_patterns');
564
+ expect(opNames).toContain('search');
565
+ expect(opNames).toContain('get_entry');
566
+ expect(opNames).toContain('capture');
567
+ expect(opNames).toContain('remove');
568
+ });
569
+
570
+ it('get_patterns should return entries for ${domain}', async () => {
571
+ vault.seed([
572
+ makeEntry({ id: '${domain}-gp1', domain: '${domain}', tags: ['test'] }),
573
+ makeEntry({ id: 'other-gp1', domain: 'other-domain', tags: ['test'] }),
574
+ ]);
575
+ const facade = ${factoryFn}(vault, brain);
576
+ const op = facade.ops.find((o) => o.name === 'get_patterns')!;
577
+ const results = (await op.handler({})) as IntelligenceEntry[];
578
+ expect(results.every((e) => e.domain === '${domain}')).toBe(true);
579
+ });
580
+
581
+ it('search should scope to ${domain} with ranked results', async () => {
582
+ vault.seed([
583
+ makeEntry({ id: '${domain}-s1', domain: '${domain}', title: 'Domain specific pattern', tags: ['find-me'] }),
584
+ makeEntry({ id: 'other-s1', domain: 'other', title: 'Other domain pattern', tags: ['nope'] }),
585
+ ]);
586
+ brain = new Brain(vault);
587
+ const facade = ${factoryFn}(vault, brain);
588
+ const op = facade.ops.find((o) => o.name === 'search')!;
589
+ const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number; breakdown: unknown }>;
590
+ expect(results.every((r) => r.entry.domain === '${domain}')).toBe(true);
591
+ if (results.length > 0) {
592
+ expect(results[0].score).toBeGreaterThan(0);
593
+ expect(results[0].breakdown).toBeDefined();
594
+ }
595
+ });
596
+
597
+ it('capture should add entry with ${domain} domain and auto-tags', async () => {
598
+ const facade = ${factoryFn}(vault, brain);
599
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
600
+ const result = (await captureOp.handler({
601
+ id: '${domain}-cap1',
602
+ type: 'pattern',
603
+ title: 'Captured Pattern for Intelligence',
604
+ severity: 'warning',
605
+ description: 'A captured pattern for testing the brain intelligence layer.',
606
+ tags: ['captured'],
607
+ })) as { captured: boolean; autoTags: string[] };
608
+ expect(result.captured).toBe(true);
609
+ expect(result.autoTags).toBeDefined();
610
+ const entry = vault.get('${domain}-cap1');
611
+ expect(entry).not.toBeNull();
612
+ expect(entry!.domain).toBe('${domain}');
613
+ });
614
+
615
+ it('get_entry should return specific entry', async () => {
616
+ vault.seed([makeEntry({ id: '${domain}-ge1', domain: '${domain}', tags: ['test'] })]);
617
+ const facade = ${factoryFn}(vault, brain);
618
+ const op = facade.ops.find((o) => o.name === 'get_entry')!;
619
+ const result = (await op.handler({ id: '${domain}-ge1' })) as IntelligenceEntry;
620
+ expect(result.id).toBe('${domain}-ge1');
621
+ });
622
+
623
+ it('get_entry should return error for missing entry', async () => {
624
+ const facade = ${factoryFn}(vault, brain);
625
+ const op = facade.ops.find((o) => o.name === 'get_entry')!;
626
+ const result = (await op.handler({ id: 'nonexistent' })) as { error: string };
627
+ expect(result.error).toBeDefined();
628
+ });
629
+
630
+ it('remove should delete entry', async () => {
631
+ vault.seed([makeEntry({ id: '${domain}-rm1', domain: '${domain}', tags: ['test'] })]);
632
+ const facade = ${factoryFn}(vault, brain);
633
+ const op = facade.ops.find((o) => o.name === 'remove')!;
634
+ const result = (await op.handler({ id: '${domain}-rm1' })) as { removed: boolean };
635
+ expect(result.removed).toBe(true);
636
+ expect(vault.get('${domain}-rm1')).toBeNull();
637
+ });
638
+ });`;
639
+ }
640
+ function pascalCase(s) {
641
+ return s
642
+ .split(/[-_\s]+/)
643
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
644
+ .join('');
645
+ }
646
+ function escapeQuotes(s) {
647
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
648
+ }
649
+ //# sourceMappingURL=test-facades.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-facades.js","sourceRoot":"","sources":["../../src/templates/test-facades.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { AgentConfig } from '../types.js';
2
+ /**
3
+ * Generate the LLM test suite for a new agent.
4
+ * Tests: SecretString, CircuitBreaker, retry, rate-limit parsing,
5
+ * KeyPool rotation, ModelRouter resolution, LLMClient availability.
6
+ */
7
+ export declare function generateLLMTest(_config: AgentConfig): string;