@jamaynor/hal-config 1.0.2 → 1.1.1
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/index.d.ts +4 -0
- package/lib/config.d.ts +111 -0
- package/lib/config.js +257 -18
- package/lib/types.d.ts +218 -0
- package/package.json +23 -4
- package/security/index.d.ts +33 -0
- package/test-utils.d.ts +30 -0
- package/CLAUDE.md +0 -84
- package/publish.ps1 +0 -30
- package/test/config-io.test.js +0 -326
- package/test/security.test.js +0 -488
- package/test/test-utils.test.js +0 -360
- package/test/test.js +0 -586
package/test/test.js
DELETED
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { dirname } from 'node:path';
|
|
7
|
-
|
|
8
|
-
import { makeTmpDir, writeJson, cleanup } from '../test-utils.js';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
// Fresh dynamic import for each test to get a clean module-level cache
|
|
13
|
-
async function freshImport() {
|
|
14
|
-
const modPath = path.resolve(__dirname, '../lib/config.js');
|
|
15
|
-
const url = `file:///${modPath.replace(/\\/g, '/')}?t=${Date.now()}`;
|
|
16
|
-
return import(url);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function readConfig(dir) {
|
|
20
|
-
return JSON.parse(fs.readFileSync(
|
|
21
|
-
path.join(dir, 'hal-system-config.json'), 'utf8'));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const SAMPLE_CONFIG = {
|
|
25
|
-
version: '2.0',
|
|
26
|
-
repoOrg: 'jamaynor',
|
|
27
|
-
'system-emails': [
|
|
28
|
-
'me@gmail.com',
|
|
29
|
-
'work@company.com',
|
|
30
|
-
],
|
|
31
|
-
'hal-obsidian-vaults': [
|
|
32
|
-
{ name: 'ja-vault', path: '/vaults/ja', label: 'Jeremy', primary: true,
|
|
33
|
-
'projects-folder': '1-Projects',
|
|
34
|
-
'daily-notes-folder': 'areas/daily-notes',
|
|
35
|
-
'email-folder': 'areas/email',
|
|
36
|
-
'meetings-folder': 'areas/meetings',
|
|
37
|
-
'people-folder': 'areas/people',
|
|
38
|
-
},
|
|
39
|
-
{ name: 'lmb-vault', path: '/vaults/lmb', label: 'LMB',
|
|
40
|
-
'projects-folder': '1-Projects' },
|
|
41
|
-
],
|
|
42
|
-
'hal-communication-accounts': [
|
|
43
|
-
{ label: 'me@gmail.com', provider: 'google', email: 'me@gmail.com',
|
|
44
|
-
scopes: ['email', 'calendar'] },
|
|
45
|
-
{ label: 'work@company.com', provider: 'ms365', email: 'work@company.com',
|
|
46
|
-
scopes: ['email'] },
|
|
47
|
-
],
|
|
48
|
-
'hal-timezone': 'America/Chicago',
|
|
49
|
-
'hal-work-hours': { start: '09:00', end: '17:00' },
|
|
50
|
-
skills: {
|
|
51
|
-
'secret-manager-bws': {
|
|
52
|
-
enabled: true, homeAgent: null,
|
|
53
|
-
'environment-variable-prefix': 'HAL_BWS_',
|
|
54
|
-
install: { repo: 'openclaw-skill-secret-manager-bws',
|
|
55
|
-
package: 'openclaw-skill-secret-manager-bws',
|
|
56
|
-
binary: 'secrets-bws', version: 'main' },
|
|
57
|
-
runtime: {},
|
|
58
|
-
},
|
|
59
|
-
'project-manager': {
|
|
60
|
-
enabled: true, homeAgent: 'cto',
|
|
61
|
-
'environment-variable-prefix': 'HAL_PROJ_MGR_',
|
|
62
|
-
install: { repo: 'openclaw-skill-project-manager',
|
|
63
|
-
package: 'openclaw-skill-project-manager',
|
|
64
|
-
binary: ['project', 'project-mgmt'], version: 'latest' },
|
|
65
|
-
runtime: { workspace: '/data/agents/hal' },
|
|
66
|
-
},
|
|
67
|
-
'plan-your-day': {
|
|
68
|
-
enabled: true, homeAgent: null,
|
|
69
|
-
'environment-variable-prefix': 'HAL_PLAN_DAY_',
|
|
70
|
-
install: { repo: 'openclaw-skill-plan-your-day',
|
|
71
|
-
package: 'openclaw-skill-plan-your-day',
|
|
72
|
-
binary: 'plan-day', version: 'main' },
|
|
73
|
-
runtime: {},
|
|
74
|
-
},
|
|
75
|
-
'disabled-skill': {
|
|
76
|
-
enabled: false, homeAgent: null,
|
|
77
|
-
install: { binary: 'disabled-bin' },
|
|
78
|
-
runtime: {},
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
halManagedEntries: [],
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// Tests
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
describe('hal-shared-config', () => {
|
|
89
|
-
let tmpDir;
|
|
90
|
-
let hal;
|
|
91
|
-
let originalAdminRoot;
|
|
92
|
-
|
|
93
|
-
beforeEach(async () => {
|
|
94
|
-
tmpDir = makeTmpDir('hsc-test-');
|
|
95
|
-
writeJson(path.join(tmpDir, 'hal-system-config.json'), SAMPLE_CONFIG);
|
|
96
|
-
originalAdminRoot = process.env.HAL_SKILL_ADMIN_ROOT;
|
|
97
|
-
process.env.HAL_SKILL_ADMIN_ROOT = tmpDir;
|
|
98
|
-
|
|
99
|
-
// Skill independent config file (deterministic under HAL_SKILL_ADMIN_ROOT)
|
|
100
|
-
writeJson(
|
|
101
|
-
path.join(tmpDir, 'email-triage', 'config', 'email-triage.json'),
|
|
102
|
-
{ gog_client_id: 'test-gog-client-id' }
|
|
103
|
-
);
|
|
104
|
-
hal = await freshImport();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
afterEach(() => {
|
|
108
|
-
if (originalAdminRoot === undefined) delete process.env.HAL_SKILL_ADMIN_ROOT;
|
|
109
|
-
else process.env.HAL_SKILL_ADMIN_ROOT = originalAdminRoot;
|
|
110
|
-
cleanup(tmpDir);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// --------------------------------------------------------------------------
|
|
114
|
-
// Loading
|
|
115
|
-
// --------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
describe('load()', () => {
|
|
118
|
-
it('reads hal-system-config.json from given dir', () => {
|
|
119
|
-
const cfg = hal.load(tmpDir);
|
|
120
|
-
assert.equal(cfg.version, '2.0');
|
|
121
|
-
assert.equal(cfg.repoOrg, 'jamaynor');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('returns empty object when file is missing', () => {
|
|
125
|
-
const emptyDir = makeTmpDir('hsc-test-');
|
|
126
|
-
const cfg = hal.load(emptyDir);
|
|
127
|
-
assert.deepStrictEqual(cfg, {});
|
|
128
|
-
cleanup(emptyDir);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('throws on corrupt JSON', () => {
|
|
132
|
-
fs.writeFileSync(path.join(tmpDir, 'hal-system-config.json'), '{bad', 'utf8');
|
|
133
|
-
assert.throws(() => hal.load(tmpDir), { name: 'SyntaxError' });
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('auto-loads on first accessor call', () => {
|
|
137
|
-
// Set env so auto-load finds our tmp dir
|
|
138
|
-
const orig = process.env.HAL_SYSTEM_CONFIG;
|
|
139
|
-
process.env.HAL_SYSTEM_CONFIG = tmpDir;
|
|
140
|
-
try {
|
|
141
|
-
const v = hal.vaults();
|
|
142
|
-
assert.equal(v.length, 2);
|
|
143
|
-
} finally {
|
|
144
|
-
if (orig === undefined) delete process.env.HAL_SYSTEM_CONFIG;
|
|
145
|
-
else process.env.HAL_SYSTEM_CONFIG = orig;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('reload()', () => {
|
|
151
|
-
it('re-reads from disk after external change', () => {
|
|
152
|
-
hal.load(tmpDir);
|
|
153
|
-
assert.equal(hal.timezone(), 'America/Chicago');
|
|
154
|
-
|
|
155
|
-
const cfg = readConfig(tmpDir);
|
|
156
|
-
cfg['hal-timezone'] = 'US/Eastern';
|
|
157
|
-
writeJson(path.join(tmpDir, 'hal-system-config.json'), cfg);
|
|
158
|
-
|
|
159
|
-
hal.reload();
|
|
160
|
-
assert.equal(hal.timezone(), 'US/Eastern');
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('raw() and configDir()', () => {
|
|
165
|
-
it('returns full config and resolved dir', () => {
|
|
166
|
-
hal.load(tmpDir);
|
|
167
|
-
assert.equal(hal.raw().repoOrg, 'jamaynor');
|
|
168
|
-
assert.equal(hal.configDir(), tmpDir);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// --------------------------------------------------------------------------
|
|
173
|
-
// Shared data accessors
|
|
174
|
-
// --------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
describe('vaults()', () => {
|
|
177
|
-
it('returns vault array', () => {
|
|
178
|
-
hal.load(tmpDir);
|
|
179
|
-
const v = hal.vaults();
|
|
180
|
-
assert.equal(v.length, 2);
|
|
181
|
-
assert.equal(v[0].name, 'ja-vault');
|
|
182
|
-
assert.equal(v[1].label, 'LMB');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('returns empty array when key missing', () => {
|
|
186
|
-
writeJson(path.join(tmpDir, 'hal-system-config.json'), { version: '1.1' });
|
|
187
|
-
hal.load(tmpDir);
|
|
188
|
-
assert.deepStrictEqual(hal.vaults(), []);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('vaultFolders() and vaultDirectoryPaths()', () => {
|
|
193
|
-
it('returns configured folder subpaths with defaults', () => {
|
|
194
|
-
hal.load(tmpDir);
|
|
195
|
-
const f = hal.vaultFolders('ja-vault');
|
|
196
|
-
assert.equal(f.projectsFolder, '1-Projects');
|
|
197
|
-
assert.equal(f.dailyNotesFolder, 'areas/daily-notes');
|
|
198
|
-
assert.equal(f.emailFolder, 'areas/email');
|
|
199
|
-
assert.equal(f.meetingsFolder, 'areas/meetings');
|
|
200
|
-
assert.equal(f.peopleFolder, 'areas/people');
|
|
201
|
-
|
|
202
|
-
const f2 = hal.vaultFolders('lmb-vault');
|
|
203
|
-
assert.equal(f2.projectsFolder, '1-Projects');
|
|
204
|
-
// defaults
|
|
205
|
-
assert.equal(f2.dailyNotesFolder, 'areas/daily-notes');
|
|
206
|
-
assert.equal(f2.emailFolder, 'areas/email');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('returns resolved full directory paths for a vault', () => {
|
|
210
|
-
hal.load(tmpDir);
|
|
211
|
-
const p = hal.vaultDirectoryPaths('ja-vault');
|
|
212
|
-
assert.equal(p.vaultPath, '/vaults/ja');
|
|
213
|
-
assert.equal(p.projectsPath, '/vaults/ja/1-Projects');
|
|
214
|
-
assert.equal(p.dailyNotesPath, '/vaults/ja/areas/daily-notes');
|
|
215
|
-
assert.equal(p.peoplePath, '/vaults/ja/areas/people');
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('accounts()', () => {
|
|
220
|
-
it('returns all accounts', () => {
|
|
221
|
-
hal.load(tmpDir);
|
|
222
|
-
assert.equal(hal.accounts().length, 2);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('filters by provider', () => {
|
|
226
|
-
hal.load(tmpDir);
|
|
227
|
-
const google = hal.accounts('google');
|
|
228
|
-
assert.equal(google.length, 1);
|
|
229
|
-
assert.equal(google[0].label, 'me@gmail.com');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('returns empty for unknown provider', () => {
|
|
233
|
-
hal.load(tmpDir);
|
|
234
|
-
assert.deepStrictEqual(hal.accounts('imap'), []);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// --------------------------------------------------------------------------
|
|
239
|
-
// Settings (getSetting)
|
|
240
|
-
// --------------------------------------------------------------------------
|
|
241
|
-
|
|
242
|
-
describe('getSetting()', () => {
|
|
243
|
-
it('returns a top-level global setting by key', () => {
|
|
244
|
-
hal.load(tmpDir);
|
|
245
|
-
assert.deepStrictEqual(hal.getSetting('system-emails'), ['me@gmail.com', 'work@company.com']);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('returns a skill-scoped setting from the skill config file', () => {
|
|
249
|
-
hal.load(tmpDir);
|
|
250
|
-
assert.equal(hal.getSetting('email-triage', 'gog_client_id'), 'test-gog-client-id');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('normalizes skill scope prefixes (hal-*)', () => {
|
|
254
|
-
hal.load(tmpDir);
|
|
255
|
-
assert.equal(hal.getSetting('hal-email-triage', 'gog_client_id'), 'test-gog-client-id');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('exposes PascalCase alias GetSetting', () => {
|
|
259
|
-
hal.load(tmpDir);
|
|
260
|
-
assert.equal(hal.GetSetting('hal-email-triage', 'gog_client_id'), 'test-gog-client-id');
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
describe('timezone()', () => {
|
|
265
|
-
it('returns timezone string', () => {
|
|
266
|
-
hal.load(tmpDir);
|
|
267
|
-
assert.equal(hal.timezone(), 'America/Chicago');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('returns null when missing', () => {
|
|
271
|
-
writeJson(path.join(tmpDir, 'hal-system-config.json'), {});
|
|
272
|
-
hal.load(tmpDir);
|
|
273
|
-
assert.equal(hal.timezone(), null);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe('workHours()', () => {
|
|
278
|
-
it('returns work hours object', () => {
|
|
279
|
-
hal.load(tmpDir);
|
|
280
|
-
assert.deepStrictEqual(hal.workHours(), { start: '09:00', end: '17:00' });
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// --------------------------------------------------------------------------
|
|
285
|
-
// Skill accessors
|
|
286
|
-
// --------------------------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
describe('skill()', () => {
|
|
289
|
-
it('returns skill config block', () => {
|
|
290
|
-
hal.load(tmpDir);
|
|
291
|
-
const pm = hal.skill('project-manager');
|
|
292
|
-
assert.equal(pm.enabled, true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('returns null for unknown skill', () => {
|
|
296
|
-
hal.load(tmpDir);
|
|
297
|
-
assert.equal(hal.skill('nonexistent'), null);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe('isEnabled()', () => {
|
|
302
|
-
it('returns true for enabled skill', () => {
|
|
303
|
-
hal.load(tmpDir);
|
|
304
|
-
assert.equal(hal.isEnabled('project-manager'), true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('returns false for disabled skill', () => {
|
|
308
|
-
hal.load(tmpDir);
|
|
309
|
-
assert.equal(hal.isEnabled('disabled-skill'), false);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('returns false for unknown skill', () => {
|
|
313
|
-
hal.load(tmpDir);
|
|
314
|
-
assert.equal(hal.isEnabled('nonexistent'), false);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('skillWorkspace()', () => {
|
|
319
|
-
it('returns workspace path from runtime', () => {
|
|
320
|
-
hal.load(tmpDir);
|
|
321
|
-
assert.equal(hal.skillWorkspace('project-manager'), '/data/agents/hal');
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('returns null when runtime has no workspace', () => {
|
|
325
|
-
hal.load(tmpDir);
|
|
326
|
-
assert.equal(hal.skillWorkspace('secret-manager-bws'), null);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('returns null for unknown skill', () => {
|
|
330
|
-
hal.load(tmpDir);
|
|
331
|
-
assert.equal(hal.skillWorkspace('nonexistent'), null);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
describe('skillBinaries()', () => {
|
|
336
|
-
it('returns array for single binary', () => {
|
|
337
|
-
hal.load(tmpDir);
|
|
338
|
-
assert.deepStrictEqual(hal.skillBinaries('secret-manager-bws'), ['secrets-bws']);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('returns array for multiple binaries', () => {
|
|
342
|
-
hal.load(tmpDir);
|
|
343
|
-
assert.deepStrictEqual(hal.skillBinaries('project-manager'),
|
|
344
|
-
['project', 'project-mgmt']);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('returns empty array for unknown skill', () => {
|
|
348
|
-
hal.load(tmpDir);
|
|
349
|
-
assert.deepStrictEqual(hal.skillBinaries('nonexistent'), []);
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
describe('homeAgent()', () => {
|
|
354
|
-
it('returns homeAgent string when set', () => {
|
|
355
|
-
hal.load(tmpDir);
|
|
356
|
-
assert.equal(hal.homeAgent('project-manager'), 'cto');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('returns null when homeAgent is null', () => {
|
|
360
|
-
hal.load(tmpDir);
|
|
361
|
-
assert.equal(hal.homeAgent('plan-your-day'), null);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('returns null for unknown skill', () => {
|
|
365
|
-
hal.load(tmpDir);
|
|
366
|
-
assert.equal(hal.homeAgent('nonexistent'), null);
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
describe('skillConfigPath()', () => {
|
|
371
|
-
it('returns canonical path under HAL_SKILL_ADMIN_ROOT', () => {
|
|
372
|
-
hal.load(tmpDir);
|
|
373
|
-
const p = hal.skillConfigPath('project-manager');
|
|
374
|
-
assert.equal(p, path.join(tmpDir, 'project-manager', 'config', 'project-manager.json'));
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('skillConfigDir()', () => {
|
|
379
|
-
it('returns the directory containing the skill config file', () => {
|
|
380
|
-
hal.load(tmpDir);
|
|
381
|
-
const d = hal.skillConfigDir('project-manager');
|
|
382
|
-
assert.equal(d, path.join(tmpDir, 'project-manager', 'config'));
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
describe('deterministic filesystem locations', () => {
|
|
387
|
-
it('returns OpenClaw shared workspace root', () => {
|
|
388
|
-
assert.equal(hal.openclawSharedWorkspaceRoot(), '/data/openclaw/workspace');
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('returns OpenClaw config file path', () => {
|
|
392
|
-
assert.equal(hal.openclawConfigFilePath(), '/data/openclaw/openclaw.json');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('returns HAL system config dir/file (default)', () => {
|
|
396
|
-
const orig = process.env.HAL_SYSTEM_CONFIG;
|
|
397
|
-
delete process.env.HAL_SYSTEM_CONFIG;
|
|
398
|
-
try {
|
|
399
|
-
assert.equal(hal.halSystemConfigDir(), '/data/openclaw/hal');
|
|
400
|
-
assert.equal(hal.halSystemConfigFilePath(), path.join('/data/openclaw/hal', 'hal-system-config.json'));
|
|
401
|
-
} finally {
|
|
402
|
-
if (orig === undefined) delete process.env.HAL_SYSTEM_CONFIG;
|
|
403
|
-
else process.env.HAL_SYSTEM_CONFIG = orig;
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('returns HAL system config dir/file (env override)', () => {
|
|
408
|
-
const orig = process.env.HAL_SYSTEM_CONFIG;
|
|
409
|
-
process.env.HAL_SYSTEM_CONFIG = '/tmp/halcfg';
|
|
410
|
-
try {
|
|
411
|
-
assert.equal(hal.halSystemConfigDir(), '/tmp/halcfg');
|
|
412
|
-
assert.equal(hal.halSystemConfigFilePath(), path.join('/tmp/halcfg', 'hal-system-config.json'));
|
|
413
|
-
} finally {
|
|
414
|
-
if (orig === undefined) delete process.env.HAL_SYSTEM_CONFIG;
|
|
415
|
-
else process.env.HAL_SYSTEM_CONFIG = orig;
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('returns per-skill admin/config/log dirs under HAL_SKILL_ADMIN_ROOT', () => {
|
|
420
|
-
hal.load(tmpDir);
|
|
421
|
-
assert.equal(hal.halSkillAdminRoot('email-triage'), path.join(tmpDir, 'email-triage'));
|
|
422
|
-
assert.equal(hal.halSkillConfigDir('email-triage'), path.join(tmpDir, 'email-triage', 'config'));
|
|
423
|
-
assert.equal(hal.halSkillLogDir('email-triage'), path.join(tmpDir, 'email-triage', 'log'));
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// --------------------------------------------------------------------------
|
|
428
|
-
// halSkillConfig (independent per-skill config files)
|
|
429
|
-
// --------------------------------------------------------------------------
|
|
430
|
-
|
|
431
|
-
describe('halSkillConfig', () => {
|
|
432
|
-
it('read() returns {} when skill config file is missing', () => {
|
|
433
|
-
hal.load(tmpDir);
|
|
434
|
-
const cfg = hal.halSkillConfig.read('missing-skill');
|
|
435
|
-
assert.deepStrictEqual(cfg, {});
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('write() writes the full skill config file (atomic)', () => {
|
|
439
|
-
hal.load(tmpDir);
|
|
440
|
-
hal.halSkillConfig.write('email-triage', { a: 1, b: 'x' });
|
|
441
|
-
|
|
442
|
-
const fp = path.join(tmpDir, 'email-triage', 'config', 'email-triage.json');
|
|
443
|
-
const onDisk = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
444
|
-
assert.deepStrictEqual(onDisk, { a: 1, b: 'x' });
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('setSetting() creates/updates a single key', () => {
|
|
448
|
-
hal.load(tmpDir);
|
|
449
|
-
hal.halSkillConfig.setSetting('email-triage', 'gog_client_id', 'new-id');
|
|
450
|
-
assert.equal(hal.halSkillConfig.getSetting('hal-email-triage', 'gog_client_id'), 'new-id');
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('merge() deep-merges plain objects and replaces arrays', () => {
|
|
454
|
-
hal.load(tmpDir);
|
|
455
|
-
hal.halSkillConfig.write('email-triage', { nested: { a: 1 }, arr: [1, 2] });
|
|
456
|
-
const merged = hal.halSkillConfig.merge('email-triage', { nested: { b: 2 }, arr: [9] });
|
|
457
|
-
assert.deepStrictEqual(merged, { nested: { a: 1, b: 2 }, arr: [9] });
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
// isInstalled and isInstalledAsync are not tested here because they
|
|
462
|
-
// probe actual binaries on PATH — tested via integration only.
|
|
463
|
-
|
|
464
|
-
// --------------------------------------------------------------------------
|
|
465
|
-
// Registration
|
|
466
|
-
// --------------------------------------------------------------------------
|
|
467
|
-
|
|
468
|
-
describe('register()', () => {
|
|
469
|
-
it('writes runtime data to skill block and flushes', () => {
|
|
470
|
-
hal.load(tmpDir);
|
|
471
|
-
hal.register('plan-your-day', { workspace: '/data/agents/hal' });
|
|
472
|
-
|
|
473
|
-
const ondisk = readConfig(tmpDir);
|
|
474
|
-
assert.equal(ondisk.skills['plan-your-day'].runtime.workspace,
|
|
475
|
-
'/data/agents/hal');
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it('merges with existing runtime data', () => {
|
|
479
|
-
hal.load(tmpDir);
|
|
480
|
-
hal.register('project-manager', { configFile: '/data/agents/hal/config/pm.json' });
|
|
481
|
-
|
|
482
|
-
const ondisk = readConfig(tmpDir);
|
|
483
|
-
assert.equal(ondisk.skills['project-manager'].runtime.workspace,
|
|
484
|
-
'/data/agents/hal');
|
|
485
|
-
assert.equal(ondisk.skills['project-manager'].runtime.configFile,
|
|
486
|
-
'/data/agents/hal/config/pm.json');
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('creates skill entry if not present', () => {
|
|
490
|
-
hal.load(tmpDir);
|
|
491
|
-
hal.register('new-skill', { workspace: '/tmp/new' });
|
|
492
|
-
|
|
493
|
-
const ondisk = readConfig(tmpDir);
|
|
494
|
-
assert.equal(ondisk.skills['new-skill'].runtime.workspace, '/tmp/new');
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
describe('writeAccounts()', () => {
|
|
499
|
-
it('appends new accounts', () => {
|
|
500
|
-
hal.load(tmpDir);
|
|
501
|
-
hal.writeAccounts([
|
|
502
|
-
{ label: 'imap@example.com', provider: 'imap', email: 'imap@example.com' },
|
|
503
|
-
]);
|
|
504
|
-
|
|
505
|
-
const ondisk = readConfig(tmpDir);
|
|
506
|
-
assert.equal(ondisk['hal-communication-accounts'].length, 3);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it('replaces existing account by label', () => {
|
|
510
|
-
hal.load(tmpDir);
|
|
511
|
-
hal.writeAccounts([
|
|
512
|
-
{ label: 'me@gmail.com', provider: 'google', email: 'me@gmail.com',
|
|
513
|
-
scopes: ['email'] },
|
|
514
|
-
]);
|
|
515
|
-
|
|
516
|
-
const ondisk = readConfig(tmpDir);
|
|
517
|
-
assert.equal(ondisk['hal-communication-accounts'].length, 2);
|
|
518
|
-
const google = ondisk['hal-communication-accounts']
|
|
519
|
-
.find(a => a.label === 'me@gmail.com');
|
|
520
|
-
assert.deepStrictEqual(google.scopes, ['email']);
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it('does not remove unmentioned accounts', () => {
|
|
524
|
-
hal.load(tmpDir);
|
|
525
|
-
hal.writeAccounts([
|
|
526
|
-
{ label: 'new@example.com', provider: 'imap', email: 'new@example.com' },
|
|
527
|
-
]);
|
|
528
|
-
|
|
529
|
-
const ondisk = readConfig(tmpDir);
|
|
530
|
-
const labels = ondisk['hal-communication-accounts'].map(a => a.label);
|
|
531
|
-
assert.ok(labels.includes('me@gmail.com'));
|
|
532
|
-
assert.ok(labels.includes('work@company.com'));
|
|
533
|
-
assert.ok(labels.includes('new@example.com'));
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
describe('writeSkillConfig()', () => {
|
|
538
|
-
it('merges into skill block', () => {
|
|
539
|
-
hal.load(tmpDir);
|
|
540
|
-
hal.writeSkillConfig('plan-your-day', {
|
|
541
|
-
recentDays: 3,
|
|
542
|
-
primaryVault: 'Jeremy',
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const ondisk = readConfig(tmpDir);
|
|
546
|
-
assert.equal(ondisk.skills['plan-your-day'].recentDays, 3);
|
|
547
|
-
assert.equal(ondisk.skills['plan-your-day'].primaryVault, 'Jeremy');
|
|
548
|
-
// existing keys preserved
|
|
549
|
-
assert.equal(ondisk.skills['plan-your-day'].enabled, true);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
describe('writeSharedSettings()', () => {
|
|
554
|
-
it('writes top-level keys', () => {
|
|
555
|
-
hal.load(tmpDir);
|
|
556
|
-
hal.writeSharedSettings({
|
|
557
|
-
'hal-timezone': 'US/Pacific',
|
|
558
|
-
'hal-daily-note-filename': 'custom.md',
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
const ondisk = readConfig(tmpDir);
|
|
562
|
-
assert.equal(ondisk['hal-timezone'], 'US/Pacific');
|
|
563
|
-
assert.equal(ondisk['hal-daily-note-filename'], 'custom.md');
|
|
564
|
-
// skills untouched
|
|
565
|
-
assert.ok(ondisk.skills['project-manager']);
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// --------------------------------------------------------------------------
|
|
570
|
-
// index.js re-export
|
|
571
|
-
// --------------------------------------------------------------------------
|
|
572
|
-
|
|
573
|
-
describe('index.js', () => {
|
|
574
|
-
it('re-exports lib/config', async () => {
|
|
575
|
-
const idx = await import('../index.js');
|
|
576
|
-
assert.equal(typeof idx.load, 'function');
|
|
577
|
-
assert.equal(typeof idx.vaults, 'function');
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
it('default export has all functions', async () => {
|
|
581
|
-
const idx = await import('../index.js');
|
|
582
|
-
assert.equal(typeof idx.default.load, 'function');
|
|
583
|
-
assert.equal(typeof idx.default.vaults, 'function');
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
});
|