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.
- 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__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/filesystemTool.test.js +717 -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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger, createMockConfig, createMockAiService, createMockStateManager, createMockAgentPool } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock constants so the module loads without side effects
|
|
5
|
+
jest.unstable_mockModule('../../utilities/constants.js', () => ({
|
|
6
|
+
INTERFACE_TYPES: { CLI: 'cli', WEB: 'web', VSCODE: 'vscode' },
|
|
7
|
+
AGENT_TYPES: { USER_CREATED: 'user-created', SYSTEM_AGENT: 'system-agent' },
|
|
8
|
+
AGENT_STATUS: { ACTIVE: 'active', IDLE: 'idle', BUSY: 'busy', PAUSED: 'paused', ERROR: 'error' },
|
|
9
|
+
MESSAGE_MODES: { CHAT: 'chat', AGENT: 'agent' },
|
|
10
|
+
ORCHESTRATOR_ACTIONS: {
|
|
11
|
+
CREATE_AGENT: 'create_agent',
|
|
12
|
+
UPDATE_AGENT: 'update_agent',
|
|
13
|
+
DELETE_AGENT: 'delete_agent',
|
|
14
|
+
UNLOAD_AGENT: 'unload_agent',
|
|
15
|
+
SEND_MESSAGE: 'send_message',
|
|
16
|
+
LIST_AGENTS: 'list_agents',
|
|
17
|
+
RESUME_SESSION: 'resume_session',
|
|
18
|
+
GET_SESSION_STATE: 'get_session_state',
|
|
19
|
+
PAUSE_AGENT: 'pause_agent',
|
|
20
|
+
RESUME_AGENT: 'resume_agent',
|
|
21
|
+
SWITCH_MODEL: 'switch_model',
|
|
22
|
+
GET_AGENT_STATUS: 'get_agent_status',
|
|
23
|
+
GET_AGENT_CONVERSATIONS: 'get_agent_conversations'
|
|
24
|
+
},
|
|
25
|
+
SYSTEM_DEFAULTS: {}
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const { default: Orchestrator } = await import('../orchestrator.js');
|
|
29
|
+
|
|
30
|
+
describe('Orchestrator', () => {
|
|
31
|
+
let orchestrator;
|
|
32
|
+
let mockConfig;
|
|
33
|
+
let mockLogger;
|
|
34
|
+
let mockAgentPool;
|
|
35
|
+
let mockMessageProcessor;
|
|
36
|
+
let mockAiService;
|
|
37
|
+
let mockStateManager;
|
|
38
|
+
|
|
39
|
+
const makeRequest = (overrides = {}) => ({
|
|
40
|
+
interface: 'web',
|
|
41
|
+
sessionId: 'session-1',
|
|
42
|
+
action: 'send_message',
|
|
43
|
+
payload: {},
|
|
44
|
+
projectDir: '/test/project',
|
|
45
|
+
...overrides
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
|
|
51
|
+
mockConfig = createMockConfig();
|
|
52
|
+
mockLogger = createMockLogger();
|
|
53
|
+
mockAiService = createMockAiService();
|
|
54
|
+
mockStateManager = createMockStateManager();
|
|
55
|
+
mockAgentPool = createMockAgentPool();
|
|
56
|
+
|
|
57
|
+
// Add methods Orchestrator actually calls
|
|
58
|
+
mockAgentPool.listActiveAgents = jest.fn().mockResolvedValue([]);
|
|
59
|
+
mockAgentPool.pauseAgent = jest.fn().mockResolvedValue({ success: true });
|
|
60
|
+
mockAgentPool.resumeAgent = jest.fn().mockResolvedValue({ success: true });
|
|
61
|
+
mockAgentPool.restoreAgent = jest.fn().mockResolvedValue(undefined);
|
|
62
|
+
mockAgentPool.deleteAgent = jest.fn().mockResolvedValue({ success: true });
|
|
63
|
+
mockAgentPool.unloadAgent = jest.fn().mockResolvedValue({ success: true });
|
|
64
|
+
|
|
65
|
+
mockMessageProcessor = {
|
|
66
|
+
processMessage: jest.fn().mockResolvedValue({
|
|
67
|
+
success: true,
|
|
68
|
+
agentId: 'agent-1',
|
|
69
|
+
queuedAt: new Date().toISOString()
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
orchestrator = new Orchestrator(
|
|
74
|
+
mockConfig,
|
|
75
|
+
mockLogger,
|
|
76
|
+
mockAgentPool,
|
|
77
|
+
mockMessageProcessor,
|
|
78
|
+
mockAiService,
|
|
79
|
+
mockStateManager
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ───── Request Routing ─────
|
|
84
|
+
|
|
85
|
+
describe('processRequest routing', () => {
|
|
86
|
+
test('CREATE_AGENT action delegates to createAgent', async () => {
|
|
87
|
+
const createdAgent = { id: 'new-1', name: 'Builder', preferredModel: 'gpt-4' };
|
|
88
|
+
mockAgentPool.createAgent.mockResolvedValue(createdAgent);
|
|
89
|
+
|
|
90
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
91
|
+
action: 'create_agent',
|
|
92
|
+
payload: { name: 'Builder', systemPrompt: 'You build things', model: 'gpt-4' }
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
expect(response.success).toBe(true);
|
|
96
|
+
expect(response.data.id).toBe('new-1');
|
|
97
|
+
expect(mockAgentPool.createAgent).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('SEND_MESSAGE action delegates to routeToAgent', async () => {
|
|
101
|
+
const agent = {
|
|
102
|
+
id: 'agent-1', name: 'Agent', status: 'active',
|
|
103
|
+
currentModel: 'gpt-4', mode: 'chat'
|
|
104
|
+
};
|
|
105
|
+
mockAgentPool._agents.set('agent-1', agent);
|
|
106
|
+
mockAgentPool.getAgent.mockImplementation((id) => mockAgentPool._agents.get(id));
|
|
107
|
+
|
|
108
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
109
|
+
action: 'send_message',
|
|
110
|
+
payload: { agentId: 'agent-1', message: 'Hello agent' }
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
expect(response.success).toBe(true);
|
|
114
|
+
expect(mockMessageProcessor.processMessage).toHaveBeenCalledWith(
|
|
115
|
+
'agent-1', 'Hello agent', expect.objectContaining({ sessionId: 'session-1' })
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('LIST_AGENTS action returns agent list', async () => {
|
|
120
|
+
const agents = [{ id: 'a1', name: 'Alpha' }, { id: 'a2', name: 'Beta' }];
|
|
121
|
+
mockAgentPool.listActiveAgents.mockResolvedValue(agents);
|
|
122
|
+
|
|
123
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
124
|
+
action: 'list_agents',
|
|
125
|
+
payload: {}
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
expect(response.success).toBe(true);
|
|
129
|
+
expect(response.data).toEqual(agents);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('unknown action returns error response', async () => {
|
|
133
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
134
|
+
action: 'totally_invalid_action',
|
|
135
|
+
payload: {}
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
expect(response.success).toBe(false);
|
|
139
|
+
expect(response.error).toContain('Unknown action');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('response includes metadata with timestamp, executionTime, sessionId', async () => {
|
|
143
|
+
mockAgentPool.listActiveAgents.mockResolvedValue([]);
|
|
144
|
+
|
|
145
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
146
|
+
action: 'list_agents',
|
|
147
|
+
payload: {}
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
expect(response.metadata).toBeDefined();
|
|
151
|
+
expect(response.metadata.timestamp).toBeDefined();
|
|
152
|
+
expect(typeof response.metadata.executionTime).toBe('number');
|
|
153
|
+
expect(response.metadata.sessionId).toBe('session-1');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ───── Session Management ─────
|
|
158
|
+
|
|
159
|
+
describe('session management', () => {
|
|
160
|
+
test('_ensureSession creates new session on first call', async () => {
|
|
161
|
+
expect(orchestrator.activeSessions.size).toBe(0);
|
|
162
|
+
|
|
163
|
+
await orchestrator._ensureSession('s-new', '/project');
|
|
164
|
+
|
|
165
|
+
expect(orchestrator.activeSessions.has('s-new')).toBe(true);
|
|
166
|
+
const session = orchestrator.activeSessions.get('s-new');
|
|
167
|
+
expect(session.projectDir).toBe('/project');
|
|
168
|
+
expect(session.createdAt).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('_ensureSession reuses existing session on subsequent calls', async () => {
|
|
172
|
+
await orchestrator._ensureSession('s-reuse', '/project');
|
|
173
|
+
const firstCreated = orchestrator.activeSessions.get('s-reuse').createdAt;
|
|
174
|
+
|
|
175
|
+
// Small delay to differentiate timestamps
|
|
176
|
+
await new Promise(r => setTimeout(r, 5));
|
|
177
|
+
await orchestrator._ensureSession('s-reuse', '/project');
|
|
178
|
+
|
|
179
|
+
expect(orchestrator.activeSessions.size).toBe(1);
|
|
180
|
+
// createdAt should remain unchanged
|
|
181
|
+
expect(orchestrator.activeSessions.get('s-reuse').createdAt).toBe(firstCreated);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ───── Agent Operations ─────
|
|
186
|
+
|
|
187
|
+
describe('agent operations', () => {
|
|
188
|
+
test('routeToAgent with valid agent returns queued response', async () => {
|
|
189
|
+
const agent = { id: 'agent-1', name: 'Bot', status: 'active', currentModel: 'gpt-4' };
|
|
190
|
+
mockAgentPool.getAgent.mockResolvedValue(agent);
|
|
191
|
+
|
|
192
|
+
const result = await orchestrator.routeToAgent('agent-1', 'Do something', { sessionId: 's1' });
|
|
193
|
+
|
|
194
|
+
expect(result.success).toBe(true);
|
|
195
|
+
expect(result.data.status).toBe('queued');
|
|
196
|
+
expect(result.data.agentId).toBe('agent-1');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('routeToAgent with paused agent throws error', async () => {
|
|
200
|
+
const futureDate = new Date(Date.now() + 60000).toISOString();
|
|
201
|
+
const agent = { id: 'agent-1', name: 'Bot', status: 'paused', pausedUntil: futureDate };
|
|
202
|
+
mockAgentPool.getAgent.mockResolvedValue(agent);
|
|
203
|
+
|
|
204
|
+
await expect(
|
|
205
|
+
orchestrator.routeToAgent('agent-1', 'Hey', {})
|
|
206
|
+
).rejects.toThrow(/paused/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('createAgent delegates to agentPool.createAgent', async () => {
|
|
210
|
+
const created = { id: 'new-a', name: 'Fresh', preferredModel: 'claude-3' };
|
|
211
|
+
mockAgentPool.createAgent.mockResolvedValue(created);
|
|
212
|
+
|
|
213
|
+
const result = await orchestrator.createAgent('You are helpful', 'claude-3', { name: 'Fresh' });
|
|
214
|
+
|
|
215
|
+
expect(result.id).toBe('new-a');
|
|
216
|
+
expect(mockAgentPool.createAgent).toHaveBeenCalledWith(
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
name: 'Fresh',
|
|
219
|
+
systemPrompt: 'You are helpful',
|
|
220
|
+
preferredModel: 'claude-3'
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ───── Error Handling ─────
|
|
227
|
+
|
|
228
|
+
describe('error handling', () => {
|
|
229
|
+
test('processRequest catches errors and returns error response', async () => {
|
|
230
|
+
mockAgentPool.listActiveAgents.mockRejectedValue(new Error('pool exploded'));
|
|
231
|
+
|
|
232
|
+
const response = await orchestrator.processRequest(makeRequest({
|
|
233
|
+
action: 'list_agents',
|
|
234
|
+
payload: {}
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
expect(response.success).toBe(false);
|
|
238
|
+
expect(response.error).toBe('pool exploded');
|
|
239
|
+
expect(response.metadata.timestamp).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('shutdown persists all agents and clears sessions', async () => {
|
|
243
|
+
const agents = [{ id: 'a1' }, { id: 'a2' }];
|
|
244
|
+
mockAgentPool.listActiveAgents.mockResolvedValue(agents);
|
|
245
|
+
|
|
246
|
+
// Pre-populate a session
|
|
247
|
+
orchestrator.activeSessions.set('s1', { id: 's1' });
|
|
248
|
+
|
|
249
|
+
await orchestrator.shutdown();
|
|
250
|
+
|
|
251
|
+
expect(mockStateManager.persistAgentState).toHaveBeenCalledTimes(2);
|
|
252
|
+
expect(mockStateManager.persistAgentState).toHaveBeenCalledWith('a1');
|
|
253
|
+
expect(mockStateManager.persistAgentState).toHaveBeenCalledWith('a2');
|
|
254
|
+
expect(orchestrator.activeSessions.size).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateManager - Comprehensive unit tests (target: 80%+ line coverage)
|
|
3
|
+
* Tests directory initialization, state persistence, project state management,
|
|
4
|
+
* agent index operations, resume/restore flows, and error handling.
|
|
5
|
+
*/
|
|
6
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
7
|
+
import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';
|
|
8
|
+
|
|
9
|
+
// ── Mock fs ──────────────────────────────────────────────────────────────────
|
|
10
|
+
const mockMkdir = jest.fn().mockResolvedValue(undefined);
|
|
11
|
+
const mockReadFile = jest.fn().mockResolvedValue('{}');
|
|
12
|
+
const mockWriteFile = jest.fn().mockResolvedValue(undefined);
|
|
13
|
+
const mockUnlink = jest.fn().mockResolvedValue(undefined);
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule('fs', () => ({
|
|
16
|
+
promises: {
|
|
17
|
+
mkdir: mockMkdir,
|
|
18
|
+
readFile: mockReadFile,
|
|
19
|
+
writeFile: mockWriteFile,
|
|
20
|
+
unlink: mockUnlink
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// ── Mock userDataDir ─────────────────────────────────────────────────────────
|
|
25
|
+
const mockGetUserDataPaths = jest.fn().mockReturnValue({
|
|
26
|
+
base: '/mock/userdata',
|
|
27
|
+
state: '/mock/userdata/state',
|
|
28
|
+
agents: '/mock/userdata/state/agents',
|
|
29
|
+
logs: '/mock/userdata/logs'
|
|
30
|
+
});
|
|
31
|
+
const mockEnsureUserDataDirs = jest.fn().mockResolvedValue({
|
|
32
|
+
base: '/mock/userdata',
|
|
33
|
+
state: '/mock/userdata/state',
|
|
34
|
+
agents: '/mock/userdata/state/agents'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
|
|
38
|
+
getUserDataPaths: mockGetUserDataPaths,
|
|
39
|
+
ensureUserDataDirs: mockEnsureUserDataDirs
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const { default: StateManager } = await import('../stateManager.js');
|
|
43
|
+
|
|
44
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
45
|
+
function makeSM(configOverrides = {}) {
|
|
46
|
+
const config = createMockConfig(configOverrides);
|
|
47
|
+
const logger = createMockLogger();
|
|
48
|
+
const sm = new StateManager(config, logger);
|
|
49
|
+
return { sm, config, logger };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
53
|
+
describe('StateManager', () => {
|
|
54
|
+
let sm, logger;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
({ sm, logger } = makeSM());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ─── Constructor ───────────────────────────────────────────────────────
|
|
62
|
+
describe('constructor', () => {
|
|
63
|
+
test('sets stateDirectory from userDataPaths', () => {
|
|
64
|
+
expect(sm.stateDirectory).toBe('/mock/userdata/state');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('sets state file paths', () => {
|
|
68
|
+
expect(sm.stateFiles.projectState).toBe('project-state.json');
|
|
69
|
+
expect(sm.stateFiles.agentIndex).toBe('agent-index.json');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('sets stateVersion', () => {
|
|
73
|
+
expect(sm.stateVersion).toBe('1.0.0');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── getStateDir / getAgentsDir ────────────────────────────────────────
|
|
78
|
+
describe('getStateDir', () => {
|
|
79
|
+
test('returns persistent state directory (ignores projectDir)', () => {
|
|
80
|
+
expect(sm.getStateDir('/some/project')).toBe('/mock/userdata/state');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getAgentsDir', () => {
|
|
85
|
+
test('returns agents directory', () => {
|
|
86
|
+
expect(sm.getAgentsDir()).toBe('/mock/userdata/state/agents');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── initializeStateDirectory ──────────────────────────────────────────
|
|
91
|
+
describe('initializeStateDirectory', () => {
|
|
92
|
+
test('calls ensureUserDataDirs', async () => {
|
|
93
|
+
await sm.initializeStateDirectory('/project');
|
|
94
|
+
expect(mockEnsureUserDataDirs).toHaveBeenCalled();
|
|
95
|
+
expect(logger.info).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('throws and logs error when ensureUserDataDirs fails', async () => {
|
|
99
|
+
mockEnsureUserDataDirs.mockRejectedValueOnce(new Error('disk full'));
|
|
100
|
+
await expect(sm.initializeStateDirectory('/project')).rejects.toThrow('disk full');
|
|
101
|
+
expect(logger.error).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── loadProjectState ──────────────────────────────────────────────────
|
|
106
|
+
describe('loadProjectState', () => {
|
|
107
|
+
test('loads and returns project state from JSON file', async () => {
|
|
108
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0', projectDir: '/p' }));
|
|
109
|
+
const state = await sm.loadProjectState('/project');
|
|
110
|
+
expect(state.version).toBe('1.0.0');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('creates default state when file does not exist', async () => {
|
|
114
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
115
|
+
const state = await sm.loadProjectState('/project');
|
|
116
|
+
expect(state.version).toBe('1.0.0');
|
|
117
|
+
expect(state.activeAgents).toEqual([]);
|
|
118
|
+
// Should have saved the default state
|
|
119
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── saveProjectState ──────────────────────────────────────────────────
|
|
124
|
+
describe('saveProjectState', () => {
|
|
125
|
+
test('writes project state JSON to disk', async () => {
|
|
126
|
+
await sm.saveProjectState('/project', { foo: 'bar' });
|
|
127
|
+
expect(mockMkdir).toHaveBeenCalled();
|
|
128
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
129
|
+
const writtenData = JSON.parse(mockWriteFile.mock.calls[0][1]);
|
|
130
|
+
expect(writtenData.foo).toBe('bar');
|
|
131
|
+
expect(writtenData.lastModified).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── getProjectState ──────────────────────────────────────────────────
|
|
136
|
+
describe('getProjectState', () => {
|
|
137
|
+
test('delegates to loadProjectState', async () => {
|
|
138
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
|
|
139
|
+
const state = await sm.getProjectState('/project');
|
|
140
|
+
expect(state.version).toBe('1.0.0');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ─── loadAgentIndex ────────────────────────────────────────────────────
|
|
145
|
+
describe('loadAgentIndex', () => {
|
|
146
|
+
test('returns parsed agent index', async () => {
|
|
147
|
+
const index = { 'agent-1': { name: 'Agent1' } };
|
|
148
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify(index));
|
|
149
|
+
const result = await sm.loadAgentIndex('/project');
|
|
150
|
+
expect(result['agent-1'].name).toBe('Agent1');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('returns empty object when file missing', async () => {
|
|
154
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
155
|
+
const result = await sm.loadAgentIndex('/project');
|
|
156
|
+
expect(result).toEqual({});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── updateAgentIndex ──────────────────────────────────────────────────
|
|
161
|
+
describe('updateAgentIndex', () => {
|
|
162
|
+
test('adds agent info to index and saves', async () => {
|
|
163
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({}));
|
|
164
|
+
const agent = {
|
|
165
|
+
id: 'agent-test',
|
|
166
|
+
name: 'Test',
|
|
167
|
+
type: 'user-created',
|
|
168
|
+
lastActivity: new Date().toISOString(),
|
|
169
|
+
currentModel: 'model-x',
|
|
170
|
+
status: 'active',
|
|
171
|
+
capabilities: ['terminal']
|
|
172
|
+
};
|
|
173
|
+
await sm.updateAgentIndex(agent, '/project');
|
|
174
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
175
|
+
const written = JSON.parse(mockWriteFile.mock.calls[0][1]);
|
|
176
|
+
expect(written['agent-test'].name).toBe('Test');
|
|
177
|
+
expect(written['agent-test'].stateFile).toContain('agent-test');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('creates new index when loadJSON fails', async () => {
|
|
181
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
182
|
+
const agent = { id: 'a1', name: 'A', type: 'system', lastActivity: '', currentModel: 'm', status: 'active', capabilities: [] };
|
|
183
|
+
await sm.updateAgentIndex(agent, '/project');
|
|
184
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── persistAgentState ─────────────────────────────────────────────────
|
|
189
|
+
describe('persistAgentState', () => {
|
|
190
|
+
test('saves agent state and conversations as separate files', async () => {
|
|
191
|
+
// First call: updateAgentIndex -> loadJSON for index
|
|
192
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({}));
|
|
193
|
+
|
|
194
|
+
const agent = {
|
|
195
|
+
id: 'agent-persist',
|
|
196
|
+
name: 'PersistAgent',
|
|
197
|
+
type: 'user-created',
|
|
198
|
+
conversations: {
|
|
199
|
+
full: { messages: [{ role: 'user', content: 'hi' }] }
|
|
200
|
+
},
|
|
201
|
+
lastActivity: new Date().toISOString(),
|
|
202
|
+
currentModel: 'model-x',
|
|
203
|
+
status: 'active',
|
|
204
|
+
capabilities: []
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await sm.persistAgentState(agent);
|
|
208
|
+
|
|
209
|
+
// Should have written state file and conversations file and index
|
|
210
|
+
expect(mockWriteFile).toHaveBeenCalledTimes(3); // state, conversations, index
|
|
211
|
+
const stateCall = mockWriteFile.mock.calls[0];
|
|
212
|
+
expect(stateCall[0]).toContain('agent-persist-state.json');
|
|
213
|
+
const convCall = mockWriteFile.mock.calls[1];
|
|
214
|
+
expect(convCall[0]).toContain('agent-persist-conversations.json');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('throws on write failure', async () => {
|
|
218
|
+
mockWriteFile.mockRejectedValueOnce(new Error('write fail'));
|
|
219
|
+
const agent = { id: 'fail', conversations: { full: {} }, name: 'F', type: 't', lastActivity: '', currentModel: 'm', status: 'a', capabilities: [] };
|
|
220
|
+
await expect(sm.persistAgentState(agent)).rejects.toThrow('write fail');
|
|
221
|
+
expect(logger.error).toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ─── deleteAgentState ──────────────────────────────────────────────────
|
|
226
|
+
describe('deleteAgentState', () => {
|
|
227
|
+
test('unlinks state and conversations files', async () => {
|
|
228
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ 'agent-del': { name: 'Del' } }));
|
|
229
|
+
await sm.deleteAgentState('agent-del');
|
|
230
|
+
expect(mockUnlink).toHaveBeenCalledTimes(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('handles ENOENT gracefully (files already missing)', async () => {
|
|
234
|
+
const enoent = new Error('ENOENT');
|
|
235
|
+
enoent.code = 'ENOENT';
|
|
236
|
+
mockUnlink.mockRejectedValueOnce(enoent);
|
|
237
|
+
mockUnlink.mockRejectedValueOnce(enoent);
|
|
238
|
+
// removeFromAgentIndex calls loadJSON
|
|
239
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({}));
|
|
240
|
+
await sm.deleteAgentState('agent-missing');
|
|
241
|
+
// Should not throw
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── resumeProject ────────────────────────────────────────────────────
|
|
246
|
+
describe('resumeProject', () => {
|
|
247
|
+
test('loads project state, agent index, and restores agents', async () => {
|
|
248
|
+
// Setup mock responses in order:
|
|
249
|
+
// 1. initializeStateDirectory -> ensureUserDataDirs (already mocked)
|
|
250
|
+
// 2. loadProjectState -> loadJSON
|
|
251
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ version: '1.0.0' }));
|
|
252
|
+
// 3. loadAgentIndex -> loadJSON
|
|
253
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({}));
|
|
254
|
+
// 4. restoreAsyncOperations -> loadJSON
|
|
255
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
256
|
+
// 5. restorePausedAgents -> loadJSON
|
|
257
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
258
|
+
// 6. restoreContextReferences -> loadJSON
|
|
259
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
260
|
+
// 7. saveLastSession -> saveJSON -> writeFile
|
|
261
|
+
// (writeFile is already mocked)
|
|
262
|
+
|
|
263
|
+
const result = await sm.resumeProject('/project');
|
|
264
|
+
expect(result.resumedSuccessfully).toBe(true);
|
|
265
|
+
expect(result.agents).toEqual([]);
|
|
266
|
+
expect(result.asyncOperations).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('returns error state on failure', async () => {
|
|
270
|
+
mockEnsureUserDataDirs.mockRejectedValueOnce(new Error('init fail'));
|
|
271
|
+
const result = await sm.resumeProject('/project');
|
|
272
|
+
expect(result.resumedSuccessfully).toBe(false);
|
|
273
|
+
expect(result.error).toContain('init fail');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── saveJSON / loadJSON ───────────────────────────────────────────────
|
|
278
|
+
describe('saveJSON (private)', () => {
|
|
279
|
+
test('creates directory and writes JSON', async () => {
|
|
280
|
+
await sm.saveJSON('/path/to/file.json', { key: 'value' });
|
|
281
|
+
expect(mockMkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true });
|
|
282
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
283
|
+
'/path/to/file.json',
|
|
284
|
+
expect.any(String),
|
|
285
|
+
'utf8'
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('loadJSON (private)', () => {
|
|
291
|
+
test('reads and parses JSON', async () => {
|
|
292
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ x: 1 }));
|
|
293
|
+
const result = await sm.loadJSON('/path/to/file.json');
|
|
294
|
+
expect(result.x).toBe(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('throws on corrupt JSON', async () => {
|
|
298
|
+
mockReadFile.mockResolvedValueOnce('not-json{{{');
|
|
299
|
+
await expect(sm.loadJSON('/bad.json')).rejects.toThrow();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('throws on missing file', async () => {
|
|
303
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
304
|
+
await expect(sm.loadJSON('/missing.json')).rejects.toThrow('ENOENT');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ─── restoreAsyncOperations ────────────────────────────────────────────
|
|
309
|
+
describe('restoreAsyncOperations', () => {
|
|
310
|
+
test('returns operations from stored data', async () => {
|
|
311
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({ operations: [{ id: 'op1' }] }));
|
|
312
|
+
const ops = await sm.restoreAsyncOperations('/project');
|
|
313
|
+
expect(ops).toEqual([{ id: 'op1' }]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('returns empty array on missing file', async () => {
|
|
317
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
318
|
+
const ops = await sm.restoreAsyncOperations('/project');
|
|
319
|
+
expect(ops).toEqual([]);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ─── restorePausedAgents ───────────────────────────────────────────────
|
|
324
|
+
describe('restorePausedAgents', () => {
|
|
325
|
+
test('returns paused agents data and resumes expired', async () => {
|
|
326
|
+
const past = new Date(Date.now() - 10000).toISOString();
|
|
327
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({
|
|
328
|
+
pausedAgents: {
|
|
329
|
+
'agent-1': { pausedAt: past, pausedUntil: past, reason: 'test' }
|
|
330
|
+
}
|
|
331
|
+
}));
|
|
332
|
+
const result = await sm.restorePausedAgents('/project');
|
|
333
|
+
// Expired agent should be moved to history
|
|
334
|
+
expect(result.pausedAgents['agent-1']).toBeUndefined();
|
|
335
|
+
expect(result.pauseHistory).toHaveLength(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('returns defaults on missing file', async () => {
|
|
339
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
340
|
+
const result = await sm.restorePausedAgents('/project');
|
|
341
|
+
expect(result.pausedAgents).toEqual({});
|
|
342
|
+
expect(result.pauseHistory).toEqual([]);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ─── restoreContextReferences ──────────────────────────────────────────
|
|
347
|
+
describe('restoreContextReferences', () => {
|
|
348
|
+
test('validates and returns references', async () => {
|
|
349
|
+
mockReadFile.mockResolvedValueOnce(JSON.stringify({
|
|
350
|
+
references: [{ id: 'ref1', type: 'file' }]
|
|
351
|
+
}));
|
|
352
|
+
const result = await sm.restoreContextReferences('/project');
|
|
353
|
+
expect(result.references).toHaveLength(1);
|
|
354
|
+
expect(result.references[0].isValid).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('returns defaults on missing file', async () => {
|
|
358
|
+
mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
|
|
359
|
+
const result = await sm.restoreContextReferences('/project');
|
|
360
|
+
expect(result.references).toEqual([]);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ─── saveLastSession / loadLastSession ─────────────────────────────────
|
|
365
|
+
describe('saveLastSession', () => {
|
|
366
|
+
test('writes session data with savedAt timestamp', async () => {
|
|
367
|
+
await sm.saveLastSession('/project', { agentCount: 2 });
|
|
368
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
369
|
+
const written = JSON.parse(mockWriteFile.mock.calls[0][1]);
|
|
370
|
+
expect(written.agentCount).toBe(2);
|
|
371
|
+
expect(written.savedAt).toBeDefined();
|
|
372
|
+
expect(written.projectDir).toBe('/project');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
package/src/core/agentPool.js
CHANGED
|
@@ -833,7 +833,17 @@ class AgentPool {
|
|
|
833
833
|
}
|
|
834
834
|
} catch (error) {
|
|
835
835
|
this.logger.warn(`Failed to clean up visual editor for agent: ${error.message}`, { agentId });
|
|
836
|
-
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Kill any running terminal processes for this agent
|
|
839
|
+
try {
|
|
840
|
+
const terminalTool = this.toolsRegistry?.getTool?.('terminal');
|
|
841
|
+
if (terminalTool && typeof terminalTool.cleanupAgent === 'function') {
|
|
842
|
+
await terminalTool.cleanupAgent(agentId);
|
|
843
|
+
this.logger.info(`Terminal processes cleaned up for agent: ${agentId}`);
|
|
844
|
+
}
|
|
845
|
+
} catch (error) {
|
|
846
|
+
this.logger.warn(`Failed to clean up terminal processes for agent: ${error.message}`, { agentId });
|
|
837
847
|
}
|
|
838
848
|
|
|
839
849
|
// Clean up agent resources
|