onbuzz 3.6.1 → 3.6.2

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 (83) hide show
  1. package/package.json +1 -1
  2. package/src/__test-utils__/fixtures/malformedJson.js +31 -0
  3. package/src/__test-utils__/globalSetup.js +9 -0
  4. package/src/__test-utils__/globalTeardown.js +12 -0
  5. package/src/__test-utils__/mockFactories.js +101 -0
  6. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
  7. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
  8. package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
  9. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
  10. package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
  11. package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
  12. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
  13. package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
  14. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
  15. package/src/core/__tests__/agentPool.test.js +601 -0
  16. package/src/core/__tests__/agentScheduler.test.js +576 -0
  17. package/src/core/__tests__/contextManager.test.js +252 -0
  18. package/src/core/__tests__/flowExecutor.test.js +262 -0
  19. package/src/core/__tests__/messageProcessor.test.js +627 -0
  20. package/src/core/__tests__/orchestrator.test.js +257 -0
  21. package/src/core/__tests__/stateManager.test.js +375 -0
  22. package/src/core/agentPool.js +11 -1
  23. package/src/index.js +25 -9
  24. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
  25. package/src/services/__tests__/agentActivityService.test.js +319 -0
  26. package/src/services/__tests__/apiKeyManager.test.js +206 -0
  27. package/src/services/__tests__/benchmarkService.test.js +184 -0
  28. package/src/services/__tests__/budgetService.test.js +211 -0
  29. package/src/services/__tests__/contextInjectionService.test.js +205 -0
  30. package/src/services/__tests__/conversationCompactionService.test.js +280 -0
  31. package/src/services/__tests__/credentialVault.test.js +469 -0
  32. package/src/services/__tests__/errorHandler.test.js +314 -0
  33. package/src/services/__tests__/fileAttachmentService.test.js +278 -0
  34. package/src/services/__tests__/flowContextService.test.js +199 -0
  35. package/src/services/__tests__/memoryService.test.js +450 -0
  36. package/src/services/__tests__/modelRouterService.test.js +388 -0
  37. package/src/services/__tests__/modelsService.test.js +261 -0
  38. package/src/services/__tests__/portRegistry.test.js +123 -0
  39. package/src/services/__tests__/projectDetector.test.js +34 -0
  40. package/src/services/__tests__/promptService.test.js +242 -0
  41. package/src/services/__tests__/qualityInspector.test.js +97 -0
  42. package/src/services/__tests__/scheduleService.test.js +308 -0
  43. package/src/services/__tests__/serviceRegistry.test.js +74 -0
  44. package/src/services/__tests__/skillsService.test.js +402 -0
  45. package/src/services/__tests__/tokenCountingService.test.js +48 -0
  46. package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
  47. package/src/tools/__tests__/agentDelayTool.test.js +342 -0
  48. package/src/tools/__tests__/asyncToolManager.test.js +344 -0
  49. package/src/tools/__tests__/baseTool.test.js +420 -0
  50. package/src/tools/__tests__/codeMapTool.test.js +348 -0
  51. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
  52. package/src/tools/__tests__/fileTreeTool.test.js +274 -0
  53. package/src/tools/__tests__/filesystemTool.test.js +717 -0
  54. package/src/tools/__tests__/helpTool.test.js +204 -0
  55. package/src/tools/__tests__/jobDoneTool.test.js +296 -0
  56. package/src/tools/__tests__/memoryTool.test.js +297 -0
  57. package/src/tools/__tests__/seekTool.test.js +282 -0
  58. package/src/tools/__tests__/skillsTool.test.js +226 -0
  59. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
  60. package/src/tools/__tests__/taskManagerTool.test.js +725 -0
  61. package/src/tools/__tests__/terminalTool.test.js +384 -0
  62. package/src/tools/__tests__/userPromptTool.test.js +297 -0
  63. package/src/tools/__tests__/webTool.e2e.test.js +25 -11
  64. package/src/tools/webTool.js +6 -12
  65. package/src/types/__tests__/agent.test.js +499 -0
  66. package/src/types/__tests__/contextReference.test.js +606 -0
  67. package/src/types/__tests__/conversation.test.js +555 -0
  68. package/src/types/__tests__/toolCommand.test.js +584 -0
  69. package/src/types/contextReference.js +1 -1
  70. package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
  71. package/src/utilities/__tests__/configManager.test.js +397 -0
  72. package/src/utilities/__tests__/constants.test.js +49 -0
  73. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
  74. package/src/utilities/__tests__/fileProcessor.test.js +104 -0
  75. package/src/utilities/__tests__/jsonRepair.test.js +104 -0
  76. package/src/utilities/__tests__/logger.test.js +129 -0
  77. package/src/utilities/__tests__/platformUtils.test.js +87 -0
  78. package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
  79. package/src/utilities/__tests__/tagParser.test.js +887 -0
  80. package/src/utilities/__tests__/toolConstants.test.js +94 -0
  81. package/src/utilities/tagParser.js +2 -2
  82. package/src/tools/browserTool.js +0 -897
  83. package/src/utilities/platformUtils.test.js +0 -98
@@ -0,0 +1,252 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock fs/promises
5
+ const mockStat = jest.fn();
6
+ const mockReadFile = jest.fn();
7
+ const mockReaddir = jest.fn();
8
+
9
+ jest.unstable_mockModule('fs', () => ({
10
+ promises: {
11
+ stat: mockStat,
12
+ readFile: mockReadFile,
13
+ readdir: mockReaddir,
14
+ }
15
+ }));
16
+
17
+ const { default: ContextManager } = await import('../contextManager.js');
18
+
19
+ describe('ContextManager', () => {
20
+ let cm;
21
+ let logger;
22
+
23
+ beforeEach(() => {
24
+ logger = createMockLogger();
25
+ cm = new ContextManager(
26
+ createMockConfig({ context: { maxSize: 10000, maxReferences: 5, cacheExpiry: 3600 } }),
27
+ logger
28
+ );
29
+ jest.clearAllMocks();
30
+ });
31
+
32
+ // --- processMessageWithContext ---
33
+
34
+ test('processMessageWithContext returns message unchanged if no contextReferences', async () => {
35
+ const msg = { content: 'Hello', contextReferences: [] };
36
+ const result = await cm.processMessageWithContext(msg, '/project');
37
+ expect(result.content).toBe('Hello');
38
+ });
39
+
40
+ test('processMessageWithContext returns message unchanged if contextReferences is missing', async () => {
41
+ const msg = { content: 'Hello' };
42
+ const result = await cm.processMessageWithContext(msg, '/project');
43
+ expect(result.content).toBe('Hello');
44
+ });
45
+
46
+ test('processMessageWithContext enhances message with file context', async () => {
47
+ mockStat.mockResolvedValue({ isFile: () => true, mtime: new Date(), size: 100 });
48
+ mockReadFile.mockResolvedValue('const x = 1;');
49
+
50
+ const msg = {
51
+ content: 'Fix this code',
52
+ contextReferences: [{ type: 'file', path: 'src/index.js' }]
53
+ };
54
+ const result = await cm.processMessageWithContext(msg, '/project');
55
+
56
+ expect(result.content).toContain('PROJECT CONTEXT REFERENCES');
57
+ expect(result.content).toContain('Fix this code');
58
+ expect(result.originalContent).toBe('Fix this code');
59
+ expect(result.processedContextReferences.length).toBe(1);
60
+ expect(result.contextSize).toBeGreaterThan(0);
61
+ });
62
+
63
+ test('processMessageWithContext returns error info on catastrophic failure', async () => {
64
+ // Force loadContextReferences to throw by making sortReferencesByPriority throw
65
+ cm.sortReferencesByPriority = () => { throw new Error('sort failure'); };
66
+
67
+ const msg = {
68
+ content: 'Fix this code',
69
+ contextReferences: [{ type: 'file', path: 'x.js' }]
70
+ };
71
+ const result = await cm.processMessageWithContext(msg, '/project');
72
+ expect(result.contextProcessingError).toBe('sort failure');
73
+ expect(result.processedContextReferences).toEqual([]);
74
+ });
75
+
76
+ // --- loadSingleReference ---
77
+
78
+ test('loadSingleReference reads file content for type=file', async () => {
79
+ mockStat.mockResolvedValue({ isFile: () => true, mtime: new Date(), size: 50 });
80
+ mockReadFile.mockResolvedValue('hello world');
81
+
82
+ const ref = { type: 'file', path: 'test.txt' };
83
+ const loaded = await cm.loadSingleReference(ref, '/project');
84
+
85
+ expect(loaded.content).toBe('hello world');
86
+ expect(loaded.exists).toBe(true);
87
+ expect(loaded.checksum).toBeDefined();
88
+ });
89
+
90
+ test('loadSingleReference handles missing file with File not found error', async () => {
91
+ mockStat.mockRejectedValue(new Error('ENOENT'));
92
+
93
+ const ref = { type: 'file', path: 'missing.js' };
94
+ await expect(cm.loadSingleReference(ref, '/project')).rejects.toThrow('File not found');
95
+ });
96
+
97
+ test('loadSingleReference applies line range for file references', async () => {
98
+ mockStat.mockResolvedValue({ isFile: () => true, mtime: new Date(), size: 100 });
99
+ mockReadFile.mockResolvedValue('line1\nline2\nline3\nline4\nline5');
100
+
101
+ const ref = { type: 'file', path: 'test.js', lines: [2, 4] };
102
+ const loaded = await cm.loadSingleReference(ref, '/project');
103
+
104
+ expect(loaded.content).toBe('line2\nline3\nline4');
105
+ });
106
+
107
+ test('loadSingleReference throws for unknown reference type', async () => {
108
+ const ref = { type: 'unknown', path: 'x' };
109
+ await expect(cm.loadSingleReference(ref, '/project')).rejects.toThrow('Unknown reference type');
110
+ });
111
+
112
+ test('loadSingleReference loads directory listing for type=directory', async () => {
113
+ mockStat.mockResolvedValue({ isDirectory: () => true });
114
+ mockReaddir.mockResolvedValue([
115
+ { name: 'file.js', isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false },
116
+ { name: 'subfolder', isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false },
117
+ ]);
118
+
119
+ const ref = { type: 'directory', path: 'src' };
120
+ const loaded = await cm.loadSingleReference(ref, '/project');
121
+
122
+ expect(loaded.exists).toBe(true);
123
+ expect(loaded.content).toContain('file.js');
124
+ expect(loaded.content).toContain('subfolder');
125
+ expect(loaded.fileCount).toBe(1);
126
+ expect(loaded.directoryCount).toBe(1);
127
+ });
128
+
129
+ test('loadSingleReference loads selection reference without file', async () => {
130
+ const ref = { type: 'selection', content: 'selected text' };
131
+ const loaded = await cm.loadSingleReference(ref, '/project');
132
+ expect(loaded.exists).toBe(true);
133
+ expect(loaded.validated).toBe(false);
134
+ });
135
+
136
+ test('loadSingleReference loads component reference', async () => {
137
+ mockReadFile.mockResolvedValue('function myFunc() {\n return 1;\n}');
138
+
139
+ const ref = { type: 'component', file: 'utils.js', name: 'myFunc' };
140
+ const loaded = await cm.loadSingleReference(ref, '/project');
141
+ expect(loaded.exists).toBe(true);
142
+ expect(loaded.content).toContain('myFunc');
143
+ });
144
+
145
+ // --- generateContextPrompt ---
146
+
147
+ test('generateContextPrompt returns empty string for no references', () => {
148
+ expect(cm.generateContextPrompt([])).toBe('');
149
+ });
150
+
151
+ test('generateContextPrompt formats references with code blocks and language', () => {
152
+ const refs = [{
153
+ type: 'file',
154
+ path: 'test.js',
155
+ content: 'const a = 1;',
156
+ exists: true
157
+ }];
158
+ const prompt = cm.generateContextPrompt(refs);
159
+ expect(prompt).toContain('FILE: test.js');
160
+ expect(prompt).toContain('```javascript');
161
+ expect(prompt).toContain('const a = 1;');
162
+ expect(prompt).toContain('END CONTEXT REFERENCES');
163
+ });
164
+
165
+ test('generateContextPrompt shows error for failed references', () => {
166
+ const refs = [{ type: 'file', path: 'bad.js', error: 'File not found', content: '[Error]' }];
167
+ const prompt = cm.generateContextPrompt(refs);
168
+ expect(prompt).toContain('Error: File not found');
169
+ });
170
+
171
+ test('generateContextPrompt shows line range metadata', () => {
172
+ const refs = [{
173
+ type: 'file', path: 'x.py', content: 'pass', exists: true,
174
+ lines: [10, 20]
175
+ }];
176
+ const prompt = cm.generateContextPrompt(refs);
177
+ expect(prompt).toContain('Lines 10-20');
178
+ });
179
+
180
+ test('generateContextPrompt shows truncation and change warnings', () => {
181
+ const refs = [{
182
+ type: 'file', path: 'x.js', content: 'code', exists: true,
183
+ truncated: true, hasChanged: true
184
+ }];
185
+ const prompt = cm.generateContextPrompt(refs);
186
+ expect(prompt).toContain('truncated');
187
+ expect(prompt).toContain('changed');
188
+ });
189
+
190
+ // --- Cache operations ---
191
+
192
+ test('addToCache and getFromCache round-trip', () => {
193
+ const data = { content: 'cached data', loadedAt: new Date().toISOString() };
194
+ cm.addToCache('key1', data);
195
+ const cached = cm.getFromCache('key1');
196
+ expect(cached).toBe(data);
197
+ });
198
+
199
+ test('getFromCache returns null for missing key', () => {
200
+ expect(cm.getFromCache('nonexistent')).toBeNull();
201
+ });
202
+
203
+ test('shouldRefreshCache returns true when cache is old', () => {
204
+ const oldRef = { loadedAt: new Date(Date.now() - 7200 * 1000).toISOString() };
205
+ expect(cm.shouldRefreshCache(oldRef)).toBe(true);
206
+ });
207
+
208
+ test('shouldRefreshCache returns false for fresh cache', () => {
209
+ const freshRef = { loadedAt: new Date().toISOString() };
210
+ expect(cm.shouldRefreshCache(freshRef)).toBe(false);
211
+ });
212
+
213
+ // --- generateCacheKey ---
214
+
215
+ test('generateCacheKey returns consistent keys for same input', () => {
216
+ const ref = { type: 'file', path: 'a.js' };
217
+ const key1 = cm.generateCacheKey(ref, '/project');
218
+ const key2 = cm.generateCacheKey(ref, '/project');
219
+ expect(key1).toBe(key2);
220
+ });
221
+
222
+ test('generateCacheKey returns different keys for different paths', () => {
223
+ const key1 = cm.generateCacheKey({ type: 'file', path: 'a.js' }, '/project');
224
+ const key2 = cm.generateCacheKey({ type: 'file', path: 'b.js' }, '/project');
225
+ expect(key1).not.toBe(key2);
226
+ });
227
+
228
+ // --- loadContextReferences ---
229
+
230
+ test('loadContextReferences truncates content when exceeding maxContextSize', async () => {
231
+ cm.maxContextSize = 50;
232
+ mockStat.mockResolvedValue({ isFile: () => true, mtime: new Date(), size: 200 });
233
+ mockReadFile.mockResolvedValue('A'.repeat(200));
234
+
235
+ const refs = [{ type: 'file', path: 'big.js' }];
236
+ const loaded = await cm.loadContextReferences(refs, '/project');
237
+
238
+ // Content should be shorter than original 200 chars
239
+ expect(loaded[0].content.length).toBeLessThan(200);
240
+ });
241
+
242
+ test('loadContextReferences pushes error reference when loading fails', async () => {
243
+ mockStat.mockRejectedValue(new Error('ENOENT'));
244
+
245
+ const refs = [{ type: 'file', path: 'missing.js' }];
246
+ const loaded = await cm.loadContextReferences(refs, '/project');
247
+
248
+ expect(loaded.length).toBe(1);
249
+ expect(loaded[0].error).toBeDefined();
250
+ expect(loaded[0].exists).toBe(false);
251
+ });
252
+ });
@@ -0,0 +1,262 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig, createMockStateManager, createMockAgentPool } from '../../__test-utils__/mockFactories.js';
3
+
4
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
5
+ AGENT_MODES: { CHAT: 'chat', AGENT: 'agent' }
6
+ }));
7
+
8
+ const { default: FlowExecutor } = await import('../flowExecutor.js');
9
+
10
+ describe('FlowExecutor', () => {
11
+ let fe;
12
+ let config, logger, stateManager, agentPool, messageProcessor;
13
+
14
+ beforeEach(() => {
15
+ config = createMockConfig();
16
+ logger = createMockLogger();
17
+ stateManager = createMockStateManager();
18
+ agentPool = createMockAgentPool();
19
+ messageProcessor = { processMessage: jest.fn().mockResolvedValue(undefined) };
20
+ fe = new FlowExecutor(config, logger, stateManager, agentPool, messageProcessor);
21
+ });
22
+
23
+ // --- topologicalSort ---
24
+
25
+ test('topologicalSort with simple linear DAG returns correct order', () => {
26
+ const nodes = [
27
+ { id: 'a', data: {} },
28
+ { id: 'b', data: {} },
29
+ { id: 'c', data: {} }
30
+ ];
31
+ const edges = [
32
+ { source: 'a', target: 'b' },
33
+ { source: 'b', target: 'c' }
34
+ ];
35
+ const sorted = fe.topologicalSort(nodes, edges);
36
+ expect(sorted.length).toBe(3);
37
+ const ids = sorted.map(n => n.id);
38
+ expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('b'));
39
+ expect(ids.indexOf('b')).toBeLessThan(ids.indexOf('c'));
40
+ });
41
+
42
+ test('topologicalSort with diamond DAG respects all edges', () => {
43
+ const nodes = [
44
+ { id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }
45
+ ];
46
+ const edges = [
47
+ { source: 'a', target: 'b' },
48
+ { source: 'a', target: 'c' },
49
+ { source: 'b', target: 'd' },
50
+ { source: 'c', target: 'd' }
51
+ ];
52
+ const sorted = fe.topologicalSort(nodes, edges);
53
+ expect(sorted.length).toBe(4);
54
+ const ids = sorted.map(n => n.id);
55
+ expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('b'));
56
+ expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('c'));
57
+ expect(ids.indexOf('b')).toBeLessThan(ids.indexOf('d'));
58
+ expect(ids.indexOf('c')).toBeLessThan(ids.indexOf('d'));
59
+ });
60
+
61
+ test('topologicalSort with single node returns that node', () => {
62
+ const sorted = fe.topologicalSort([{ id: 'x' }], []);
63
+ expect(sorted.length).toBe(1);
64
+ expect(sorted[0].id).toBe('x');
65
+ });
66
+
67
+ test('topologicalSort with cycle logs warning and returns partial result', () => {
68
+ const nodes = [{ id: 'a' }, { id: 'b' }];
69
+ const edges = [
70
+ { source: 'a', target: 'b' },
71
+ { source: 'b', target: 'a' }
72
+ ];
73
+ const sorted = fe.topologicalSort(nodes, edges);
74
+ // Cycle means sorted.length < nodes.length
75
+ expect(sorted.length).toBeLessThan(nodes.length);
76
+ expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('cycles'));
77
+ });
78
+
79
+ test('topologicalSort with empty/null nodes returns empty array', () => {
80
+ expect(fe.topologicalSort([], [])).toEqual([]);
81
+ expect(fe.topologicalSort(null, [])).toEqual([]);
82
+ });
83
+
84
+ // --- executeInputNode ---
85
+
86
+ test('executeInputNode passes input through with template', async () => {
87
+ const node = { id: 'in1', data: { promptTemplate: '{{userInput}}' } };
88
+ const context = { input: 'Hello world', variables: {} };
89
+ const result = await fe.executeInputNode(node, context);
90
+ expect(result.type).toBe('input');
91
+ expect(result.output).toBe('Hello world');
92
+ expect(result.raw).toBe('Hello world');
93
+ });
94
+
95
+ test('executeInputNode uses default template when none provided', async () => {
96
+ const node = { id: 'in1', data: {} };
97
+ const context = { input: 'Test input', variables: {} };
98
+ const result = await fe.executeInputNode(node, context);
99
+ expect(result.output).toBe('Test input');
100
+ });
101
+
102
+ // --- executeOutputNode ---
103
+
104
+ test('executeOutputNode collects final output from previous nodes in text format', async () => {
105
+ const node = { id: 'out1', data: { outputFormat: 'text' } };
106
+ const flow = { edges: [{ source: 'agent1', target: 'out1' }] };
107
+ const context = {
108
+ input: '', variables: {},
109
+ nodeOutputs: { agent1: { output: 'Agent result here' } }
110
+ };
111
+ const result = await fe.executeOutputNode(node, context, flow);
112
+ expect(result.type).toBe('output');
113
+ expect(result.format).toBe('text');
114
+ expect(result.output).toBe('Agent result here');
115
+ });
116
+
117
+ test('executeOutputNode uses json format when specified', async () => {
118
+ const node = { id: 'out1', data: { outputFormat: 'json' } };
119
+ const flow = { edges: [{ source: 'a1', target: 'out1' }] };
120
+ const context = { input: '', variables: {}, nodeOutputs: { a1: { output: 'text data' } } };
121
+ const result = await fe.executeOutputNode(node, context, flow);
122
+ expect(result.format).toBe('json');
123
+ expect(result.output).toHaveProperty('result');
124
+ });
125
+
126
+ // --- setWebSocketManager ---
127
+
128
+ test('setWebSocketManager stores the reference', () => {
129
+ const wsm = { broadcast: jest.fn() };
130
+ fe.setWebSocketManager(wsm);
131
+ expect(fe.webSocketManager).toBe(wsm);
132
+ });
133
+
134
+ // --- getActiveExecutions ---
135
+
136
+ test('getActiveExecutions returns empty array initially', () => {
137
+ const active = fe.getActiveExecutions();
138
+ expect(active).toEqual([]);
139
+ });
140
+
141
+ test('getActiveExecutions returns entries during a running flow', () => {
142
+ fe.activeExecutions.set('run-1', {
143
+ flowId: 'flow-1', status: 'running', startedAt: new Date()
144
+ });
145
+ const active = fe.getActiveExecutions();
146
+ expect(active.length).toBe(1);
147
+ expect(active[0].runId).toBe('run-1');
148
+ expect(active[0].flowId).toBe('flow-1');
149
+ expect(active[0].status).toBe('running');
150
+ });
151
+
152
+ // --- stopExecution ---
153
+
154
+ test('stopExecution marks execution as stopped and returns true', async () => {
155
+ fe.activeExecutions.set('run-1', { flowId: 'f1', status: 'running' });
156
+ fe.completionListeners.set('run-1', {});
157
+ const result = await fe.stopExecution('run-1');
158
+ expect(result).toBe(true);
159
+ expect(fe.activeExecutions.get('run-1').status).toBe('stopped');
160
+ expect(fe.completionListeners.has('run-1')).toBe(false);
161
+ });
162
+
163
+ test('stopExecution returns false for nonexistent run', async () => {
164
+ const result = await fe.stopExecution('nonexistent');
165
+ expect(result).toBe(false);
166
+ });
167
+
168
+ // --- notifyAgentCompletion ---
169
+
170
+ test('notifyAgentCompletion resolves waiting listener', () => {
171
+ let resolved = null;
172
+ fe.completionListeners.set('run-1-agent-1', {
173
+ agentId: 'agent-1',
174
+ runId: 'run-1',
175
+ resolve: (data) => { resolved = data; }
176
+ });
177
+ const found = fe.notifyAgentCompletion('agent-1', { summary: 'Done', success: true });
178
+ expect(found).toBe(true);
179
+ expect(resolved).not.toBeNull();
180
+ expect(resolved.summary).toBe('Done');
181
+ expect(resolved.completed).toBe(true);
182
+ });
183
+
184
+ test('notifyAgentCompletion returns false when no listener found', () => {
185
+ const found = fe.notifyAgentCompletion('unknown-agent', {});
186
+ expect(found).toBe(false);
187
+ });
188
+
189
+ // --- executeFlow ---
190
+
191
+ test('executeFlow with input+output nodes completes successfully', async () => {
192
+ const flowId = 'test-flow';
193
+ const flow = {
194
+ id: flowId,
195
+ name: 'Test Flow',
196
+ nodes: [
197
+ { id: 'in', type: 'input', data: {} },
198
+ { id: 'out', type: 'output', data: {} }
199
+ ],
200
+ edges: [{ source: 'in', target: 'out' }],
201
+ variables: {}
202
+ };
203
+
204
+ stateManager.getFlow = jest.fn().mockResolvedValue(flow);
205
+ stateManager.createFlowRun = jest.fn().mockResolvedValue({ id: 'run-1' });
206
+ stateManager.updateFlowRun = jest.fn().mockResolvedValue(undefined);
207
+ stateManager.getFlowRun = jest.fn().mockResolvedValue({ id: 'run-1', nodeStates: {} });
208
+
209
+ const result = await fe.executeFlow(flowId, { userInput: 'Hello' });
210
+ expect(result.status).toBe('completed');
211
+ expect(result.runId).toBe('run-1');
212
+ });
213
+
214
+ test('executeFlow throws when flow not found', async () => {
215
+ stateManager.getFlow = jest.fn().mockResolvedValue(null);
216
+ stateManager.createFlowRun = jest.fn();
217
+ await expect(fe.executeFlow('missing-flow', {})).rejects.toThrow('Flow not found');
218
+ });
219
+
220
+ // --- truncateOutput ---
221
+
222
+ test('truncateOutput truncates long strings', () => {
223
+ const longStr = 'x'.repeat(2000);
224
+ const result = fe.truncateOutput(longStr);
225
+ expect(result.length).toBeLessThan(2000);
226
+ expect(result).toContain('truncated');
227
+ });
228
+
229
+ test('truncateOutput returns short strings unchanged', () => {
230
+ expect(fe.truncateOutput('short')).toBe('short');
231
+ });
232
+
233
+ test('truncateOutput handles large objects', () => {
234
+ const obj = { data: 'x'.repeat(2000) };
235
+ const result = fe.truncateOutput(obj);
236
+ expect(result.truncated).toBe(true);
237
+ expect(result.preview).toBeDefined();
238
+ });
239
+
240
+ // --- applyTemplate ---
241
+
242
+ test('applyTemplate substitutes variables', () => {
243
+ const result = fe.applyTemplate('Hello {{name}}, you have {{count}} items', { name: 'Alice', count: 5 });
244
+ expect(result).toBe('Hello Alice, you have 5 items');
245
+ });
246
+
247
+ // --- collectPreviousOutput ---
248
+
249
+ test('collectPreviousOutput combines multiple outputs', () => {
250
+ const nodeOutputs = {
251
+ n1: { output: 'First' },
252
+ n2: { output: 'Second' }
253
+ };
254
+ const result = fe.collectPreviousOutput(['n1', 'n2'], nodeOutputs);
255
+ expect(result).toContain('First');
256
+ expect(result).toContain('Second');
257
+ });
258
+
259
+ test('collectPreviousOutput returns empty string for no outputs', () => {
260
+ expect(fe.collectPreviousOutput([], {})).toBe('');
261
+ });
262
+ });