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.
- package/package.json +1 -1
- package/src/__test-utils__/fixtures/malformedJson.js +31 -0
- package/src/__test-utils__/globalSetup.js +9 -0
- package/src/__test-utils__/globalTeardown.js +12 -0
- package/src/__test-utils__/mockFactories.js +101 -0
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
- package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
- package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
- package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
- package/src/core/__tests__/agentPool.test.js +601 -0
- package/src/core/__tests__/agentScheduler.test.js +576 -0
- package/src/core/__tests__/contextManager.test.js +252 -0
- package/src/core/__tests__/flowExecutor.test.js +262 -0
- package/src/core/__tests__/messageProcessor.test.js +627 -0
- package/src/core/__tests__/orchestrator.test.js +257 -0
- package/src/core/__tests__/stateManager.test.js +375 -0
- package/src/core/agentPool.js +11 -1
- package/src/index.js +25 -9
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
- package/src/services/__tests__/agentActivityService.test.js +319 -0
- package/src/services/__tests__/apiKeyManager.test.js +206 -0
- package/src/services/__tests__/benchmarkService.test.js +184 -0
- package/src/services/__tests__/budgetService.test.js +211 -0
- package/src/services/__tests__/contextInjectionService.test.js +205 -0
- package/src/services/__tests__/conversationCompactionService.test.js +280 -0
- package/src/services/__tests__/credentialVault.test.js +469 -0
- package/src/services/__tests__/errorHandler.test.js +314 -0
- package/src/services/__tests__/fileAttachmentService.test.js +278 -0
- package/src/services/__tests__/flowContextService.test.js +199 -0
- package/src/services/__tests__/memoryService.test.js +450 -0
- package/src/services/__tests__/modelRouterService.test.js +388 -0
- package/src/services/__tests__/modelsService.test.js +261 -0
- package/src/services/__tests__/portRegistry.test.js +123 -0
- package/src/services/__tests__/projectDetector.test.js +34 -0
- package/src/services/__tests__/promptService.test.js +242 -0
- package/src/services/__tests__/qualityInspector.test.js +97 -0
- package/src/services/__tests__/scheduleService.test.js +308 -0
- package/src/services/__tests__/serviceRegistry.test.js +74 -0
- package/src/services/__tests__/skillsService.test.js +402 -0
- package/src/services/__tests__/tokenCountingService.test.js +48 -0
- package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
- package/src/tools/__tests__/agentDelayTool.test.js +342 -0
- package/src/tools/__tests__/asyncToolManager.test.js +344 -0
- package/src/tools/__tests__/baseTool.test.js +420 -0
- package/src/tools/__tests__/codeMapTool.test.js +348 -0
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
- package/src/tools/__tests__/fileSystemTool.test.js +717 -0
- package/src/tools/__tests__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/helpTool.test.js +204 -0
- package/src/tools/__tests__/jobDoneTool.test.js +296 -0
- package/src/tools/__tests__/memoryTool.test.js +297 -0
- package/src/tools/__tests__/seekTool.test.js +282 -0
- package/src/tools/__tests__/skillsTool.test.js +226 -0
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
- package/src/tools/__tests__/taskManagerTool.test.js +725 -0
- package/src/tools/__tests__/terminalTool.test.js +384 -0
- package/src/tools/__tests__/userPromptTool.test.js +297 -0
- package/src/tools/__tests__/webTool.e2e.test.js +25 -11
- package/src/tools/webTool.js +6 -12
- package/src/types/__tests__/agent.test.js +499 -0
- package/src/types/__tests__/contextReference.test.js +606 -0
- package/src/types/__tests__/conversation.test.js +555 -0
- package/src/types/__tests__/toolCommand.test.js +584 -0
- package/src/types/contextReference.js +1 -1
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
- package/src/utilities/__tests__/configManager.test.js +397 -0
- package/src/utilities/__tests__/constants.test.js +49 -0
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
- package/src/utilities/__tests__/fileProcessor.test.js +104 -0
- package/src/utilities/__tests__/jsonRepair.test.js +104 -0
- package/src/utilities/__tests__/logger.test.js +129 -0
- package/src/utilities/__tests__/platformUtils.test.js +87 -0
- package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
- package/src/utilities/__tests__/tagParser.test.js +887 -0
- package/src/utilities/__tests__/toolConstants.test.js +94 -0
- package/src/utilities/tagParser.js +2 -2
- package/src/tools/browserTool.js +0 -897
- package/src/utilities/platformUtils.test.js +0 -98
- /package/src/tools/{filesystemTool.js → fileSystemTool.js} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock fs, child_process, and userDataDir before importing
|
|
5
|
+
jest.unstable_mockModule('fs', () => ({
|
|
6
|
+
promises: {
|
|
7
|
+
readFile: jest.fn().mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })),
|
|
8
|
+
writeFile: jest.fn().mockResolvedValue(undefined),
|
|
9
|
+
mkdir: jest.fn().mockResolvedValue(undefined),
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
14
|
+
execSync: jest.fn().mockReturnValue('')
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
|
|
18
|
+
getUserDataDir: jest.fn().mockReturnValue('/tmp/test-loxia-data')
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { default: ScheduleService, CRON_PRESETS, parseCron, cronMatchesDate, getNextCronDate } = await import('../scheduleService.js');
|
|
22
|
+
|
|
23
|
+
describe('parseCron', () => {
|
|
24
|
+
test('parses wildcard fields to full range', () => {
|
|
25
|
+
const parsed = parseCron('* * * * *');
|
|
26
|
+
expect(parsed.minutes.size).toBe(60);
|
|
27
|
+
expect(parsed.hours.size).toBe(24);
|
|
28
|
+
expect(parsed.daysOfMonth.size).toBe(31);
|
|
29
|
+
expect(parsed.months.size).toBe(12);
|
|
30
|
+
expect(parsed.daysOfWeek.size).toBe(7);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('parses step values (*/15)', () => {
|
|
34
|
+
const parsed = parseCron('*/15 * * * *');
|
|
35
|
+
expect([...parsed.minutes].sort((a, b) => a - b)).toEqual([0, 15, 30, 45]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('parses ranges (1-5)', () => {
|
|
39
|
+
const parsed = parseCron('0 9 * * 1-5');
|
|
40
|
+
expect([...parsed.daysOfWeek].sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('parses lists (0,6)', () => {
|
|
44
|
+
const parsed = parseCron('0 10 * * 0,6');
|
|
45
|
+
expect([...parsed.daysOfWeek].sort((a, b) => a - b)).toEqual([0, 6]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('parses specific numeric values', () => {
|
|
49
|
+
const parsed = parseCron('30 8 15 6 3');
|
|
50
|
+
expect([...parsed.minutes]).toEqual([30]);
|
|
51
|
+
expect([...parsed.hours]).toEqual([8]);
|
|
52
|
+
expect([...parsed.daysOfMonth]).toEqual([15]);
|
|
53
|
+
expect([...parsed.months]).toEqual([6]);
|
|
54
|
+
expect([...parsed.daysOfWeek]).toEqual([3]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('parses step with range (1-10/2)', () => {
|
|
58
|
+
const parsed = parseCron('1-10/2 * * * *');
|
|
59
|
+
expect([...parsed.minutes].sort((a, b) => a - b)).toEqual([1, 3, 5, 7, 9]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('parses step with specific start (5/10)', () => {
|
|
63
|
+
const parsed = parseCron('5/10 * * * *');
|
|
64
|
+
expect([...parsed.minutes].sort((a, b) => a - b)).toEqual([5, 15, 25, 35, 45, 55]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('throws on invalid cron expression (wrong field count)', () => {
|
|
68
|
+
expect(() => parseCron('* * *')).toThrow('expected 5 fields');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('cronMatchesDate', () => {
|
|
73
|
+
test('returns true when date matches all fields', () => {
|
|
74
|
+
// 2026-01-05 is a Monday (day 1), month 1, day 5
|
|
75
|
+
const date = new Date(2026, 0, 5, 9, 0);
|
|
76
|
+
const parsed = parseCron('0 9 5 1 1');
|
|
77
|
+
expect(cronMatchesDate(parsed, date)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns false when minute does not match', () => {
|
|
81
|
+
const date = new Date(2026, 0, 5, 9, 30);
|
|
82
|
+
const parsed = parseCron('0 9 5 1 1');
|
|
83
|
+
expect(cronMatchesDate(parsed, date)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('matches every-minute wildcard for any date', () => {
|
|
87
|
+
const date = new Date(2026, 5, 15, 14, 37);
|
|
88
|
+
const parsed = parseCron('* * * * *');
|
|
89
|
+
expect(cronMatchesDate(parsed, date)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('returns false when hour does not match', () => {
|
|
93
|
+
const date = new Date(2026, 0, 5, 10, 0);
|
|
94
|
+
const parsed = parseCron('0 9 * * *');
|
|
95
|
+
expect(cronMatchesDate(parsed, date)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('getNextCronDate', () => {
|
|
100
|
+
test('returns next matching date in the future', () => {
|
|
101
|
+
const parsed = parseCron('0 9 * * *'); // daily at 9:00
|
|
102
|
+
const after = new Date(2026, 0, 1, 10, 0);
|
|
103
|
+
const next = getNextCronDate(parsed, after);
|
|
104
|
+
expect(next).not.toBeNull();
|
|
105
|
+
expect(next.getHours()).toBe(9);
|
|
106
|
+
expect(next.getMinutes()).toBe(0);
|
|
107
|
+
expect(next.getDate()).toBe(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('returns null for impossible date like Feb 31', () => {
|
|
111
|
+
const parsed = parseCron('0 0 31 2 *');
|
|
112
|
+
const next = getNextCronDate(parsed);
|
|
113
|
+
expect(next).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('returns a Date instance for every-minute', () => {
|
|
117
|
+
const parsed = parseCron('* * * * *');
|
|
118
|
+
const now = new Date();
|
|
119
|
+
const next = getNextCronDate(parsed, now);
|
|
120
|
+
expect(next).toBeInstanceOf(Date);
|
|
121
|
+
expect(next.getTime()).toBeGreaterThan(now.getTime());
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('CRON_PRESETS', () => {
|
|
126
|
+
test('contains expected preset keys and values', () => {
|
|
127
|
+
expect(CRON_PRESETS['every-minute']).toBe('* * * * *');
|
|
128
|
+
expect(CRON_PRESETS['every-5-minutes']).toBe('*/5 * * * *');
|
|
129
|
+
expect(CRON_PRESETS['daily']).toBe('0 9 * * *');
|
|
130
|
+
expect(CRON_PRESETS['weekdays']).toBe('0 9 * * 1-5');
|
|
131
|
+
expect(CRON_PRESETS['weekends']).toBe('0 10 * * 0,6');
|
|
132
|
+
expect(CRON_PRESETS['monthly']).toBe('0 9 1 * *');
|
|
133
|
+
expect(CRON_PRESETS['weekly-monday']).toBe('0 9 * * 1');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('all presets are valid parseable cron expressions', () => {
|
|
137
|
+
for (const [, expr] of Object.entries(CRON_PRESETS)) {
|
|
138
|
+
expect(() => parseCron(expr)).not.toThrow();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('ScheduleService', () => {
|
|
144
|
+
let service;
|
|
145
|
+
let logger;
|
|
146
|
+
|
|
147
|
+
beforeEach(async () => {
|
|
148
|
+
logger = createMockLogger();
|
|
149
|
+
service = new ScheduleService(logger);
|
|
150
|
+
await service.initialize();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('createSchedule creates and stores a schedule with correct fields', async () => {
|
|
154
|
+
const schedule = await service.createSchedule({
|
|
155
|
+
name: 'Test Schedule',
|
|
156
|
+
prompt: 'Do something',
|
|
157
|
+
targetType: 'agent',
|
|
158
|
+
targetId: 'agent-1',
|
|
159
|
+
cronExpression: '0 9 * * *'
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(schedule.id).toMatch(/^schedule-/);
|
|
163
|
+
expect(schedule.name).toBe('Test Schedule');
|
|
164
|
+
expect(schedule.enabled).toBe(true);
|
|
165
|
+
expect(schedule.cronExpression).toBe('0 9 * * *');
|
|
166
|
+
expect(schedule.nextRun).toBeDefined();
|
|
167
|
+
expect(schedule.runCount).toBe(0);
|
|
168
|
+
expect(schedule.lastRun).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('createSchedule resolves cron presets to actual expressions', async () => {
|
|
172
|
+
const schedule = await service.createSchedule({
|
|
173
|
+
name: 'Daily',
|
|
174
|
+
prompt: 'Hello',
|
|
175
|
+
targetType: 'agent',
|
|
176
|
+
targetId: 'a1',
|
|
177
|
+
cronExpression: 'daily'
|
|
178
|
+
});
|
|
179
|
+
expect(schedule.cronExpression).toBe('0 9 * * *');
|
|
180
|
+
expect(schedule.cronPreset).toBe('daily');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('createSchedule throws on missing name', async () => {
|
|
184
|
+
await expect(service.createSchedule({ prompt: 'x', targetType: 'agent', targetId: 'a', cronExpression: '* * * * *' }))
|
|
185
|
+
.rejects.toThrow('Schedule name is required');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('createSchedule throws on missing prompt', async () => {
|
|
189
|
+
await expect(service.createSchedule({ name: 'x' })).rejects.toThrow('Prompt is required');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('createSchedule throws on invalid targetType', async () => {
|
|
193
|
+
await expect(service.createSchedule({
|
|
194
|
+
name: 'x', prompt: 'y', targetType: 'invalid', targetId: 'z', cronExpression: '* * * * *'
|
|
195
|
+
})).rejects.toThrow('targetType must be');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('createSchedule throws on missing targetId', async () => {
|
|
199
|
+
await expect(service.createSchedule({
|
|
200
|
+
name: 'x', prompt: 'y', targetType: 'agent', cronExpression: '* * * * *'
|
|
201
|
+
})).rejects.toThrow('targetId is required');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('createSchedule throws on missing cronExpression', async () => {
|
|
205
|
+
await expect(service.createSchedule({
|
|
206
|
+
name: 'x', prompt: 'y', targetType: 'agent', targetId: 'a1'
|
|
207
|
+
})).rejects.toThrow('cronExpression is required');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('getSchedule returns schedule by id or null', async () => {
|
|
211
|
+
const created = await service.createSchedule({
|
|
212
|
+
name: 'Get Test', prompt: 'p', targetType: 'flow', targetId: 'f1', cronExpression: '* * * * *'
|
|
213
|
+
});
|
|
214
|
+
expect(service.getSchedule(created.id)).toBe(created);
|
|
215
|
+
expect(service.getSchedule('nonexistent')).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('deleteSchedule removes the schedule', async () => {
|
|
219
|
+
const created = await service.createSchedule({
|
|
220
|
+
name: 'Del Test', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '* * * * *'
|
|
221
|
+
});
|
|
222
|
+
await service.deleteSchedule(created.id);
|
|
223
|
+
expect(service.getSchedule(created.id)).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('deleteSchedule throws on nonexistent id', async () => {
|
|
227
|
+
await expect(service.deleteSchedule('nope')).rejects.toThrow('Schedule not found');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('listSchedules returns all schedules sorted by createdAt descending', async () => {
|
|
231
|
+
await service.createSchedule({
|
|
232
|
+
name: 'A', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '* * * * *'
|
|
233
|
+
});
|
|
234
|
+
await service.createSchedule({
|
|
235
|
+
name: 'B', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '* * * * *'
|
|
236
|
+
});
|
|
237
|
+
const list = service.listSchedules();
|
|
238
|
+
expect(list.length).toBe(2);
|
|
239
|
+
const names = list.map(s => s.name);
|
|
240
|
+
expect(names).toContain('A');
|
|
241
|
+
expect(names).toContain('B');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('updateSchedule modifies allowed fields', async () => {
|
|
245
|
+
const created = await service.createSchedule({
|
|
246
|
+
name: 'Up Test', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '* * * * *'
|
|
247
|
+
});
|
|
248
|
+
const updated = await service.updateSchedule(created.id, { name: 'Updated Name', enabled: false });
|
|
249
|
+
expect(updated.name).toBe('Updated Name');
|
|
250
|
+
expect(updated.enabled).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('updateSchedule throws on nonexistent id', async () => {
|
|
254
|
+
await expect(service.updateSchedule('nope', {})).rejects.toThrow('Schedule not found');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('updateSchedule recalculates nextRun when cron changes', async () => {
|
|
258
|
+
const created = await service.createSchedule({
|
|
259
|
+
name: 'Cron Change', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '* * * * *'
|
|
260
|
+
});
|
|
261
|
+
const updated = await service.updateSchedule(created.id, { cronExpression: '0 12 * * *' });
|
|
262
|
+
expect(updated.cronExpression).toBe('0 12 * * *');
|
|
263
|
+
expect(updated.nextRun).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('updateSchedule recalculates nextRun when re-enabling', async () => {
|
|
267
|
+
const created = await service.createSchedule({
|
|
268
|
+
name: 'Re-enable', prompt: 'p', targetType: 'agent', targetId: 'a1', cronExpression: '0 9 * * *', enabled: false
|
|
269
|
+
});
|
|
270
|
+
const updated = await service.updateSchedule(created.id, { enabled: true });
|
|
271
|
+
expect(updated.enabled).toBe(true);
|
|
272
|
+
expect(updated.nextRun).toBeDefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('start and stop manage the check timer', () => {
|
|
276
|
+
service.start();
|
|
277
|
+
expect(service.checkTimer).not.toBeNull();
|
|
278
|
+
service.stop();
|
|
279
|
+
expect(service.checkTimer).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('start is idempotent (calling twice does not duplicate timers)', () => {
|
|
283
|
+
service.start();
|
|
284
|
+
const timer1 = service.checkTimer;
|
|
285
|
+
service.start();
|
|
286
|
+
expect(service.checkTimer).toBe(timer1);
|
|
287
|
+
service.stop();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('getPresets returns a copy of CRON_PRESETS', () => {
|
|
291
|
+
const presets = service.getPresets();
|
|
292
|
+
expect(presets).toEqual(CRON_PRESETS);
|
|
293
|
+
presets['custom'] = 'modified';
|
|
294
|
+
expect(CRON_PRESETS['custom']).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('dependency injection setters store references', () => {
|
|
298
|
+
const pool = {};
|
|
299
|
+
const mp = {};
|
|
300
|
+
const fe = {};
|
|
301
|
+
service.setAgentPool(pool);
|
|
302
|
+
service.setMessageProcessor(mp);
|
|
303
|
+
service.setFlowExecutor(fe);
|
|
304
|
+
expect(service.agentPool).toBe(pool);
|
|
305
|
+
expect(service.messageProcessor).toBe(mp);
|
|
306
|
+
expect(service.flowExecutor).toBe(fe);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
// Mock portRegistry to prevent file system access
|
|
4
|
+
jest.unstable_mockModule('../portRegistry.js', () => ({
|
|
5
|
+
getPortRegistry: jest.fn(() => ({
|
|
6
|
+
registerService: jest.fn().mockResolvedValue(undefined),
|
|
7
|
+
unregisterService: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
getAllServices: jest.fn().mockResolvedValue({}),
|
|
9
|
+
getService: jest.fn().mockResolvedValue(null),
|
|
10
|
+
cleanupStaleEntries: jest.fn().mockResolvedValue(undefined)
|
|
11
|
+
}))
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const { ServiceRegistry, ServiceStatus, registry } = await import('../serviceRegistry.js');
|
|
15
|
+
|
|
16
|
+
describe('ServiceRegistry', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Clear any registered services between tests
|
|
19
|
+
registry.clear();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('ServiceStatus has STARTING, RUNNING, STOPPED values', () => {
|
|
23
|
+
expect(ServiceStatus.STARTING).toBe('starting');
|
|
24
|
+
expect(ServiceStatus.RUNNING).toBe('running');
|
|
25
|
+
expect(ServiceStatus.STOPPED).toBe('stopped');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('registry is a ServiceRegistry instance', () => {
|
|
29
|
+
expect(registry).toBeInstanceOf(ServiceRegistry);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('register adds a service retrievable by get', () => {
|
|
33
|
+
const info = registry.register('test-service', {
|
|
34
|
+
port: 3456,
|
|
35
|
+
host: 'localhost',
|
|
36
|
+
persistToFile: false
|
|
37
|
+
});
|
|
38
|
+
expect(info).toHaveProperty('name', 'test-service');
|
|
39
|
+
expect(info).toHaveProperty('port', 3456);
|
|
40
|
+
expect(info).toHaveProperty('url');
|
|
41
|
+
|
|
42
|
+
const retrieved = registry.get('test-service');
|
|
43
|
+
expect(retrieved).not.toBeNull();
|
|
44
|
+
expect(retrieved.port).toBe(3456);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('unregister removes the service', () => {
|
|
48
|
+
registry.register('temp-service', { port: 9999, persistToFile: false });
|
|
49
|
+
expect(registry.get('temp-service')).not.toBeNull();
|
|
50
|
+
|
|
51
|
+
const removed = registry.unregister('temp-service', false);
|
|
52
|
+
expect(removed).toBe(true);
|
|
53
|
+
expect(registry.get('temp-service')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('getStats returns object with totalServices', () => {
|
|
57
|
+
registry.register('svc-a', { port: 1111, persistToFile: false });
|
|
58
|
+
registry.register('svc-b', { port: 2222, persistToFile: false });
|
|
59
|
+
const stats = registry.getStats();
|
|
60
|
+
expect(stats).toHaveProperty('totalServices');
|
|
61
|
+
expect(stats.totalServices).toBe(2);
|
|
62
|
+
expect(stats).toHaveProperty('runningServices');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('clear removes all services', () => {
|
|
66
|
+
registry.register('svc-1', { port: 4001, persistToFile: false });
|
|
67
|
+
registry.register('svc-2', { port: 4002, persistToFile: false });
|
|
68
|
+
expect(registry.getStats().totalServices).toBe(2);
|
|
69
|
+
|
|
70
|
+
registry.clear();
|
|
71
|
+
expect(registry.getStats().totalServices).toBe(0);
|
|
72
|
+
expect(registry.get('svc-1')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|