@openclaw-cloud/agent-controller 0.2.6 → 0.2.7

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 (76) hide show
  1. package/dist/commands/install.js +3 -0
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/config-file.d.ts +9 -0
  4. package/dist/config-file.js +47 -0
  5. package/dist/config-file.js.map +1 -0
  6. package/dist/connection.d.ts +1 -0
  7. package/dist/connection.js +27 -13
  8. package/dist/connection.js.map +1 -1
  9. package/dist/handlers/backup.js +7 -2
  10. package/dist/handlers/backup.js.map +1 -1
  11. package/dist/handlers/knowledge-sync.d.ts +2 -0
  12. package/dist/handlers/knowledge-sync.js +51 -0
  13. package/dist/handlers/knowledge-sync.js.map +1 -0
  14. package/dist/heartbeat.d.ts +1 -0
  15. package/dist/heartbeat.js +30 -0
  16. package/dist/heartbeat.js.map +1 -1
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/types.d.ts +2 -1
  20. package/package.json +6 -1
  21. package/.claude/cc-notify.sh +0 -32
  22. package/.claude/settings.json +0 -31
  23. package/.husky/pre-commit +0 -1
  24. package/BIZPLAN.md +0 -530
  25. package/CLAUDE.md +0 -172
  26. package/Dockerfile +0 -9
  27. package/__tests__/api.test.ts +0 -183
  28. package/__tests__/backup.test.ts +0 -145
  29. package/__tests__/board-handler.test.ts +0 -323
  30. package/__tests__/chat.test.ts +0 -191
  31. package/__tests__/config.test.ts +0 -100
  32. package/__tests__/connection.test.ts +0 -289
  33. package/__tests__/file-delete.test.ts +0 -90
  34. package/__tests__/file-write.test.ts +0 -119
  35. package/__tests__/gateway-adapter.test.ts +0 -366
  36. package/__tests__/gateway-client.test.ts +0 -272
  37. package/__tests__/handlers.test.ts +0 -150
  38. package/__tests__/heartbeat.test.ts +0 -124
  39. package/__tests__/onboarding.test.ts +0 -55
  40. package/__tests__/package-install.test.ts +0 -109
  41. package/__tests__/pair.test.ts +0 -60
  42. package/__tests__/self-update.test.ts +0 -123
  43. package/__tests__/stop.test.ts +0 -38
  44. package/jest.config.ts +0 -16
  45. package/src/api.ts +0 -62
  46. package/src/commands/install.ts +0 -68
  47. package/src/commands/self-update.ts +0 -43
  48. package/src/commands/uninstall.ts +0 -19
  49. package/src/config-file.ts +0 -56
  50. package/src/connection.ts +0 -203
  51. package/src/debug.ts +0 -11
  52. package/src/handlers/backup.ts +0 -101
  53. package/src/handlers/board-handler.ts +0 -155
  54. package/src/handlers/chat.ts +0 -79
  55. package/src/handlers/config.ts +0 -48
  56. package/src/handlers/deploy.ts +0 -32
  57. package/src/handlers/exec.ts +0 -32
  58. package/src/handlers/file-delete.ts +0 -46
  59. package/src/handlers/file-write.ts +0 -65
  60. package/src/handlers/knowledge-sync.ts +0 -53
  61. package/src/handlers/onboarding.ts +0 -19
  62. package/src/handlers/package-install.ts +0 -69
  63. package/src/handlers/pair.ts +0 -26
  64. package/src/handlers/restart.ts +0 -19
  65. package/src/handlers/stop.ts +0 -17
  66. package/src/heartbeat.ts +0 -110
  67. package/src/index.ts +0 -97
  68. package/src/openclaw/gateway-adapter.ts +0 -129
  69. package/src/openclaw/gateway-client.ts +0 -131
  70. package/src/openclaw/index.ts +0 -17
  71. package/src/openclaw/types.ts +0 -41
  72. package/src/platform/linux.ts +0 -108
  73. package/src/platform/macos.ts +0 -122
  74. package/src/platform/windows.ts +0 -92
  75. package/src/types.ts +0 -94
  76. package/tsconfig.json +0 -18
@@ -1,323 +0,0 @@
1
- import { BoardHandler } from '../src/handlers/board-handler';
2
- import type { AgentApi } from '../src/api';
3
- import type { BoardInfo, CardDetail, BoardEvent } from '../src/types';
4
- import childProcess from 'node:child_process';
5
-
6
- jest.mock('node:child_process');
7
- const mockExec = childProcess.exec as unknown as jest.Mock;
8
-
9
- function mockApi(overrides: Partial<AgentApi> = {}): AgentApi {
10
- return {
11
- get: jest.fn(),
12
- post: jest.fn(),
13
- publishResponse: jest.fn().mockResolvedValue(undefined),
14
- publishHeartbeat: jest.fn().mockResolvedValue(undefined),
15
- ...overrides,
16
- };
17
- }
18
-
19
- function makeBoardInfo(opts: Partial<{
20
- boardId: string;
21
- workspaceId: string;
22
- myColumnIds: string[];
23
- columns: BoardInfo['board']['columns'];
24
- }> = {}): BoardInfo {
25
- return {
26
- board: {
27
- id: opts.boardId ?? 'board-1',
28
- workspaceId: opts.workspaceId ?? 'ws-1',
29
- myColumnIds: opts.myColumnIds ?? ['col-1'],
30
- columns: opts.columns ?? [{
31
- id: 'col-1',
32
- name: 'To Do',
33
- type: 'agent',
34
- position: 0,
35
- cards: [],
36
- }],
37
- },
38
- };
39
- }
40
-
41
- function makeCardDetail(opts: Partial<CardDetail['card']> = {}): CardDetail {
42
- return {
43
- card: {
44
- id: opts.id ?? 'card-1',
45
- title: opts.title ?? 'Test task',
46
- description: opts.description ?? 'Do the thing',
47
- priority: opts.priority ?? 'medium',
48
- columnId: opts.columnId ?? 'col-1',
49
- columnName: opts.columnName ?? 'To Do',
50
- assignedAgentId: opts.assignedAgentId ?? null,
51
- },
52
- };
53
- }
54
-
55
- function makeResponse(status: number, body: string = ''): Response {
56
- return {
57
- ok: status >= 200 && status < 300,
58
- status,
59
- text: () => Promise.resolve(body),
60
- } as Response;
61
- }
62
-
63
- describe('BoardHandler', () => {
64
- let api: AgentApi;
65
- let handler: BoardHandler;
66
-
67
- beforeEach(() => {
68
- jest.clearAllMocks();
69
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
70
- cb(null, Buffer.from('ok'), Buffer.from(''));
71
- });
72
- api = mockApi();
73
- handler = new BoardHandler(api);
74
- });
75
-
76
- describe('initialize', () => {
77
- it('sets boardId, workspaceId and myColumnIds from API response', async () => {
78
- const info = makeBoardInfo({ boardId: 'b-42', workspaceId: 'ws-99', myColumnIds: ['c-1', 'c-2'] });
79
- (api.get as jest.Mock).mockResolvedValue(info);
80
-
81
- const result = await handler.initialize();
82
-
83
- // initialize() returns workspaceId for channel subscription
84
- expect(result).toBe('ws-99');
85
- expect(handler.getBoardId()).toBe('b-42');
86
- expect(api.get).toHaveBeenCalledWith('/api/agent/board');
87
- });
88
-
89
- it('claims unassigned card in my column during init (backfill)', async () => {
90
- const info = makeBoardInfo({
91
- columns: [{
92
- id: 'col-1', name: 'To Do', type: 'agent', position: 0,
93
- cards: [{ id: 'card-99', title: 'Backfill', assignedAgentId: null }],
94
- }],
95
- });
96
- (api.get as jest.Mock).mockResolvedValue(info);
97
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
98
- // Mock card detail for startTask
99
- (api.get as jest.Mock).mockResolvedValueOnce(info).mockResolvedValueOnce(makeCardDetail({ id: 'card-99' }));
100
-
101
- await handler.initialize();
102
-
103
- expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/card-99/claim');
104
- });
105
-
106
- it('returns null on API error', async () => {
107
- (api.get as jest.Mock).mockRejectedValue(new Error('network fail'));
108
-
109
- const result = await handler.initialize();
110
-
111
- expect(result).toBeNull();
112
- expect(handler.getBoardId()).toBeNull();
113
- });
114
- });
115
-
116
- describe('onBoardEvent', () => {
117
- beforeEach(async () => {
118
- const info = makeBoardInfo();
119
- (api.get as jest.Mock).mockResolvedValue(info);
120
- await handler.initialize();
121
- jest.clearAllMocks();
122
- });
123
-
124
- it('claims card when card:entered in my column and idle', async () => {
125
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
126
- (api.get as jest.Mock).mockResolvedValue(makeCardDetail());
127
-
128
- const event: BoardEvent = { event: 'card:entered', cardId: 'card-5', columnId: 'col-1' };
129
- await handler.onBoardEvent(event);
130
-
131
- expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/card-5/claim');
132
- });
133
-
134
- it('ignores card:entered for column not in myColumnIds', async () => {
135
- const event: BoardEvent = { event: 'card:entered', cardId: 'card-5', columnId: 'other-col' };
136
- await handler.onBoardEvent(event);
137
-
138
- expect(api.post).not.toHaveBeenCalled();
139
- });
140
-
141
- it('ignores card:entered when already working', async () => {
142
- // First: claim a card to set state to working
143
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
144
- (api.get as jest.Mock).mockResolvedValue(makeCardDetail());
145
- await handler.onBoardEvent({ event: 'card:entered', cardId: 'card-1', columnId: 'col-1' });
146
-
147
- jest.clearAllMocks();
148
-
149
- // Second: new card enters, should be ignored
150
- await handler.onBoardEvent({ event: 'card:entered', cardId: 'card-2', columnId: 'col-1' });
151
- expect(api.post).not.toHaveBeenCalled();
152
- });
153
-
154
- it('ignores card:claimed, card:moved, card:commented events', async () => {
155
- for (const event of ['card:claimed', 'card:moved', 'card:commented'] as const) {
156
- await handler.onBoardEvent({ event, cardId: 'card-1' });
157
- }
158
- expect(api.post).not.toHaveBeenCalled();
159
- expect(api.get).not.toHaveBeenCalled();
160
- });
161
- });
162
-
163
- describe('tryClaimCard', () => {
164
- beforeEach(async () => {
165
- (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
166
- await handler.initialize();
167
- jest.clearAllMocks();
168
- });
169
-
170
- it('sets state to working on successful claim (200)', async () => {
171
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
172
- (api.get as jest.Mock).mockResolvedValue(makeCardDetail({ id: 'card-10' }));
173
-
174
- await handler.tryClaimCard('card-10');
175
-
176
- expect(handler.getBoardStatus()).toEqual({ state: 'working', cardId: 'card-10' });
177
- });
178
-
179
- it('stays idle on 409 (already claimed)', async () => {
180
- (api.post as jest.Mock).mockResolvedValue(makeResponse(409));
181
-
182
- await handler.tryClaimCard('card-10');
183
-
184
- expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
185
- });
186
-
187
- it('stays idle on 500 error', async () => {
188
- (api.post as jest.Mock).mockResolvedValue(makeResponse(500, 'Internal error'));
189
-
190
- await handler.tryClaimCard('card-10');
191
-
192
- expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
193
- });
194
-
195
- it('stays idle on network error', async () => {
196
- (api.post as jest.Mock).mockRejectedValue(new Error('connection refused'));
197
-
198
- await handler.tryClaimCard('card-10');
199
-
200
- expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
201
- });
202
- });
203
-
204
- describe('startTask', () => {
205
- beforeEach(async () => {
206
- (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
207
- await handler.initialize();
208
- jest.clearAllMocks();
209
- });
210
-
211
- it('fetches card detail and calls openclaw exec', async () => {
212
- const detail = makeCardDetail({ id: 'card-20', title: 'Build feature', description: 'Some desc', priority: 'high' });
213
- (api.get as jest.Mock).mockResolvedValue(detail);
214
-
215
- await handler.startTask('card-20');
216
-
217
- expect(api.get).toHaveBeenCalledWith('/api/agent/board/cards/card-20');
218
- expect(mockExec).toHaveBeenCalledTimes(1);
219
- const cmd = mockExec.mock.calls[0][0] as string;
220
- expect(cmd).toContain('openclaw system event');
221
- expect(cmd).toContain('Build feature');
222
- expect(cmd).toContain('Priority: high');
223
- });
224
-
225
- it('handles API error gracefully', async () => {
226
- (api.get as jest.Mock).mockRejectedValue(new Error('not found'));
227
-
228
- await handler.startTask('card-missing');
229
-
230
- expect(mockExec).not.toHaveBeenCalled();
231
- });
232
- });
233
-
234
- describe('completeTask', () => {
235
- it('resets state to idle and triggers checkQueue', async () => {
236
- const info = makeBoardInfo();
237
- (api.get as jest.Mock).mockResolvedValue(info);
238
- await handler.initialize();
239
-
240
- // Claim a card first
241
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
242
- (api.get as jest.Mock).mockResolvedValueOnce(makeCardDetail()).mockResolvedValueOnce(info);
243
-
244
- await handler.tryClaimCard('card-1');
245
- expect(handler.getBoardStatus().state).toBe('working');
246
-
247
- handler.completeTask('card-1');
248
- expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
249
- });
250
- });
251
-
252
- describe('checkQueue', () => {
253
- beforeEach(async () => {
254
- (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
255
- await handler.initialize();
256
- jest.clearAllMocks();
257
- });
258
-
259
- it('sorts candidates by priority then createdAt', async () => {
260
- const info = makeBoardInfo({
261
- columns: [{
262
- id: 'col-1', name: 'To Do', type: 'agent', position: 0,
263
- cards: [
264
- { id: 'low-old', title: 'Low old', priority: 'low', assignedAgentId: null, createdAt: '2026-01-01' },
265
- { id: 'critical-new', title: 'Critical', priority: 'critical', assignedAgentId: null, createdAt: '2026-02-01' },
266
- { id: 'high-old', title: 'High old', priority: 'high', assignedAgentId: null, createdAt: '2026-01-15' },
267
- ],
268
- }],
269
- });
270
- (api.get as jest.Mock).mockResolvedValue(info);
271
- (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
272
- (api.get as jest.Mock).mockResolvedValueOnce(info).mockResolvedValueOnce(makeCardDetail({ id: 'critical-new' }));
273
-
274
- await handler.checkQueue();
275
-
276
- // Should try to claim the critical card first
277
- expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/critical-new/claim');
278
- });
279
-
280
- it('does nothing when no unassigned cards', async () => {
281
- const info = makeBoardInfo({
282
- columns: [{
283
- id: 'col-1', name: 'To Do', type: 'agent', position: 0,
284
- cards: [
285
- { id: 'card-1', title: 'Taken', assignedAgentId: 'agent-1' },
286
- ],
287
- }],
288
- });
289
- (api.get as jest.Mock).mockResolvedValue(info);
290
-
291
- await handler.checkQueue();
292
-
293
- expect(api.post).not.toHaveBeenCalled();
294
- });
295
-
296
- it('skips cards in columns not in myColumnIds', async () => {
297
- const info = makeBoardInfo({
298
- myColumnIds: ['col-1'],
299
- columns: [
300
- { id: 'col-1', name: 'Mine', type: 'agent', position: 0, cards: [] },
301
- { id: 'col-2', name: 'Not mine', type: 'human', position: 1, cards: [
302
- { id: 'card-x', title: 'Other', assignedAgentId: null },
303
- ]},
304
- ],
305
- });
306
- (api.get as jest.Mock).mockResolvedValue(info);
307
-
308
- await handler.checkQueue();
309
-
310
- expect(api.post).not.toHaveBeenCalled();
311
- });
312
- });
313
-
314
- describe('getters', () => {
315
- it('getBoardStatus returns idle by default', () => {
316
- expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
317
- });
318
-
319
- it('getBoardId returns null before initialize', () => {
320
- expect(handler.getBoardId()).toBeNull();
321
- });
322
- });
323
- });
@@ -1,191 +0,0 @@
1
- import { handleChatListSessions, handleChatHistory, handleChatSend } from '../src/handlers/chat';
2
- import type { AgentCommand } from '../src/types';
3
-
4
- jest.mock('../src/openclaw/index');
5
-
6
- import { getChatProvider } from '../src/openclaw/index';
7
-
8
- const mockGetChatProvider = getChatProvider as jest.Mock;
9
-
10
- function makeCommand(
11
- type: AgentCommand['type'],
12
- payload: Record<string, unknown> = {},
13
- id = 'cmd-1',
14
- ): AgentCommand {
15
- return { id, type, payload };
16
- }
17
-
18
- describe('handleChatListSessions', () => {
19
- let publish: jest.Mock;
20
-
21
- beforeEach(() => {
22
- jest.clearAllMocks();
23
- publish = jest.fn().mockResolvedValue(undefined);
24
- });
25
-
26
- it('publishes error when provider is null', async () => {
27
- mockGetChatProvider.mockReturnValue(null);
28
- const cmd = makeCommand('chat_list_sessions');
29
- await handleChatListSessions(cmd, publish);
30
- expect(publish).toHaveBeenCalledWith(
31
- expect.objectContaining({
32
- type: 'chat_sessions_response',
33
- correlationId: 'cmd-1',
34
- sessions: [],
35
- error: expect.stringContaining('not initialized'),
36
- }),
37
- );
38
- });
39
-
40
- it('publishes sessions on success', async () => {
41
- const sessions = [{ key: 'sess-1', sessionId: 'sid-1', kind: 'direct', updatedAt: 0 }];
42
- mockGetChatProvider.mockReturnValue({
43
- listSessions: jest.fn().mockResolvedValue(sessions),
44
- });
45
- const cmd = makeCommand('chat_list_sessions');
46
- await handleChatListSessions(cmd, publish);
47
- expect(publish).toHaveBeenCalledWith({
48
- type: 'chat_sessions_response',
49
- correlationId: 'cmd-1',
50
- sessions,
51
- });
52
- });
53
-
54
- it('publishes error when provider.listSessions throws', async () => {
55
- mockGetChatProvider.mockReturnValue({
56
- listSessions: jest.fn().mockRejectedValue(new Error('db error')),
57
- });
58
- const cmd = makeCommand('chat_list_sessions');
59
- await handleChatListSessions(cmd, publish);
60
- expect(publish).toHaveBeenCalledWith(
61
- expect.objectContaining({
62
- type: 'chat_sessions_response',
63
- sessions: [],
64
- error: 'db error',
65
- }),
66
- );
67
- });
68
- });
69
-
70
- describe('handleChatHistory', () => {
71
- let publish: jest.Mock;
72
-
73
- beforeEach(() => {
74
- jest.clearAllMocks();
75
- publish = jest.fn().mockResolvedValue(undefined);
76
- });
77
-
78
- it('publishes messages on success', async () => {
79
- const messages = [{ role: 'user', content: 'hello', timestamp: '2026-01-01T00:00:00Z' }];
80
- mockGetChatProvider.mockReturnValue({
81
- getHistory: jest.fn().mockResolvedValue(messages),
82
- });
83
- const cmd = makeCommand('chat_history', { sessionKey: 'sess-1', limit: 50 });
84
- await handleChatHistory(cmd, publish);
85
- expect(publish).toHaveBeenCalledWith({
86
- type: 'chat_history_response',
87
- correlationId: 'cmd-1',
88
- messages,
89
- });
90
- });
91
-
92
- it('publishes error when provider is null', async () => {
93
- mockGetChatProvider.mockReturnValue(null);
94
- const cmd = makeCommand('chat_history', { sessionKey: 'sess-1' });
95
- await handleChatHistory(cmd, publish);
96
- expect(publish).toHaveBeenCalledWith(
97
- expect.objectContaining({
98
- type: 'chat_history_response',
99
- messages: [],
100
- error: expect.stringContaining('not initialized'),
101
- }),
102
- );
103
- });
104
-
105
- it('publishes error when provider.getHistory throws', async () => {
106
- mockGetChatProvider.mockReturnValue({
107
- getHistory: jest.fn().mockRejectedValue(new Error('history error')),
108
- });
109
- const cmd = makeCommand('chat_history', { sessionKey: 'sess-1' });
110
- await handleChatHistory(cmd, publish);
111
- expect(publish).toHaveBeenCalledWith(
112
- expect.objectContaining({
113
- type: 'chat_history_response',
114
- messages: [],
115
- error: 'history error',
116
- }),
117
- );
118
- });
119
- });
120
-
121
- describe('handleChatSend', () => {
122
- let publish: jest.Mock;
123
-
124
- beforeEach(() => {
125
- jest.clearAllMocks();
126
- publish = jest.fn().mockResolvedValue(undefined);
127
- });
128
-
129
- it('always sends typing indicator before checking provider', async () => {
130
- mockGetChatProvider.mockReturnValue(null);
131
- const cmd = makeCommand('chat_send', { sessionKey: 'sess-1', text: 'hi' });
132
- await handleChatSend(cmd, publish, 'agent-1');
133
- expect(publish).toHaveBeenCalledWith({ type: 'chat_typing', agentId: 'agent-1', state: true });
134
- });
135
-
136
- it('publishes error response when provider is null', async () => {
137
- mockGetChatProvider.mockReturnValue(null);
138
- const cmd = makeCommand('chat_send', { sessionKey: 'sess-1', text: 'hi' });
139
- await handleChatSend(cmd, publish, 'agent-1');
140
- const published = (publish as jest.Mock).mock.calls.map((c) => c[0]);
141
- expect(published).toContainEqual(
142
- expect.objectContaining({
143
- type: 'chat_response',
144
- correlationId: 'cmd-1',
145
- error: expect.stringContaining('not initialized'),
146
- }),
147
- );
148
- });
149
-
150
- it('calls onDelta and onDone via sendMessage callbacks', async () => {
151
- const sendMessage = jest.fn().mockImplementation(async (params: any) => {
152
- await params.onDelta('partial');
153
- await params.onDone('full response');
154
- });
155
- mockGetChatProvider.mockReturnValue({ sendMessage });
156
- const cmd = makeCommand('chat_send', { sessionKey: 'sess-1', text: 'hi' });
157
- await handleChatSend(cmd, publish, 'agent-1');
158
- const published = (publish as jest.Mock).mock.calls.map((c) => c[0]);
159
- expect(published).toContainEqual(
160
- expect.objectContaining({ type: 'chat_delta', correlationId: 'cmd-1', text: 'partial' }),
161
- );
162
- expect(published).toContainEqual(
163
- expect.objectContaining({ type: 'chat_response', correlationId: 'cmd-1', text: 'full response' }),
164
- );
165
- });
166
-
167
- it('publishes error via onError callback', async () => {
168
- const sendMessage = jest.fn().mockImplementation(async (params: any) => {
169
- await params.onError('stream error');
170
- });
171
- mockGetChatProvider.mockReturnValue({ sendMessage });
172
- const cmd = makeCommand('chat_send', { sessionKey: 'sess-1', text: 'hi' });
173
- await handleChatSend(cmd, publish, 'agent-1');
174
- const published = (publish as jest.Mock).mock.calls.map((c) => c[0]);
175
- expect(published).toContainEqual(
176
- expect.objectContaining({ type: 'chat_response', error: 'stream error' }),
177
- );
178
- });
179
-
180
- it('publishes error when provider.sendMessage throws', async () => {
181
- mockGetChatProvider.mockReturnValue({
182
- sendMessage: jest.fn().mockRejectedValue(new Error('send failed')),
183
- });
184
- const cmd = makeCommand('chat_send', { sessionKey: 'sess-1', text: 'hi' });
185
- await handleChatSend(cmd, publish, 'agent-1');
186
- const published = (publish as jest.Mock).mock.calls.map((c) => c[0]);
187
- expect(published).toContainEqual(
188
- expect.objectContaining({ type: 'chat_response', error: 'send failed' }),
189
- );
190
- });
191
- });
@@ -1,100 +0,0 @@
1
- import { handleConfig } from '../src/handlers/config';
2
- import type { AgentCommand } from '../src/types';
3
- import childProcess from 'node:child_process';
4
- import fs from 'node:fs/promises';
5
-
6
- jest.mock('node:child_process');
7
- jest.mock('node:fs/promises');
8
-
9
- const mockExec = childProcess.exec as unknown as jest.Mock;
10
- const mockMkdir = fs.mkdir as jest.Mock;
11
- const mockWriteFile = fs.writeFile as jest.Mock;
12
-
13
- function fakeExec(error: Error | null, stdout = '', stderr = '') {
14
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
15
- cb(error, Buffer.from(stdout), Buffer.from(stderr));
16
- });
17
- }
18
-
19
- describe('handleConfig', () => {
20
- beforeEach(() => {
21
- jest.clearAllMocks();
22
- mockMkdir.mockResolvedValue(undefined);
23
- mockWriteFile.mockResolvedValue(undefined);
24
- });
25
-
26
- it('rejects missing filename', async () => {
27
- const cmd: AgentCommand = { id: '1', type: 'config', payload: { content: 'some: config' } };
28
- const res = await handleConfig(cmd);
29
- expect(res.success).toBe(false);
30
- expect(res.error).toContain('Missing');
31
- });
32
-
33
- it('rejects missing content', async () => {
34
- const cmd: AgentCommand = { id: '2', type: 'config', payload: { filename: 'test.yml' } };
35
- const res = await handleConfig(cmd);
36
- expect(res.success).toBe(false);
37
- expect(res.error).toContain('Missing');
38
- });
39
-
40
- it('creates directory, writes file, and runs restart on success', async () => {
41
- fakeExec(null, 'restarted', '');
42
- const cmd: AgentCommand = {
43
- id: '3',
44
- type: 'config',
45
- payload: { filename: 'agent.yml', content: 'key: val' },
46
- };
47
- const res = await handleConfig(cmd);
48
-
49
- expect(fs.mkdir).toHaveBeenCalledWith('/etc/openclaw', { recursive: true });
50
- expect(fs.writeFile).toHaveBeenCalledWith(
51
- expect.stringContaining('agent.yml'),
52
- 'key: val',
53
- 'utf-8',
54
- );
55
- expect(mockExec).toHaveBeenCalledWith(
56
- 'openclaw gateway restart',
57
- expect.any(Object),
58
- expect.any(Function),
59
- );
60
- expect(res.success).toBe(true);
61
- expect(res.data?.path).toContain('agent.yml');
62
- });
63
-
64
- it('returns error when writeFile fails', async () => {
65
- mockWriteFile.mockRejectedValue(new Error('EACCES: permission denied'));
66
- const cmd: AgentCommand = {
67
- id: '4',
68
- type: 'config',
69
- payload: { filename: 'agent.yml', content: 'key: val' },
70
- };
71
- const res = await handleConfig(cmd);
72
- expect(res.success).toBe(false);
73
- expect(res.error).toContain('EACCES');
74
- });
75
-
76
- it('returns success:false with error message when restart fails', async () => {
77
- fakeExec(new Error('restart failed'), '', 'stderr output');
78
- const cmd: AgentCommand = {
79
- id: '5',
80
- type: 'config',
81
- payload: { filename: 'agent.yml', content: 'key: val' },
82
- };
83
- const res = await handleConfig(cmd);
84
- expect(res.success).toBe(false);
85
- expect(res.error).toBe('restart failed');
86
- expect(res.data?.path).toContain('agent.yml');
87
- });
88
-
89
- it('strips directory traversal from filename', async () => {
90
- fakeExec(null, '', '');
91
- const cmd: AgentCommand = {
92
- id: '6',
93
- type: 'config',
94
- payload: { filename: '../../../etc/passwd', content: 'root:x:0:0' },
95
- };
96
- const res = await handleConfig(cmd);
97
- // path.basename strips the directory part
98
- expect(res.data?.path).toBe('/etc/openclaw/passwd');
99
- });
100
- });