onbuzz 3.6.1 → 3.6.3

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 (84) 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__/fileSystemTool.test.js +717 -0
  53. package/src/tools/__tests__/fileTreeTool.test.js +274 -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
  84. /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
@@ -0,0 +1,184 @@
1
+ import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // We need to mock global fetch and setInterval/setTimeout
5
+ const { default: BenchmarkService } = await import('../benchmarkService.js');
6
+
7
+ describe('BenchmarkService', () => {
8
+ let service;
9
+ let logger;
10
+ let config;
11
+ let originalFetch;
12
+
13
+ beforeEach(() => {
14
+ jest.useFakeTimers();
15
+ logger = createMockLogger();
16
+ config = { backend: { baseUrl: 'https://test-backend.example.com' } };
17
+ service = new BenchmarkService(config, logger);
18
+ originalFetch = global.fetch;
19
+ global.fetch = jest.fn();
20
+ });
21
+
22
+ afterEach(() => {
23
+ jest.useRealTimers();
24
+ global.fetch = originalFetch;
25
+ });
26
+
27
+ test('constructor sets backend URL from config', () => {
28
+ expect(service.azureBackendUrl).toBe('https://test-backend.example.com');
29
+ });
30
+
31
+ test('constructor uses default URL when config has no backend', () => {
32
+ const s = new BenchmarkService({}, logger);
33
+ expect(s.azureBackendUrl).toContain('azurefd.net');
34
+ });
35
+
36
+ test('getBenchmarks returns null and warns when no data', () => {
37
+ const result = service.getBenchmarks();
38
+ expect(result).toBeNull();
39
+ expect(logger.warn).toHaveBeenCalled();
40
+ });
41
+
42
+ test('getBenchmarks returns benchmark text when available', () => {
43
+ service.benchmarkText = 'some benchmark data';
44
+ expect(service.getBenchmarks()).toBe('some benchmark data');
45
+ });
46
+
47
+ test('getBenchmarkTable is alias for getBenchmarks', () => {
48
+ service.benchmarkText = 'table data';
49
+ expect(service.getBenchmarkTable()).toBe('table data');
50
+ });
51
+
52
+ test('loadBenchmarks skips if already loading', async () => {
53
+ service.isLoading = true;
54
+ await service.loadBenchmarks();
55
+ expect(logger.debug).toHaveBeenCalledWith('Benchmark loading already in progress');
56
+ });
57
+
58
+ test('loadBenchmarks skips if endpoint not available', async () => {
59
+ service.endpointAvailable = false;
60
+ await service.loadBenchmarks();
61
+ expect(global.fetch).not.toHaveBeenCalled();
62
+ });
63
+
64
+ test('loadBenchmarks succeeds with valid response', async () => {
65
+ global.fetch.mockResolvedValueOnce({
66
+ ok: true,
67
+ text: jest.fn().mockResolvedValueOnce('benchmark text data')
68
+ });
69
+
70
+ await service.loadBenchmarks();
71
+ expect(service.benchmarkText).toBe('benchmark text data');
72
+ expect(service.lastUpdated).toBeInstanceOf(Date);
73
+ expect(service.isLoading).toBe(false);
74
+ });
75
+
76
+ test('loadBenchmarks handles 404 by disabling endpoint', async () => {
77
+ global.fetch.mockResolvedValueOnce({
78
+ ok: false,
79
+ status: 404,
80
+ statusText: 'Not Found'
81
+ });
82
+
83
+ await service.loadBenchmarks();
84
+ expect(service.endpointAvailable).toBe(false);
85
+ expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('404'));
86
+ });
87
+
88
+ test('loadBenchmarks handles non-404 error', async () => {
89
+ global.fetch.mockResolvedValueOnce({
90
+ ok: false,
91
+ status: 500,
92
+ statusText: 'Internal Server Error'
93
+ });
94
+
95
+ await service.loadBenchmarks();
96
+ expect(service.endpointAvailable).toBe(true);
97
+ expect(logger.error).toHaveBeenCalled();
98
+ });
99
+
100
+ test('loadBenchmarks handles empty response', async () => {
101
+ global.fetch.mockResolvedValueOnce({
102
+ ok: true,
103
+ text: jest.fn().mockResolvedValueOnce('')
104
+ });
105
+
106
+ await service.loadBenchmarks();
107
+ expect(service.benchmarkText).toBeNull();
108
+ expect(service.isLoading).toBe(false);
109
+ });
110
+
111
+ test('loadBenchmarks handles fetch timeout (AbortError)', async () => {
112
+ const abortError = new Error('Aborted');
113
+ abortError.name = 'AbortError';
114
+ global.fetch.mockRejectedValueOnce(abortError);
115
+
116
+ await service.loadBenchmarks();
117
+ expect(logger.error).toHaveBeenCalled();
118
+ expect(service.isLoading).toBe(false);
119
+ });
120
+
121
+ test('loadBenchmarks includes auth header when apiKey configured', async () => {
122
+ service.config = { apiKey: 'test-api-key' };
123
+ global.fetch.mockResolvedValueOnce({
124
+ ok: true,
125
+ text: jest.fn().mockResolvedValueOnce('data')
126
+ });
127
+
128
+ await service.loadBenchmarks();
129
+ const fetchCall = global.fetch.mock.calls[0];
130
+ expect(fetchCall[1].headers.Authorization).toBe('Bearer test-api-key');
131
+ });
132
+
133
+ test('needsRefresh returns true when never updated', () => {
134
+ expect(service.needsRefresh()).toBe(true);
135
+ });
136
+
137
+ test('needsRefresh returns false shortly after update', () => {
138
+ service.lastUpdated = new Date();
139
+ expect(service.needsRefresh()).toBe(false);
140
+ });
141
+
142
+ test('initialize loads benchmarks and schedules refresh', async () => {
143
+ global.fetch.mockResolvedValueOnce({
144
+ ok: true,
145
+ text: jest.fn().mockResolvedValueOnce('initial data')
146
+ });
147
+
148
+ await service.initialize();
149
+ expect(service.benchmarkText).toBe('initial data');
150
+ expect(logger.info).toHaveBeenCalledWith('Benchmark service initialized');
151
+ });
152
+
153
+ test('initialize handles errors', async () => {
154
+ global.fetch.mockRejectedValueOnce(new Error('network fail'));
155
+ await service.initialize();
156
+ expect(service.benchmarkText).toBeNull();
157
+ expect(logger.error).toHaveBeenCalled();
158
+ });
159
+
160
+ test('forceRefresh calls loadBenchmarks', async () => {
161
+ global.fetch.mockResolvedValueOnce({
162
+ ok: true,
163
+ text: jest.fn().mockResolvedValueOnce('refreshed')
164
+ });
165
+ await service.forceRefresh();
166
+ expect(service.benchmarkText).toBe('refreshed');
167
+ });
168
+
169
+ test('getStatus returns service status', () => {
170
+ service.benchmarkText = 'data';
171
+ service.lastUpdated = new Date('2024-01-01');
172
+ const status = service.getStatus();
173
+ expect(status.initialized).toBe(true);
174
+ expect(status.hasData).toBe(true);
175
+ expect(status.lastUpdated).toBe('2024-01-01T00:00:00.000Z');
176
+ expect(status.isLoading).toBe(false);
177
+ });
178
+
179
+ test('getStatus when no data', () => {
180
+ const status = service.getStatus();
181
+ expect(status.initialized).toBe(false);
182
+ expect(status.lastUpdated).toBeNull();
183
+ });
184
+ });
@@ -0,0 +1,211 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock constants to avoid loading the full module
5
+ jest.unstable_mockModule('../../utilities/constants.js', () => ({
6
+ BUDGET_LIMITS: { DAILY: 10, WEEKLY: 50, MONTHLY: 200 },
7
+ USAGE_ALERTS: { THRESHOLDS: [50, 75, 90, 100], COOLDOWN_PERIOD: 3600000 }
8
+ }));
9
+
10
+ const { BudgetService } = await import('../budgetService.js');
11
+
12
+ describe('BudgetService', () => {
13
+ let service;
14
+ let mockLogger;
15
+ let mockConfig;
16
+ let mockModelsService;
17
+
18
+ beforeEach(() => {
19
+ jest.clearAllMocks();
20
+
21
+ mockLogger = createMockLogger();
22
+ mockConfig = createMockConfig();
23
+ mockConfig.budgets = { daily: 10, weekly: 50, monthly: 200 };
24
+
25
+ mockModelsService = {
26
+ getModels: jest.fn().mockReturnValue([
27
+ {
28
+ name: 'gpt-4',
29
+ pricing: { input: 0.03, output: 0.06 }
30
+ },
31
+ {
32
+ name: 'claude-3-haiku',
33
+ pricing: { input: 0.00025, output: 0.00125 }
34
+ }
35
+ ])
36
+ };
37
+
38
+ service = new BudgetService(mockConfig, mockLogger, mockModelsService);
39
+ });
40
+
41
+ // ───── Cost Calculation ─────
42
+
43
+ describe('calculateCost', () => {
44
+ test('returns 0 for zero tokens', () => {
45
+ const cost = service.calculateCost('gpt-4', {
46
+ prompt_tokens: 0,
47
+ completion_tokens: 0,
48
+ total_tokens: 0
49
+ });
50
+
51
+ expect(cost).toBe(0);
52
+ });
53
+
54
+ test('returns positive number for non-zero tokens', () => {
55
+ const cost = service.calculateCost('gpt-4', {
56
+ prompt_tokens: 1000,
57
+ completion_tokens: 500,
58
+ total_tokens: 1500
59
+ });
60
+
61
+ expect(cost).toBeGreaterThan(0);
62
+ });
63
+
64
+ test('uses model-specific pricing when available', () => {
65
+ // gpt-4 pricing: input 0.03/1K, output 0.06/1K
66
+ const cost = service.calculateCost('gpt-4', {
67
+ prompt_tokens: 1000,
68
+ completion_tokens: 1000,
69
+ total_tokens: 2000
70
+ });
71
+
72
+ // Expected: (1000 * 0.03/1000) + (1000 * 0.06/1000) = 0.03 + 0.06 = 0.09
73
+ expect(cost).toBeCloseTo(0.09, 5);
74
+ });
75
+
76
+ test('uses default pricing (returns 0) for unknown models', () => {
77
+ const cost = service.calculateCost('unknown-model-xyz', {
78
+ prompt_tokens: 1000,
79
+ completion_tokens: 500,
80
+ total_tokens: 1500
81
+ });
82
+
83
+ // No pricing found -> returns 0
84
+ expect(cost).toBe(0);
85
+ });
86
+ });
87
+
88
+ // ───── Usage Tracking ─────
89
+
90
+ describe('trackUsage', () => {
91
+ test('increments daily usage', () => {
92
+ service.trackUsage('agent-1', 'gpt-4', {
93
+ prompt_tokens: 100,
94
+ completion_tokens: 50,
95
+ total_tokens: 150
96
+ });
97
+
98
+ const dayKey = service.getDayKey(new Date());
99
+ const dailyUsage = service.usage.daily.get(dayKey);
100
+
101
+ expect(dailyUsage).toBeDefined();
102
+ expect(dailyUsage.tokens).toBe(150);
103
+ expect(dailyUsage.requests).toBe(1);
104
+ });
105
+
106
+ test('tracks per-agent usage', () => {
107
+ service.trackUsage('agent-1', 'gpt-4', {
108
+ prompt_tokens: 100,
109
+ completion_tokens: 50,
110
+ total_tokens: 150
111
+ });
112
+
113
+ const dayKey = service.getDayKey(new Date());
114
+ const dailyUsage = service.usage.daily.get(dayKey);
115
+
116
+ expect(dailyUsage.byAgent['agent-1']).toBeDefined();
117
+ expect(dailyUsage.byAgent['agent-1'].tokens).toBe(150);
118
+ expect(dailyUsage.byAgent['agent-1'].requests).toBe(1);
119
+ });
120
+
121
+ test('tracks per-model usage', () => {
122
+ service.trackUsage('agent-1', 'claude-3-haiku', {
123
+ prompt_tokens: 200,
124
+ completion_tokens: 100,
125
+ total_tokens: 300
126
+ });
127
+
128
+ const dayKey = service.getDayKey(new Date());
129
+ const dailyUsage = service.usage.daily.get(dayKey);
130
+
131
+ expect(dailyUsage.byModel['claude-3-haiku']).toBeDefined();
132
+ expect(dailyUsage.byModel['claude-3-haiku'].tokens).toBe(300);
133
+ });
134
+
135
+ test('multiple calls accumulate correctly', () => {
136
+ service.trackUsage('agent-1', 'gpt-4', {
137
+ prompt_tokens: 100, completion_tokens: 50, total_tokens: 150
138
+ });
139
+ service.trackUsage('agent-2', 'gpt-4', {
140
+ prompt_tokens: 200, completion_tokens: 100, total_tokens: 300
141
+ });
142
+ service.trackUsage('agent-1', 'gpt-4', {
143
+ prompt_tokens: 50, completion_tokens: 25, total_tokens: 75
144
+ });
145
+
146
+ expect(service.usage.total.tokens).toBe(525);
147
+ expect(service.usage.total.requests).toBe(3);
148
+
149
+ const dayKey = service.getDayKey(new Date());
150
+ const dailyUsage = service.usage.daily.get(dayKey);
151
+ expect(dailyUsage.tokens).toBe(525);
152
+ expect(dailyUsage.requests).toBe(3);
153
+
154
+ // agent-1 should have accumulated across two calls
155
+ expect(dailyUsage.byAgent['agent-1'].tokens).toBe(225);
156
+ expect(dailyUsage.byAgent['agent-1'].requests).toBe(2);
157
+ });
158
+ });
159
+
160
+ // ───── Budget Checking ─────
161
+
162
+ describe('budget checking', () => {
163
+ test('isWithinBudget returns true when under limit', () => {
164
+ // No usage tracked yet, so well under limit
165
+ expect(service.isWithinBudget('daily')).toBe(true);
166
+ });
167
+
168
+ test('isWithinBudget returns false when over daily limit', () => {
169
+ // Force daily usage above the $10 limit
170
+ const dayKey = service.getDayKey(new Date());
171
+ service.usage.daily.set(dayKey, {
172
+ cost: 15,
173
+ tokens: 100000,
174
+ requests: 50,
175
+ byAgent: {},
176
+ byModel: {}
177
+ });
178
+
179
+ expect(service.isWithinBudget('daily')).toBe(false);
180
+ });
181
+
182
+ test('getRemainingBudget decreases after usage', () => {
183
+ const before = service.getRemainingBudget();
184
+ expect(before.daily).toBe(10);
185
+
186
+ // Track some usage that has cost
187
+ service.trackUsage('agent-1', 'gpt-4', {
188
+ prompt_tokens: 1000,
189
+ completion_tokens: 1000,
190
+ total_tokens: 2000
191
+ });
192
+
193
+ const after = service.getRemainingBudget();
194
+ expect(after.daily).toBeLessThan(before.daily);
195
+ });
196
+
197
+ test('setBudgets with custom values overrides defaults', () => {
198
+ service.setBudgets({ daily: 25, weekly: 100, monthly: 500 });
199
+
200
+ expect(service.budgets.daily).toBe(25);
201
+ expect(service.budgets.weekly).toBe(100);
202
+ expect(service.budgets.monthly).toBe(500);
203
+
204
+ // Remaining budget should reflect new limits
205
+ const remaining = service.getRemainingBudget();
206
+ expect(remaining.daily).toBe(25);
207
+ expect(remaining.weekly).toBe(100);
208
+ expect(remaining.monthly).toBe(500);
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,205 @@
1
+ import { jest, describe, test, expect, beforeEach } from '@jest/globals';
2
+ import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
3
+
4
+ // Mock FileAttachmentService and serviceRegistry
5
+ jest.unstable_mockModule('../fileAttachmentService.js', () => ({
6
+ default: jest.fn().mockImplementation(() => ({
7
+ initialize: jest.fn().mockResolvedValue(undefined),
8
+ getActiveAttachments: jest.fn().mockResolvedValue([]),
9
+ getAttachmentContent: jest.fn().mockResolvedValue(null)
10
+ }))
11
+ }));
12
+
13
+ jest.unstable_mockModule('../serviceRegistry.js', () => ({
14
+ default: {
15
+ getAll: jest.fn(() => ({}))
16
+ }
17
+ }));
18
+
19
+ const { default: ContextInjectionService } = await import('../contextInjectionService.js');
20
+ const { default: registry } = await import('../serviceRegistry.js');
21
+
22
+ describe('ContextInjectionService', () => {
23
+ let service;
24
+ let logger;
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ logger = createMockLogger();
29
+ service = new ContextInjectionService({}, logger);
30
+ });
31
+
32
+ test('initialize delegates to attachmentService', async () => {
33
+ await service.initialize();
34
+ expect(service.attachmentService.initialize).toHaveBeenCalled();
35
+ });
36
+
37
+ test('buildDynamicContext returns empty string when no attachments', async () => {
38
+ service.attachmentService.getActiveAttachments.mockResolvedValue([]);
39
+ const result = await service.buildDynamicContext('agent-1');
40
+ expect(result).toBe('');
41
+ });
42
+
43
+ test('buildDynamicContext builds content file section', async () => {
44
+ service.attachmentService.getActiveAttachments.mockResolvedValue([
45
+ { fileId: 'f1', fileName: 'test.txt', mode: 'content', contentType: 'text', fileType: 'text/plain', size: 1024 }
46
+ ]);
47
+ service.attachmentService.getAttachmentContent.mockResolvedValue('file content here');
48
+
49
+ const result = await service.buildDynamicContext('agent-1');
50
+ expect(result).toContain('<attached-files>');
51
+ expect(result).toContain('</attached-files>');
52
+ expect(result).toContain('file content here');
53
+ });
54
+
55
+ test('buildDynamicContext builds reference file section', async () => {
56
+ service.attachmentService.getActiveAttachments.mockResolvedValue([
57
+ { fileId: 'f2', fileName: 'ref.js', mode: 'reference', fileType: 'text/javascript', size: 2048, originalPath: '/path/to/ref.js', lastModified: '2024-01-01T00:00:00Z' }
58
+ ]);
59
+
60
+ const result = await service.buildDynamicContext('agent-1');
61
+ expect(result).toContain('<file-references>');
62
+ expect(result).toContain('</file-references>');
63
+ expect(result).toContain('ref.js');
64
+ expect(result).toContain('filesystem tool');
65
+ });
66
+
67
+ test('buildDynamicContext skips content files with null content', async () => {
68
+ service.attachmentService.getActiveAttachments.mockResolvedValue([
69
+ { fileId: 'f1', fileName: 'test.txt', mode: 'content', contentType: 'text', fileType: 'text/plain', size: 512 }
70
+ ]);
71
+ service.attachmentService.getAttachmentContent.mockResolvedValue(null);
72
+
73
+ const result = await service.buildDynamicContext('agent-1');
74
+ expect(result).toContain('<attached-files>');
75
+ expect(result).not.toContain('test.txt');
76
+ });
77
+
78
+ test('buildDynamicContext handles errors and returns empty string', async () => {
79
+ service.attachmentService.getActiveAttachments.mockRejectedValue(new Error('fail'));
80
+ const result = await service.buildDynamicContext('agent-1');
81
+ expect(result).toBe('');
82
+ expect(logger.error).toHaveBeenCalled();
83
+ });
84
+
85
+ test('formatContentFile handles text content type', () => {
86
+ const result = service.formatContentFile(
87
+ { fileName: 'test.txt', fileType: 'text/plain', size: 1024, contentType: 'text' },
88
+ 'hello world'
89
+ );
90
+ expect(result).toContain('test.txt');
91
+ expect(result).toContain('hello world');
92
+ expect(result).toContain('1.00KB');
93
+ });
94
+
95
+ test('formatContentFile handles image content type', () => {
96
+ const result = service.formatContentFile(
97
+ { fileName: 'img.png', fileType: 'image/png', size: 2048, contentType: 'image' },
98
+ 'data:image/png;base64,abc'
99
+ );
100
+ expect(result).toContain('img.png');
101
+ expect(result).toContain('base64');
102
+ });
103
+
104
+ test('formatContentFile handles pdf content type', () => {
105
+ const result = service.formatContentFile(
106
+ { fileName: 'doc.pdf', fileType: 'application/pdf', size: 4096, contentType: 'pdf' },
107
+ 'extracted text'
108
+ );
109
+ expect(result).toContain('doc.pdf');
110
+ expect(result).toContain('extracted text');
111
+ });
112
+
113
+ test('formatContentFile uses fallback for unknown content type', () => {
114
+ const result = service.formatContentFile(
115
+ { fileName: 'data.bin', fileType: 'application/octet-stream', size: 512, contentType: 'binary' },
116
+ 'raw data'
117
+ );
118
+ expect(result).toContain('data.bin');
119
+ expect(result).toContain('raw data');
120
+ });
121
+
122
+ test('formatReferenceFile returns formatted XML tag', () => {
123
+ const result = service.formatReferenceFile({
124
+ fileName: 'ref.js',
125
+ originalPath: '/src/ref.js',
126
+ size: 2048,
127
+ fileType: 'text/javascript',
128
+ lastModified: '2024-06-15T12:00:00Z'
129
+ });
130
+ expect(result).toContain('ref.js');
131
+ expect(result).toContain('/src/ref.js');
132
+ expect(result).toContain('2024-06-15');
133
+ });
134
+
135
+ test('escapeXml escapes special characters', () => {
136
+ expect(service.escapeXml('a&b<c>d"e\'f')).toBe('a&amp;b&lt;c&gt;d&quot;e&apos;f');
137
+ expect(service.escapeXml(null)).toBe('');
138
+ expect(service.escapeXml('')).toBe('');
139
+ });
140
+
141
+ test('formatBytes handles various sizes', () => {
142
+ expect(service.formatBytes(0)).toBe('0 Bytes');
143
+ expect(service.formatBytes(500)).toContain('Bytes');
144
+ expect(service.formatBytes(1024)).toContain('KB');
145
+ expect(service.formatBytes(1048576)).toContain('MB');
146
+ });
147
+
148
+ test('estimateTotalTokens sums content tokens and reference overhead', async () => {
149
+ service.attachmentService.getActiveAttachments.mockResolvedValue([
150
+ { mode: 'content', tokenEstimate: 100 },
151
+ { mode: 'content', tokenEstimate: 200 },
152
+ { mode: 'reference' }
153
+ ]);
154
+ const total = await service.estimateTotalTokens('agent-1');
155
+ expect(total).toBe(320); // 100 + 200 + 20
156
+ });
157
+
158
+ test('estimateTotalTokens handles error', async () => {
159
+ service.attachmentService.getActiveAttachments.mockRejectedValue(new Error('fail'));
160
+ const total = await service.estimateTotalTokens('agent-1');
161
+ expect(total).toBe(0);
162
+ });
163
+
164
+ test('getAttachmentSummary returns summary object', async () => {
165
+ service.attachmentService.getActiveAttachments.mockResolvedValue([
166
+ { fileName: 'a.txt', mode: 'content', size: 1024, tokenEstimate: 50 },
167
+ { fileName: 'b.js', mode: 'reference', size: 2048, tokenEstimate: 0 }
168
+ ]);
169
+ const summary = await service.getAttachmentSummary('agent-1');
170
+ expect(summary.totalActive).toBe(2);
171
+ expect(summary.contentMode).toBe(1);
172
+ expect(summary.referenceMode).toBe(1);
173
+ expect(summary.files).toHaveLength(2);
174
+ });
175
+
176
+ test('getAttachmentSummary handles error with defaults', async () => {
177
+ service.attachmentService.getActiveAttachments.mockRejectedValue(new Error('fail'));
178
+ const summary = await service.getAttachmentSummary('agent-1');
179
+ expect(summary.totalActive).toBe(0);
180
+ expect(summary.files).toEqual([]);
181
+ });
182
+
183
+ test('buildSystemConstraints returns port constraint string', () => {
184
+ registry.getAll.mockReturnValue({
185
+ web: { port: 3000 },
186
+ api: { port: 8080 }
187
+ });
188
+ const result = service.buildSystemConstraints();
189
+ expect(result).toContain('3000');
190
+ expect(result).toContain('8080');
191
+ expect(result).toContain('never kill');
192
+ });
193
+
194
+ test('buildSystemConstraints returns empty when no ports', () => {
195
+ registry.getAll.mockReturnValue({});
196
+ const result = service.buildSystemConstraints();
197
+ expect(result).toBe('');
198
+ });
199
+
200
+ test('buildSystemConstraints handles errors gracefully', () => {
201
+ registry.getAll.mockImplementation(() => { throw new Error('fail'); });
202
+ const result = service.buildSystemConstraints();
203
+ expect(result).toBe('');
204
+ });
205
+ });