@openclaw-cloud/agent-controller 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bin/agent-controller.js +6 -0
  2. package/dist/commands/backup-cli.d.ts +1 -0
  3. package/dist/commands/backup-cli.js +65 -0
  4. package/dist/commands/backup-cli.js.map +1 -0
  5. package/dist/commands/install.js +3 -0
  6. package/dist/commands/install.js.map +1 -1
  7. package/dist/commands/knowledge-sync-cli.d.ts +1 -0
  8. package/dist/commands/knowledge-sync-cli.js +61 -0
  9. package/dist/commands/knowledge-sync-cli.js.map +1 -0
  10. package/dist/config-file.d.ts +9 -0
  11. package/dist/config-file.js +47 -0
  12. package/dist/config-file.js.map +1 -0
  13. package/dist/connection.d.ts +1 -0
  14. package/dist/connection.js +27 -13
  15. package/dist/connection.js.map +1 -1
  16. package/dist/handlers/backup.js +7 -2
  17. package/dist/handlers/backup.js.map +1 -1
  18. package/dist/handlers/knowledge-sync.d.ts +2 -0
  19. package/dist/handlers/knowledge-sync.js +51 -0
  20. package/dist/handlers/knowledge-sync.js.map +1 -0
  21. package/dist/heartbeat.d.ts +1 -0
  22. package/dist/heartbeat.js +30 -0
  23. package/dist/heartbeat.js.map +1 -1
  24. package/dist/index.js +7 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/types.d.ts +2 -1
  27. package/package.json +6 -1
  28. package/.claude/cc-notify.sh +0 -32
  29. package/.claude/settings.json +0 -31
  30. package/.husky/pre-commit +0 -1
  31. package/BIZPLAN.md +0 -530
  32. package/CLAUDE.md +0 -172
  33. package/Dockerfile +0 -9
  34. package/__tests__/api.test.ts +0 -183
  35. package/__tests__/backup.test.ts +0 -145
  36. package/__tests__/board-handler.test.ts +0 -323
  37. package/__tests__/chat.test.ts +0 -191
  38. package/__tests__/config.test.ts +0 -100
  39. package/__tests__/connection.test.ts +0 -289
  40. package/__tests__/file-delete.test.ts +0 -90
  41. package/__tests__/file-write.test.ts +0 -119
  42. package/__tests__/gateway-adapter.test.ts +0 -366
  43. package/__tests__/gateway-client.test.ts +0 -272
  44. package/__tests__/handlers.test.ts +0 -150
  45. package/__tests__/heartbeat.test.ts +0 -124
  46. package/__tests__/onboarding.test.ts +0 -55
  47. package/__tests__/package-install.test.ts +0 -109
  48. package/__tests__/pair.test.ts +0 -60
  49. package/__tests__/self-update.test.ts +0 -123
  50. package/__tests__/stop.test.ts +0 -38
  51. package/jest.config.ts +0 -16
  52. package/src/api.ts +0 -62
  53. package/src/commands/install.ts +0 -68
  54. package/src/commands/self-update.ts +0 -43
  55. package/src/commands/uninstall.ts +0 -19
  56. package/src/config-file.ts +0 -56
  57. package/src/connection.ts +0 -203
  58. package/src/debug.ts +0 -11
  59. package/src/handlers/backup.ts +0 -101
  60. package/src/handlers/board-handler.ts +0 -155
  61. package/src/handlers/chat.ts +0 -79
  62. package/src/handlers/config.ts +0 -48
  63. package/src/handlers/deploy.ts +0 -32
  64. package/src/handlers/exec.ts +0 -32
  65. package/src/handlers/file-delete.ts +0 -46
  66. package/src/handlers/file-write.ts +0 -65
  67. package/src/handlers/knowledge-sync.ts +0 -53
  68. package/src/handlers/onboarding.ts +0 -19
  69. package/src/handlers/package-install.ts +0 -69
  70. package/src/handlers/pair.ts +0 -26
  71. package/src/handlers/restart.ts +0 -19
  72. package/src/handlers/stop.ts +0 -17
  73. package/src/heartbeat.ts +0 -110
  74. package/src/index.ts +0 -97
  75. package/src/openclaw/gateway-adapter.ts +0 -129
  76. package/src/openclaw/gateway-client.ts +0 -131
  77. package/src/openclaw/index.ts +0 -17
  78. package/src/openclaw/types.ts +0 -41
  79. package/src/platform/linux.ts +0 -108
  80. package/src/platform/macos.ts +0 -122
  81. package/src/platform/windows.ts +0 -92
  82. package/src/types.ts +0 -94
  83. package/tsconfig.json +0 -18
@@ -1,289 +0,0 @@
1
- import { Centrifuge } from 'centrifuge';
2
- import type { AgentApi } from '../src/api';
3
-
4
- jest.mock('centrifuge');
5
- jest.mock('ws', () => jest.fn());
6
-
7
- const MockCentrifuge = Centrifuge as jest.MockedClass<typeof Centrifuge>;
8
-
9
- const BACKEND_URL = 'http://backend:3000';
10
- const AGENT_TOKEN = 'agent-token';
11
- const AGENT_ID = 'test-1';
12
- const JWT = 'centrifugo.jwt.token';
13
-
14
- // Mock subscription instance
15
- function createMockSubscription(channel: string) {
16
- const handlers: Record<string, Function> = {};
17
- return {
18
- on: jest.fn((event: string, cb: Function) => { handlers[event] = cb; }),
19
- subscribe: jest.fn(),
20
- channel,
21
- _handlers: handlers,
22
- };
23
- }
24
-
25
- function createMockClient(mockSubs: ReturnType<typeof createMockSubscription>[]) {
26
- let subIndex = 0;
27
- const clientHandlers: Record<string, Function> = {};
28
- return {
29
- on: jest.fn((event: string, cb: Function) => { clientHandlers[event] = cb; }),
30
- newSubscription: jest.fn(() => mockSubs[subIndex++]),
31
- connect: jest.fn(),
32
- disconnect: jest.fn(),
33
- publish: jest.fn().mockResolvedValue(undefined),
34
- _handlers: clientHandlers,
35
- };
36
- }
37
-
38
- function makeApi(): AgentApi & { publishResponse: jest.Mock } {
39
- return {
40
- get: jest.fn(),
41
- post: jest.fn(),
42
- publishResponse: jest.fn().mockResolvedValue(undefined),
43
- publishHeartbeat: jest.fn().mockResolvedValue(undefined),
44
- } as unknown as AgentApi & { publishResponse: jest.Mock };
45
- }
46
-
47
- function mockFetchOk(token = JWT) {
48
- global.fetch = jest.fn().mockResolvedValue({
49
- ok: true,
50
- json: () => Promise.resolve({ token }),
51
- } as unknown as Response);
52
- }
53
-
54
- beforeEach(() => {
55
- jest.clearAllMocks();
56
- mockFetchOk();
57
- });
58
-
59
- describe('createConnection', () => {
60
- it('passes getToken (not raw token) to Centrifuge when backendUrl is provided', async () => {
61
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
62
- const mockClient = createMockClient([commandSub]);
63
- MockCentrifuge.mockImplementation(() => mockClient as any);
64
-
65
- const { createConnection } = await import('../src/connection');
66
-
67
- createConnection({
68
- url: 'ws://localhost:8000/connection/websocket',
69
- token: AGENT_TOKEN,
70
- agentId: AGENT_ID,
71
- backendUrl: BACKEND_URL,
72
- api: makeApi(),
73
- });
74
-
75
- expect(MockCentrifuge).toHaveBeenCalledWith(
76
- 'ws://localhost:8000/connection/websocket',
77
- expect.objectContaining({ getToken: expect.any(Function) })
78
- );
79
- // Must NOT pass raw token as a field
80
- const [[, opts]] = MockCentrifuge.mock.calls;
81
- expect(opts).not.toHaveProperty('token');
82
-
83
- expect(mockClient.newSubscription).toHaveBeenCalledWith(`agent:${AGENT_ID}`);
84
- expect(mockClient.newSubscription).toHaveBeenCalledTimes(1);
85
- expect(commandSub.subscribe).toHaveBeenCalled();
86
- expect(mockClient.connect).toHaveBeenCalled();
87
- expect(commandSub.on).toHaveBeenCalledWith('publication', expect.any(Function));
88
- });
89
-
90
- it('getToken fetches JWT from backend with correct headers', async () => {
91
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
92
- const mockClient = createMockClient([commandSub]);
93
- let capturedGetToken: (() => Promise<string>) | undefined;
94
- MockCentrifuge.mockImplementation((_url, opts: any) => {
95
- capturedGetToken = opts.getToken;
96
- return mockClient as any;
97
- });
98
-
99
- const { createConnection } = await import('../src/connection');
100
- createConnection({
101
- url: 'ws://localhost:8000/connection/websocket',
102
- token: AGENT_TOKEN,
103
- agentId: AGENT_ID,
104
- backendUrl: BACKEND_URL,
105
- api: makeApi(),
106
- });
107
-
108
- expect(capturedGetToken).toBeDefined();
109
- const result = await capturedGetToken!();
110
-
111
- expect(result).toBe(JWT);
112
- expect(global.fetch).toHaveBeenCalledWith(
113
- `${BACKEND_URL}/api/internal/centrifugo-token`,
114
- expect.objectContaining({
115
- method: 'POST',
116
- headers: expect.objectContaining({
117
- 'Authorization': `Bearer ${AGENT_TOKEN}`,
118
- 'Content-Type': 'application/json',
119
- }),
120
- body: JSON.stringify({ agentId: AGENT_ID }),
121
- })
122
- );
123
- });
124
-
125
- it('getToken retries once and succeeds on second attempt', async () => {
126
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
127
- const mockClient = createMockClient([commandSub]);
128
- let capturedGetToken: (() => Promise<string>) | undefined;
129
- MockCentrifuge.mockImplementation((_url, opts: any) => {
130
- capturedGetToken = opts.getToken;
131
- return mockClient as any;
132
- });
133
-
134
- // First call fails, second succeeds
135
- global.fetch = jest.fn()
136
- .mockRejectedValueOnce(new Error('network error'))
137
- .mockResolvedValueOnce({
138
- ok: true,
139
- json: () => Promise.resolve({ token: JWT }),
140
- } as unknown as Response);
141
-
142
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
143
-
144
- const { createConnection } = await import('../src/connection');
145
- createConnection({
146
- url: 'ws://localhost:8000/connection/websocket',
147
- token: AGENT_TOKEN,
148
- agentId: AGENT_ID,
149
- backendUrl: BACKEND_URL,
150
- api: makeApi(),
151
- });
152
-
153
- const result = await capturedGetToken!();
154
- expect(result).toBe(JWT);
155
- expect(global.fetch).toHaveBeenCalledTimes(2);
156
- expect(consoleSpy).toHaveBeenCalledWith(
157
- expect.stringContaining('Failed to fetch Centrifugo JWT'),
158
- expect.anything()
159
- );
160
- consoleSpy.mockRestore();
161
- });
162
-
163
- it('getToken throws after both attempts fail', async () => {
164
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
165
- const mockClient = createMockClient([commandSub]);
166
- let capturedGetToken: (() => Promise<string>) | undefined;
167
- MockCentrifuge.mockImplementation((_url, opts: any) => {
168
- capturedGetToken = opts.getToken;
169
- return mockClient as any;
170
- });
171
-
172
- const fetchError = new Error('network down');
173
- global.fetch = jest.fn().mockRejectedValue(fetchError);
174
-
175
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
176
-
177
- const { createConnection } = await import('../src/connection');
178
- createConnection({
179
- url: 'ws://localhost:8000/connection/websocket',
180
- token: AGENT_TOKEN,
181
- agentId: AGENT_ID,
182
- backendUrl: BACKEND_URL,
183
- api: makeApi(),
184
- });
185
-
186
- await expect(capturedGetToken!()).rejects.toThrow('network down');
187
- expect(global.fetch).toHaveBeenCalledTimes(2);
188
- consoleSpy.mockRestore();
189
- });
190
-
191
- it('getToken throws when backend returns non-ok HTTP status', async () => {
192
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
193
- const mockClient = createMockClient([commandSub]);
194
- let capturedGetToken: (() => Promise<string>) | undefined;
195
- MockCentrifuge.mockImplementation((_url, opts: any) => {
196
- capturedGetToken = opts.getToken;
197
- return mockClient as any;
198
- });
199
-
200
- global.fetch = jest.fn().mockResolvedValue({
201
- ok: false,
202
- status: 401,
203
- text: () => Promise.resolve('Unauthorized'),
204
- } as unknown as Response);
205
-
206
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
207
-
208
- const { createConnection } = await import('../src/connection');
209
- createConnection({
210
- url: 'ws://localhost:8000/connection/websocket',
211
- token: AGENT_TOKEN,
212
- agentId: AGENT_ID,
213
- backendUrl: BACKEND_URL,
214
- api: makeApi(),
215
- });
216
-
217
- await expect(capturedGetToken!()).rejects.toThrow('HTTP 401');
218
- // Both attempts return 401 → called twice
219
- expect(global.fetch).toHaveBeenCalledTimes(2);
220
- consoleSpy.mockRestore();
221
- });
222
-
223
- it('routes exec command to handler and publishes via api.publishResponse', async () => {
224
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
225
- const mockClient = createMockClient([commandSub]);
226
- MockCentrifuge.mockImplementation(() => mockClient as any);
227
-
228
- jest.mock('node:child_process', () => ({
229
- exec: jest.fn((_cmd: string, _opts: unknown, cb: Function) => {
230
- cb(null, Buffer.from('output'), Buffer.from(''));
231
- }),
232
- }));
233
-
234
- const api = makeApi();
235
- const { createConnection } = await import('../src/connection');
236
- createConnection({
237
- url: 'ws://localhost:8000/connection/websocket',
238
- token: AGENT_TOKEN,
239
- agentId: AGENT_ID,
240
- backendUrl: BACKEND_URL,
241
- api,
242
- });
243
-
244
- // Simulate a publication on the command channel
245
- const publicationHandler = commandSub._handlers['publication'];
246
- expect(publicationHandler).toBeDefined();
247
-
248
- await publicationHandler({
249
- data: { id: 'cmd-1', type: 'exec', payload: { command: 'echo hi' } },
250
- channel: `agent:${AGENT_ID}`,
251
- });
252
-
253
- // Should have published a response via api, not client.publish
254
- expect(mockClient.publish).not.toHaveBeenCalled();
255
- expect(api.publishResponse).toHaveBeenCalledWith(
256
- AGENT_ID,
257
- expect.objectContaining({ id: 'cmd-1', type: 'exec' })
258
- );
259
- });
260
-
261
- it('handles unknown command types gracefully', async () => {
262
- const commandSub = createMockSubscription(`agent:${AGENT_ID}`);
263
- const mockClient = createMockClient([commandSub]);
264
- MockCentrifuge.mockImplementation(() => mockClient as any);
265
-
266
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
267
-
268
- const api = makeApi();
269
- const { createConnection } = await import('../src/connection');
270
- createConnection({
271
- url: 'ws://localhost:8000/connection/websocket',
272
- token: AGENT_TOKEN,
273
- agentId: AGENT_ID,
274
- backendUrl: BACKEND_URL,
275
- api,
276
- });
277
-
278
- const publicationHandler = commandSub._handlers['publication'];
279
- await publicationHandler({
280
- data: { id: 'cmd-2', type: 'unknown_cmd', payload: {} },
281
- channel: `agent:${AGENT_ID}`,
282
- });
283
-
284
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown command type'));
285
- expect(mockClient.publish).not.toHaveBeenCalled();
286
- expect(api.publishResponse).not.toHaveBeenCalled();
287
- consoleSpy.mockRestore();
288
- });
289
- });
@@ -1,90 +0,0 @@
1
- import { handleFileDelete } from '../src/handlers/file-delete';
2
- import type { AgentCommand } from '../src/types';
3
- import fs from 'node:fs/promises';
4
-
5
- jest.mock('node:fs/promises');
6
-
7
- const mockUnlink = fs.unlink as jest.Mock;
8
-
9
- const baseCmd: AgentCommand = { id: '1', type: 'file_delete', payload: {} };
10
-
11
- describe('handleFileDelete', () => {
12
- beforeEach(() => {
13
- jest.clearAllMocks();
14
- mockUnlink.mockResolvedValue(undefined);
15
- });
16
-
17
- it('deletes file and returns success', async () => {
18
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'config/agent.yml' } };
19
- const res = await handleFileDelete(cmd);
20
- expect(res.success).toBe(true);
21
- expect(mockUnlink).toHaveBeenCalledWith(expect.stringContaining('agent.yml'));
22
- });
23
-
24
- it('returns resolved path in data on success', async () => {
25
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'some/file.txt' } };
26
- const res = await handleFileDelete(cmd);
27
- expect(res.success).toBe(true);
28
- expect(res.data?.path).toEqual(expect.stringContaining('file.txt'));
29
- });
30
-
31
- it('rejects path traversal with ".."', async () => {
32
- const cmd: AgentCommand = { ...baseCmd, payload: { path: '../etc/passwd' } };
33
- const res = await handleFileDelete(cmd);
34
- expect(res.success).toBe(false);
35
- expect(res.error).toContain('Invalid path');
36
- expect(mockUnlink).not.toHaveBeenCalled();
37
- });
38
-
39
- it('rejects nested path traversal (foo/../bar)', async () => {
40
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'foo/../bar/secret' } };
41
- const res = await handleFileDelete(cmd);
42
- expect(res.success).toBe(false);
43
- expect(res.error).toContain('Invalid path');
44
- expect(mockUnlink).not.toHaveBeenCalled();
45
- });
46
-
47
- it('rejects absolute path starting with "/"', async () => {
48
- const cmd: AgentCommand = { ...baseCmd, payload: { path: '/etc/passwd' } };
49
- const res = await handleFileDelete(cmd);
50
- expect(res.success).toBe(false);
51
- expect(res.error).toContain('Invalid path');
52
- expect(mockUnlink).not.toHaveBeenCalled();
53
- });
54
-
55
- it('returns error when path is missing', async () => {
56
- const cmd: AgentCommand = { ...baseCmd, payload: {} };
57
- const res = await handleFileDelete(cmd);
58
- expect(res.success).toBe(false);
59
- expect(res.error).toContain('Missing');
60
- expect(mockUnlink).not.toHaveBeenCalled();
61
- });
62
-
63
- it('returns error when unlink fails (file not found)', async () => {
64
- mockUnlink.mockRejectedValue(new Error('ENOENT: no such file or directory'));
65
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'missing.txt' } };
66
- const res = await handleFileDelete(cmd);
67
- expect(res.success).toBe(false);
68
- expect(res.error).toContain('ENOENT');
69
- });
70
-
71
- it('returns error when unlink fails (permission denied)', async () => {
72
- mockUnlink.mockRejectedValue(new Error('EACCES: permission denied'));
73
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'protected.txt' } };
74
- const res = await handleFileDelete(cmd);
75
- expect(res.success).toBe(false);
76
- expect(res.error).toContain('EACCES');
77
- });
78
-
79
- it('response type is file_delete', async () => {
80
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'file.txt' } };
81
- const res = await handleFileDelete(cmd);
82
- expect(res.type).toBe('file_delete');
83
- });
84
-
85
- it('response id matches command id', async () => {
86
- const cmd: AgentCommand = { ...baseCmd, id: 'unique-cmd-id', payload: { path: 'file.txt' } };
87
- const res = await handleFileDelete(cmd);
88
- expect(res.id).toBe('unique-cmd-id');
89
- });
90
- });
@@ -1,119 +0,0 @@
1
- import { handleFileWrite } from '../src/handlers/file-write';
2
- import type { AgentCommand } from '../src/types';
3
- import fs from 'node:fs/promises';
4
-
5
- jest.mock('node:fs/promises');
6
-
7
- const mockMkdir = fs.mkdir as jest.Mock;
8
- const mockWriteFile = fs.writeFile as jest.Mock;
9
- const mockAccess = fs.access as jest.Mock;
10
-
11
- const baseCmd: AgentCommand = { id: '1', type: 'file_write', payload: {} };
12
-
13
- describe('handleFileWrite', () => {
14
- beforeEach(() => {
15
- jest.clearAllMocks();
16
- mockMkdir.mockResolvedValue(undefined);
17
- mockWriteFile.mockResolvedValue(undefined);
18
- });
19
-
20
- it('writes file and returns written:true', async () => {
21
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'config/agent.yml', content: 'key: val' } };
22
- const res = await handleFileWrite(cmd);
23
- expect(res.success).toBe(true);
24
- expect(res.data?.written).toBe(true);
25
- expect(mockWriteFile).toHaveBeenCalledWith(
26
- expect.stringContaining('agent.yml'),
27
- 'key: val',
28
- 'utf-8',
29
- );
30
- });
31
-
32
- it('skips write when overwrite=false and file already exists', async () => {
33
- mockAccess.mockResolvedValue(undefined); // file exists
34
- const cmd: AgentCommand = {
35
- ...baseCmd,
36
- payload: { path: 'config/agent.yml', content: 'key: val', overwrite: false },
37
- };
38
- const res = await handleFileWrite(cmd);
39
- expect(res.success).toBe(true);
40
- expect(res.data?.written).toBe(false);
41
- expect(res.data?.skipped).toBe(true);
42
- expect(mockWriteFile).not.toHaveBeenCalled();
43
- });
44
-
45
- it('writes when overwrite=false and file does not exist', async () => {
46
- mockAccess.mockRejectedValue(new Error('ENOENT'));
47
- const cmd: AgentCommand = {
48
- ...baseCmd,
49
- payload: { path: 'config/new.yml', content: 'data', overwrite: false },
50
- };
51
- const res = await handleFileWrite(cmd);
52
- expect(res.success).toBe(true);
53
- expect(res.data?.written).toBe(true);
54
- expect(mockWriteFile).toHaveBeenCalled();
55
- });
56
-
57
- it('rejects path traversal with ".."', async () => {
58
- const cmd: AgentCommand = { ...baseCmd, payload: { path: '../etc/passwd', content: 'root' } };
59
- const res = await handleFileWrite(cmd);
60
- expect(res.success).toBe(false);
61
- expect(res.error).toContain('Invalid path');
62
- expect(mockWriteFile).not.toHaveBeenCalled();
63
- });
64
-
65
- it('rejects nested path traversal (foo/../bar)', async () => {
66
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'foo/../bar/secret', content: 'x' } };
67
- const res = await handleFileWrite(cmd);
68
- expect(res.success).toBe(false);
69
- expect(res.error).toContain('Invalid path');
70
- expect(mockWriteFile).not.toHaveBeenCalled();
71
- });
72
-
73
- it('rejects absolute path starting with "/"', async () => {
74
- const cmd: AgentCommand = { ...baseCmd, payload: { path: '/etc/passwd', content: 'root' } };
75
- const res = await handleFileWrite(cmd);
76
- expect(res.success).toBe(false);
77
- expect(res.error).toContain('Invalid path');
78
- expect(mockWriteFile).not.toHaveBeenCalled();
79
- });
80
-
81
- it('creates parent directories before writing', async () => {
82
- const cmd: AgentCommand = {
83
- ...baseCmd,
84
- payload: { path: 'deep/nested/dir/file.txt', content: 'hello' },
85
- };
86
- const res = await handleFileWrite(cmd);
87
- expect(res.success).toBe(true);
88
- expect(mockMkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true });
89
- });
90
-
91
- it('returns error when path is missing', async () => {
92
- const cmd: AgentCommand = { ...baseCmd, payload: { content: 'data' } };
93
- const res = await handleFileWrite(cmd);
94
- expect(res.success).toBe(false);
95
- expect(res.error).toContain('Missing');
96
- });
97
-
98
- it('returns error when content is missing', async () => {
99
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'file.txt' } };
100
- const res = await handleFileWrite(cmd);
101
- expect(res.success).toBe(false);
102
- expect(res.error).toContain('Missing');
103
- });
104
-
105
- it('allows empty string content', async () => {
106
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'empty.txt', content: '' } };
107
- const res = await handleFileWrite(cmd);
108
- expect(res.success).toBe(true);
109
- expect(mockWriteFile).toHaveBeenCalledWith(expect.any(String), '', 'utf-8');
110
- });
111
-
112
- it('returns error when writeFile fails', async () => {
113
- mockWriteFile.mockRejectedValue(new Error('EACCES: permission denied'));
114
- const cmd: AgentCommand = { ...baseCmd, payload: { path: 'file.txt', content: 'data' } };
115
- const res = await handleFileWrite(cmd);
116
- expect(res.success).toBe(false);
117
- expect(res.error).toContain('EACCES');
118
- });
119
- });