@jamaynor/hal-config 1.0.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/CLAUDE.md +84 -0
- package/index.js +3 -0
- package/lib/config.js +675 -0
- package/package.json +23 -0
- package/publish.ps1 +30 -0
- package/security/access-control.js +308 -0
- package/security/governor.js +313 -0
- package/security/index.js +31 -0
- package/security/redactor.js +129 -0
- package/security/sanitizer.js +571 -0
- package/test/config-io.test.js +326 -0
- package/test/security.test.js +488 -0
- package/test/test-utils.test.js +360 -0
- package/test/test.js +586 -0
- package/test-utils.js +255 -0
package/test-utils.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-utils
|
|
3
|
+
*
|
|
4
|
+
* Responsibility: Reusable test helpers for HAL skill test suites — temp dirs,
|
|
5
|
+
* file scaffolding, output capture, and process.exit mocking.
|
|
6
|
+
*
|
|
7
|
+
* Public Interface:
|
|
8
|
+
* test-utils
|
|
9
|
+
* ├── makeTmpDir(prefix?: string): string
|
|
10
|
+
* ├── makeTmpDirAsync(prefix?: string): Promise<string>
|
|
11
|
+
* ├── cleanup(dir: string): void
|
|
12
|
+
* ├── cleanupAsync(dir: string): Promise<void>
|
|
13
|
+
* ├── scaffold(baseDir: string, { dirs?, files? }): void
|
|
14
|
+
* ├── captureLog(): { lines: string[], restore: () => void }
|
|
15
|
+
* ├── captureStderr(): { lines: string[], restore: () => void }
|
|
16
|
+
* ├── captureWarn(): { lines: string[], restore: () => void }
|
|
17
|
+
* ├── captureOutput(): { stdout: string[], stderr: string[], raw: Array, restore: () => void }
|
|
18
|
+
* ├── silent(fn: Function): any
|
|
19
|
+
* ├── withCapturedLog(fn: Function): string[]
|
|
20
|
+
* ├── writeJson(filePath: string, data: any): void
|
|
21
|
+
* └── mockProcessExit(): { code: number|undefined, restore: () => void }
|
|
22
|
+
*
|
|
23
|
+
* Intentionally NOT extracted (grouped by reason):
|
|
24
|
+
*
|
|
25
|
+
* Skill-specific mock builders (22 — tightly coupled to their module under test):
|
|
26
|
+
* email-triage: buildMockDetect, buildNoopPromptMod, buildNoAnswerPromptMod,
|
|
27
|
+
* buildMockSystemCfg, buildNullContactResolver, buildNoopStarterRules,
|
|
28
|
+
* buildReadyImapCtor, buildErrorImapCtor, runWizardWithDeps,
|
|
29
|
+
* mockClient, mockClientWithError, mockSanitize, mockScan,
|
|
30
|
+
* mockScanAllow, mockScanBlock, blockResult, reviewResult, allowResult,
|
|
31
|
+
* buildMockIo, buildCleanIo, makeMockFs, makeEmptyFs,
|
|
32
|
+
* cleanGate, blockedGate, successFn, freshFn, memFs,
|
|
33
|
+
* makeRunLogWithMockFs, makeMemFs, makeEntry, makeSendNotification,
|
|
34
|
+
* makeCmdNoise
|
|
35
|
+
* project-mgr: makeMockSpawn
|
|
36
|
+
*
|
|
37
|
+
* Skill-specific utility functions (10 — domain logic, not test infrastructure):
|
|
38
|
+
* plan-your-day: dateOffset, buildNoteWithTasks, makeVault, writeConfig
|
|
39
|
+
* email-triage: normPath, sha256, fakeBaseline, writeSkillConfig, makeTempDirs
|
|
40
|
+
* secret-mgr: run, secrets, wrapper, cleanup (Bitwarden secret cleanup)
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import fsp from 'node:fs/promises';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
import os from 'node:os';
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Task 1.4 — Temp dirs, cleanup, scaffold
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export function makeTmpDir(prefix) {
|
|
53
|
+
const p = prefix !== undefined ? prefix : 'hal-test-';
|
|
54
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), p));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function makeTmpDirAsync(prefix) {
|
|
58
|
+
const p = prefix !== undefined ? prefix : 'hal-test-';
|
|
59
|
+
return fsp.mkdtemp(path.join(os.tmpdir(), p));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cleanup(dir) {
|
|
63
|
+
if (!dir) return;
|
|
64
|
+
if (!fs.existsSync(dir)) return;
|
|
65
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function cleanupAsync(dir) {
|
|
69
|
+
if (!dir) return;
|
|
70
|
+
try {
|
|
71
|
+
await fsp.access(dir);
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await fsp.rm(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function scaffold(baseDir, { dirs, files } = {}) {
|
|
79
|
+
if (dirs) {
|
|
80
|
+
for (const d of dirs) {
|
|
81
|
+
fs.mkdirSync(path.join(baseDir, d), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (files) {
|
|
85
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
86
|
+
const fullPath = path.join(baseDir, relPath);
|
|
87
|
+
const dirname = path.dirname(fullPath);
|
|
88
|
+
fs.mkdirSync(dirname, { recursive: true });
|
|
89
|
+
const data = typeof content === 'string'
|
|
90
|
+
? content
|
|
91
|
+
: JSON.stringify(content, null, 2);
|
|
92
|
+
fs.writeFileSync(fullPath, data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Task 1.5 — Output capture helpers
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
export function captureLog() {
|
|
102
|
+
const lines = [];
|
|
103
|
+
const original = console.log;
|
|
104
|
+
let restored = false;
|
|
105
|
+
console.log = (...args) => { lines.push(args.join(' ')); };
|
|
106
|
+
function restore() {
|
|
107
|
+
if (restored) return;
|
|
108
|
+
restored = true;
|
|
109
|
+
console.log = original;
|
|
110
|
+
}
|
|
111
|
+
return { lines, restore };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function captureStderr() {
|
|
115
|
+
const lines = [];
|
|
116
|
+
const original = console.error;
|
|
117
|
+
let restored = false;
|
|
118
|
+
console.error = (...args) => { lines.push(args.join(' ')); };
|
|
119
|
+
function restore() {
|
|
120
|
+
if (restored) return;
|
|
121
|
+
restored = true;
|
|
122
|
+
console.error = original;
|
|
123
|
+
}
|
|
124
|
+
return { lines, restore };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function captureWarn() {
|
|
128
|
+
const lines = [];
|
|
129
|
+
const original = console.warn;
|
|
130
|
+
let restored = false;
|
|
131
|
+
console.warn = (...args) => { lines.push(args.join(' ')); };
|
|
132
|
+
function restore() {
|
|
133
|
+
if (restored) return;
|
|
134
|
+
restored = true;
|
|
135
|
+
console.warn = original;
|
|
136
|
+
}
|
|
137
|
+
return { lines, restore };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function captureOutput() {
|
|
141
|
+
const stdout = [];
|
|
142
|
+
const stderr = [];
|
|
143
|
+
const raw = [];
|
|
144
|
+
|
|
145
|
+
const origLog = console.log;
|
|
146
|
+
const origError = console.error;
|
|
147
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
148
|
+
|
|
149
|
+
let restored = false;
|
|
150
|
+
|
|
151
|
+
console.log = (...args) => { stdout.push(args.join(' ')); };
|
|
152
|
+
console.error = (...args) => { stderr.push(args.join(' ')); };
|
|
153
|
+
process.stdout.write = (chunk, ...rest) => {
|
|
154
|
+
raw.push(chunk);
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function restore() {
|
|
159
|
+
if (restored) return;
|
|
160
|
+
restored = true;
|
|
161
|
+
console.log = origLog;
|
|
162
|
+
console.error = origError;
|
|
163
|
+
process.stdout.write = origWrite;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { stdout, stderr, raw, restore };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function silent(fn) {
|
|
170
|
+
const noop = () => {};
|
|
171
|
+
const origLog = console.log;
|
|
172
|
+
const origError = console.error;
|
|
173
|
+
const origWarn = console.warn;
|
|
174
|
+
console.log = noop;
|
|
175
|
+
console.error = noop;
|
|
176
|
+
console.warn = noop;
|
|
177
|
+
|
|
178
|
+
function restore() {
|
|
179
|
+
console.log = origLog;
|
|
180
|
+
console.error = origError;
|
|
181
|
+
console.warn = origWarn;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let result;
|
|
185
|
+
try {
|
|
186
|
+
result = fn();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
restore();
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (result && typeof result.then === 'function') {
|
|
193
|
+
return result.then(
|
|
194
|
+
(val) => { restore(); return val; },
|
|
195
|
+
(err) => { restore(); throw err; }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
restore();
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function withCapturedLog(fn) {
|
|
204
|
+
const cap = captureLog();
|
|
205
|
+
let result;
|
|
206
|
+
try {
|
|
207
|
+
result = fn();
|
|
208
|
+
} catch (err) {
|
|
209
|
+
cap.restore();
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (result && typeof result.then === 'function') {
|
|
214
|
+
return result.then(
|
|
215
|
+
() => { cap.restore(); return cap.lines; },
|
|
216
|
+
(err) => { cap.restore(); throw err; }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cap.restore();
|
|
221
|
+
return cap.lines;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// Task 1.6 — writeJson and mockProcessExit
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
export function writeJson(filePath, data) {
|
|
229
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
230
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function mockProcessExit() {
|
|
234
|
+
const original = process.exit;
|
|
235
|
+
const holder = { code: undefined };
|
|
236
|
+
let restored = false;
|
|
237
|
+
|
|
238
|
+
process.exit = (exitCode) => {
|
|
239
|
+
holder.code = exitCode;
|
|
240
|
+
const err = new Error('process.exit called');
|
|
241
|
+
err.exitCode = exitCode;
|
|
242
|
+
throw err;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function restore() {
|
|
246
|
+
if (restored) return;
|
|
247
|
+
restored = true;
|
|
248
|
+
process.exit = original;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
get code() { return holder.code; },
|
|
253
|
+
restore,
|
|
254
|
+
};
|
|
255
|
+
}
|