-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathworkspace.test.ts
More file actions
393 lines (323 loc) · 15 KB
/
Copy pathworkspace.test.ts
File metadata and controls
393 lines (323 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import fs from 'node:fs';
import fsPromises from 'node:fs/promises';
import path from 'node:path';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
getWorkspaceRoot,
getChatSettingsPath,
getAgentSessionSettingsPath,
readChatSettings,
writeChatSettings,
readAgentSessionSettings,
writeAgentSessionSettings,
isValidAgentId,
getAgentDir,
getAgentSettingsPath,
getAgent,
writeAgentSettings,
listAgents,
deleteAgent,
resolveTemplatePath,
copyTemplate,
readSettings,
writeSettings,
readEnvironment,
getActiveEnvironmentName,
resolveTargetAgentSkillsDir,
} from './workspace.js';
import type { Agent, Settings, Environment } from './config.js';
describe('workspace utilities', () => {
const testDir = path.join(process.cwd(), '.clawmini-test-workspace');
const clawminiDir = path.join(testDir, '.clawmini');
beforeEach(async () => {
await fsPromises.mkdir(clawminiDir, { recursive: true });
});
afterEach(async () => {
if (fs.existsSync(testDir)) {
await fsPromises.rm(testDir, { recursive: true, force: true });
}
});
describe('path resolution', () => {
it('should resolve workspace root to the directory containing .clawmini', () => {
const startDir = path.join(testDir, 'some', 'deep', 'dir');
fs.mkdirSync(startDir, { recursive: true });
expect(getWorkspaceRoot(startDir)).toBe(testDir);
});
it('should return getChatSettingsPath correctly', () => {
const p = getChatSettingsPath('test-chat', testDir);
expect(p).toBe(path.join(clawminiDir, 'chats', 'test-chat', 'settings.json'));
});
it('should return getAgentSessionSettingsPath correctly', () => {
const p = getAgentSessionSettingsPath('test-agent', 'test-session', testDir);
expect(p).toBe(
path.join(clawminiDir, 'agents', 'test-agent', 'sessions', 'test-session', 'settings.json')
);
});
});
describe('isValidAgentId', () => {
it('should return true for valid IDs', () => {
expect(isValidAgentId('my-agent')).toBe(true);
expect(isValidAgentId('agent123')).toBe(true);
expect(isValidAgentId('A_b-c')).toBe(true);
});
it('should return false for invalid IDs', () => {
expect(isValidAgentId('')).toBe(false);
expect(isValidAgentId('../my-agent')).toBe(false);
expect(isValidAgentId('my/agent')).toBe(false);
expect(isValidAgentId('my\\agent')).toBe(false);
});
});
describe('getAgentDir & getAgentSettingsPath', () => {
it('should throw on invalid agent ID', () => {
expect(() => getAgentDir('../invalid', testDir)).toThrow('Invalid agent ID');
expect(() => getAgentSettingsPath('../invalid', testDir)).toThrow('Invalid agent ID');
});
it('should return correct path for valid agent ID', () => {
expect(getAgentDir('agent-1', testDir)).toBe(path.join(clawminiDir, 'agents', 'agent-1'));
expect(getAgentSettingsPath('agent-1', testDir)).toBe(
path.join(clawminiDir, 'agents', 'agent-1', 'settings.json')
);
});
});
describe('Chat Settings read/write', () => {
it('should return null if chat settings do not exist', async () => {
const settings = await readChatSettings('non-existent', testDir);
expect(settings).toBeNull();
});
it('should write and read chat settings', async () => {
const data = { defaultAgent: 'agent-1' };
await writeChatSettings('chat-1', data, testDir);
const p = getChatSettingsPath('chat-1', testDir);
expect(fs.existsSync(p)).toBe(true);
const settings = await readChatSettings('chat-1', testDir);
expect(settings).toEqual(data);
});
it('should clean up locks after updateChatSettings (no memory leak)', async () => {
const { chatSettingsLocks, updateChatSettings } = await import('./workspace.js');
expect(chatSettingsLocks.size).toBe(0);
await updateChatSettings('leak-chat', (settings) => settings, testDir);
expect(chatSettingsLocks.size).toBe(0);
expect(chatSettingsLocks.has('leak-chat')).toBe(false);
});
it('should return null if JSON is invalid', async () => {
const p = getChatSettingsPath('chat-invalid', testDir);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, '{ invalid json', 'utf-8');
const settings = await readChatSettings('chat-invalid', testDir);
expect(settings).toBeNull();
});
});
describe('Agent Settings read/write', () => {
it('should return null if agent settings do not exist', async () => {
const agent = await getAgent('non-existent', testDir);
expect(agent).toBeNull();
});
it('should throw an error if agent settings JSON is invalid', async () => {
const p = getAgentSettingsPath('agent-invalid-json', testDir);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, '{ invalid json', 'utf-8');
await expect(getAgent('agent-invalid-json', testDir)).rejects.toThrow(/Invalid JSON/);
});
it('should throw an error if agent settings schema is invalid', async () => {
const p = getAgentSettingsPath('agent-invalid-schema', testDir);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify({ directory: 123 }), 'utf-8'); // directory should be string
await expect(getAgent('agent-invalid-schema', testDir)).rejects.toThrow(/Invalid schema/);
});
it('should write and read agent settings', async () => {
const agentData: Agent = {
env: { FOO: 'bar' },
directory: './test-dir',
commands: { new: 'test-new' },
};
await writeAgentSettings('agent-1', agentData, testDir);
const p = getAgentSettingsPath('agent-1', testDir);
expect(fs.existsSync(p)).toBe(true);
const agent = await getAgent('agent-1', testDir);
expect(agent).toEqual({ ...agentData, files: './attachments' });
});
it('should return list of agents', async () => {
const agentData: Agent = { env: { FOO: 'bar' } };
await writeAgentSettings('agent-a', agentData, testDir);
await writeAgentSettings('agent-b', agentData, testDir);
const agentsDir = path.join(clawminiDir, 'agents');
// Create a dummy dir without settings.json
await fsPromises.mkdir(path.join(agentsDir, 'agent-c'), { recursive: true });
const list = await listAgents(testDir);
expect(list.sort()).toEqual(['agent-a', 'agent-b']);
});
it('should delete agent', async () => {
const agentData: Agent = { env: { FOO: 'bar' } };
await writeAgentSettings('agent-to-delete', agentData, testDir);
let list = await listAgents(testDir);
expect(list).toContain('agent-to-delete');
await deleteAgent('agent-to-delete', testDir);
list = await listAgents(testDir);
expect(list).not.toContain('agent-to-delete');
});
});
describe('resolveTargetAgentSkillsDir', () => {
it('should throw if agent directory does not exist', async () => {
await expect(resolveTargetAgentSkillsDir('non-existent', testDir)).rejects.toThrow(
'Agent not found: non-existent'
);
});
it('should resolve custom skillsDir if settings.json is valid', async () => {
const agentData: Agent = { skillsDir: 'custom-skills-dir' };
await writeAgentSettings('agent-custom-skills', agentData, testDir);
const resolved = await resolveTargetAgentSkillsDir('agent-custom-skills', testDir);
expect(resolved).toBe(path.join(testDir, 'agent-custom-skills', 'custom-skills-dir'));
});
it('should fall back to .agents/skills if skillsDir is missing from settings', async () => {
const agentData: Agent = { env: {} };
await writeAgentSettings('agent-no-skillsdir', agentData, testDir);
const resolved = await resolveTargetAgentSkillsDir('agent-no-skillsdir', testDir);
expect(resolved).toBe(path.join(testDir, 'agent-no-skillsdir', '.agents', 'skills'));
});
it('should fall back to .agents/skills if settings.json is missing or malformed', async () => {
const agentDir = path.join(clawminiDir, 'agents', 'agent-malformed');
await fsPromises.mkdir(agentDir, { recursive: true });
await fsPromises.writeFile(path.join(agentDir, 'settings.json'), '{ malformed json', 'utf-8');
const resolved = await resolveTargetAgentSkillsDir('agent-malformed', testDir);
expect(resolved).toBe(path.join(testDir, 'agent-malformed', '.agents', 'skills'));
});
});
describe('Agent Session Settings read/write', () => {
it('should return null if agent session settings do not exist', async () => {
const settings = await readAgentSessionSettings('agent-1', 'session-1', testDir);
expect(settings).toBeNull();
});
it('should write and read agent session settings', async () => {
const data = { context: 'some context', step: 5 };
await writeAgentSessionSettings('agent-1', 'session-1', data, testDir);
const p = getAgentSessionSettingsPath('agent-1', 'session-1', testDir);
expect(fs.existsSync(p)).toBe(true);
const settings = await readAgentSessionSettings('agent-1', 'session-1', testDir);
expect(settings).toEqual(data);
});
it('should return null if JSON is invalid', async () => {
const p = getAgentSessionSettingsPath('agent-invalid', 'session-invalid', testDir);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, 'invalid json }', 'utf-8');
const settings = await readAgentSessionSettings('agent-invalid', 'session-invalid', testDir);
expect(settings).toBeNull();
});
});
describe('Template resolution and copying', () => {
it('should resolve local template first', async () => {
const templateName = 'test-template';
const localTemplateDir = path.join(clawminiDir, 'templates', templateName);
await fsPromises.mkdir(localTemplateDir, { recursive: true });
const resolved = await resolveTemplatePath(templateName, testDir);
expect(resolved).toBe(localTemplateDir);
});
it('should fall back to built-in template', async () => {
const templateName = 'test-builtin';
// Create a dummy builtin template in the project root's templates dir
const workspaceRoot = getWorkspaceRoot(process.cwd());
const builtinTemplateDir = path.join(workspaceRoot, 'templates', templateName);
await fsPromises.mkdir(builtinTemplateDir, { recursive: true });
try {
const resolved = await resolveTemplatePath(templateName, testDir);
expect(resolved).toBe(builtinTemplateDir);
} finally {
await fsPromises.rm(builtinTemplateDir, { recursive: true, force: true });
}
});
it('should throw if template not found', async () => {
await expect(resolveTemplatePath('non-existent-template', testDir)).rejects.toThrow(
'Template not found: non-existent-template'
);
});
it('should copy template to empty directory', async () => {
const templateName = 'copy-template';
const localTemplateDir = path.join(clawminiDir, 'templates', templateName);
await fsPromises.mkdir(localTemplateDir, { recursive: true });
await fsPromises.writeFile(path.join(localTemplateDir, 'file.txt'), 'hello', 'utf-8');
const targetDir = path.join(testDir, 'target-dir');
await fsPromises.mkdir(targetDir, { recursive: true });
await copyTemplate(templateName, targetDir, testDir);
const content = await fsPromises.readFile(path.join(targetDir, 'file.txt'), 'utf-8');
expect(content).toBe('hello');
});
it('should fail if target directory is not empty', async () => {
const templateName = 'copy-template-fail';
const localTemplateDir = path.join(clawminiDir, 'templates', templateName);
await fsPromises.mkdir(localTemplateDir, { recursive: true });
const targetDir = path.join(testDir, 'target-dir-fail');
await fsPromises.mkdir(targetDir, { recursive: true });
await fsPromises.writeFile(path.join(targetDir, 'existing.txt'), 'existing', 'utf-8');
await expect(copyTemplate(templateName, targetDir, testDir)).rejects.toThrow(
`Target directory is not empty: ${targetDir}`
);
});
it('should fail if target directory does not exist', async () => {
const templateName = 'copy-template-create';
const localTemplateDir = path.join(clawminiDir, 'templates', templateName);
await fsPromises.mkdir(localTemplateDir, { recursive: true });
await fsPromises.writeFile(path.join(localTemplateDir, 'file.txt'), 'hello', 'utf-8');
const targetDir = path.join(testDir, 'target-dir-create');
// Intentionally not creating the target directory
await expect(copyTemplate(templateName, targetDir, testDir)).rejects.toThrow(
`Target directory does not exist: ${targetDir}`
);
});
});
describe('Settings and Environments', () => {
it('should read and write settings', async () => {
const data: Settings = {
environments: { './': 'default-env' },
files: './files',
timestampPrefix: true,
};
await writeSettings(data, testDir);
const read = await readSettings(testDir);
expect(read).toEqual(data);
});
it('should return null if reading non-existent settings', async () => {
const read = await readSettings(testDir);
expect(read).toBeNull();
});
it('should return null if reading non-existent environment', async () => {
const read = await readEnvironment('non-existent', testDir);
expect(read).toBeNull();
});
it('should read environment env.json', async () => {
const envDir = path.join(clawminiDir, 'environments', 'test-env');
await fsPromises.mkdir(envDir, { recursive: true });
const envData: Environment = { prefix: 'test run {ENV_ARGS}' };
await fsPromises.writeFile(path.join(envDir, 'env.json'), JSON.stringify(envData));
const read = await readEnvironment('test-env', testDir);
expect(read).toEqual(envData);
});
it('should get active environment name based on specificity', async () => {
const data: Settings = {
environments: {
'./': 'root-env',
'./agents': 'agents-env',
'./agents/specific-agent': 'specific-env',
},
};
await writeSettings(data, testDir);
expect(await getActiveEnvironmentName('./', testDir)).toBe('root-env');
expect(await getActiveEnvironmentName('./other', testDir)).toBe('root-env');
expect(await getActiveEnvironmentName('./agents/some-agent', testDir)).toBe('agents-env');
expect(await getActiveEnvironmentName('./agents/specific-agent', testDir)).toBe(
'specific-env'
);
expect(await getActiveEnvironmentName('./agents/specific-agent/sub', testDir)).toBe(
'specific-env'
);
});
it('should return null if no environment matches', async () => {
const data: Settings = {
environments: {
'./agents': 'agents-env',
},
};
await writeSettings(data, testDir);
// './' is not inside './agents'
expect(await getActiveEnvironmentName('./', testDir)).toBeNull();
});
});
});