@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/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
+ }