@nforma.ai/nforma 0.28.0 → 0.29.0

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.
@@ -1,354 +0,0 @@
1
- #!/usr/bin/env node
2
- // Test suite for hooks/nf-session-start.js
3
- // Uses Node.js built-in test runner: node --test hooks/nf-session-start.test.js
4
- //
5
- // All tests spawn the hook as a child process with a mock stdin JSON payload.
6
- // The hook has no exports — only subprocess integration tests are possible.
7
- // Timeout is 8000ms to account for async operations (bootstrap secrets sync).
8
-
9
- 'use strict';
10
-
11
- const { test } = require('node:test');
12
- const assert = require('node:assert/strict');
13
- const { spawnSync } = require('child_process');
14
- const fs = require('fs');
15
- const os = require('os');
16
- const path = require('path');
17
-
18
- const HOOK_PATH = path.join(__dirname, 'nf-session-start.js');
19
-
20
- // ─── Helpers ────────────────────────────────────────────────────────────────
21
-
22
- function makeTmpDir() {
23
- const dir = path.join(
24
- os.tmpdir(),
25
- 'nf-ss-' + Date.now() + '-' + Math.random().toString(36).slice(2)
26
- );
27
- fs.mkdirSync(dir, { recursive: true });
28
- return dir;
29
- }
30
-
31
- function runHook(stdinPayload, opts = {}) {
32
- const input = stdinPayload === null ? '' : JSON.stringify(stdinPayload);
33
- const result = spawnSync('node', [HOOK_PATH], {
34
- input,
35
- encoding: 'utf8',
36
- timeout: 8000,
37
- ...opts,
38
- });
39
- return {
40
- stdout: result.stdout || '',
41
- stderr: result.stderr || '',
42
- exitCode: result.status,
43
- parsed: (() => { try { return JSON.parse(result.stdout); } catch { return null; } })(),
44
- };
45
- }
46
-
47
- // Write a minimal package.json to the given dir.
48
- function writePackageJson(dir, name) {
49
- fs.writeFileSync(
50
- path.join(dir, 'package.json'),
51
- JSON.stringify({ name }, null, 2),
52
- 'utf8'
53
- );
54
- }
55
-
56
- // Write pending-fixes.json under .planning/telemetry/ in the given dir.
57
- function writePendingFixes(dir, issues) {
58
- const telemetryDir = path.join(dir, '.planning', 'telemetry');
59
- fs.mkdirSync(telemetryDir, { recursive: true });
60
- fs.writeFileSync(
61
- path.join(telemetryDir, 'pending-fixes.json'),
62
- JSON.stringify({ issues }, null, 2),
63
- 'utf8'
64
- );
65
- }
66
-
67
- // Read pending-fixes.json back from disk.
68
- function readPendingFixes(dir) {
69
- const fixesPath = path.join(dir, '.planning', 'telemetry', 'pending-fixes.json');
70
- return JSON.parse(fs.readFileSync(fixesPath, 'utf8'));
71
- }
72
-
73
- // ─── Subprocess integration tests ───────────────────────────────────────────
74
-
75
- test('valid empty JSON stdin → exits 0 (secrets not found, silently skips)', () => {
76
- // Pass a cwd that is not a nForma repo so telemetry branch also skips.
77
- const tmpDir = makeTmpDir();
78
- const { exitCode, stderr } = runHook({ cwd: tmpDir });
79
- assert.equal(exitCode, 0, 'hook must exit 0 even when secrets module is absent');
80
- // There should be no hard error in stderr (sync errors are expected-silent here)
81
- // We allow stderr to contain debug notes but must not crash.
82
- assert.ok(
83
- !stderr.includes('TypeError') && !stderr.includes('ReferenceError'),
84
- 'should not throw a JS error on stderr: ' + stderr
85
- );
86
- });
87
-
88
- test('invalid JSON stdin → exits 0 (fail-open)', () => {
89
- const result = spawnSync('node', [HOOK_PATH], {
90
- input: 'not valid json {{{',
91
- encoding: 'utf8',
92
- timeout: 8000,
93
- });
94
- assert.equal(result.status, 0, 'hook must exit 0 on JSON parse failure');
95
- });
96
-
97
- test('empty stdin → exits 0 (fail-open)', () => {
98
- const result = spawnSync('node', [HOOK_PATH], {
99
- input: '',
100
- encoding: 'utf8',
101
- timeout: 8000,
102
- });
103
- assert.equal(result.status, 0, 'hook must exit 0 when stdin is empty');
104
- });
105
-
106
- test('telemetry: unsurfaced issue with priority >= 50 → outputs additionalContext and marks issue surfaced', () => {
107
- const tmpDir = makeTmpDir();
108
- writePackageJson(tmpDir, 'nforma');
109
- writePendingFixes(tmpDir, [
110
- {
111
- id: 'fix-001',
112
- description: 'Quorum scoreboard drift detected in last 3 sessions',
113
- action: 'Run node bin/update-scoreboard.cjs --repair',
114
- priority: 80,
115
- surfaced: false,
116
- },
117
- ]);
118
-
119
- const { exitCode, parsed } = runHook({ cwd: tmpDir });
120
-
121
- assert.equal(exitCode, 0, 'hook must exit 0');
122
- assert.ok(parsed !== null, 'stdout should be valid JSON');
123
- assert.ok(parsed.hookSpecificOutput, 'output must have hookSpecificOutput');
124
- assert.equal(
125
- parsed.hookSpecificOutput.hookEventName,
126
- 'SessionStart',
127
- 'hookEventName must be SessionStart'
128
- );
129
-
130
- const ctx = parsed.hookSpecificOutput.additionalContext;
131
- assert.ok(typeof ctx === 'string' && ctx.length > 0, 'additionalContext must be a non-empty string');
132
- assert.ok(
133
- ctx.includes('Quorum scoreboard drift detected'),
134
- 'additionalContext must include the issue description'
135
- );
136
- assert.ok(
137
- ctx.includes('priority=80'),
138
- 'additionalContext must include the priority'
139
- );
140
- assert.ok(
141
- ctx.includes('node bin/update-scoreboard.cjs --repair'),
142
- 'additionalContext must include the suggested action'
143
- );
144
-
145
- // Verify the file was updated on disk to mark the issue surfaced.
146
- const updated = readPendingFixes(tmpDir);
147
- const issue = updated.issues[0];
148
- assert.equal(issue.surfaced, true, 'issue.surfaced must be set to true after surfacing');
149
- assert.ok(
150
- typeof issue.surfacedAt === 'string' && issue.surfacedAt.length > 0,
151
- 'issue.surfacedAt must be an ISO timestamp string'
152
- );
153
- });
154
-
155
- test('telemetry: issue already surfaced (surfaced=true) → no additionalContext output', () => {
156
- const tmpDir = makeTmpDir();
157
- writePackageJson(tmpDir, 'nforma');
158
- writePendingFixes(tmpDir, [
159
- {
160
- id: 'fix-002',
161
- description: 'Old issue already surfaced',
162
- action: 'Nothing to do',
163
- priority: 90,
164
- surfaced: true,
165
- surfacedAt: '2026-02-01T00:00:00.000Z',
166
- },
167
- ]);
168
-
169
- const { exitCode, stdout } = runHook({ cwd: tmpDir });
170
-
171
- assert.equal(exitCode, 0, 'hook must exit 0');
172
- // When no telemetry issue is surfaced the hook writes nothing to stdout.
173
- assert.equal(stdout.trim(), '', 'stdout must be empty when issue is already surfaced');
174
- });
175
-
176
- test('telemetry: priority below 50 → no additionalContext output', () => {
177
- const tmpDir = makeTmpDir();
178
- writePackageJson(tmpDir, 'nforma');
179
- writePendingFixes(tmpDir, [
180
- {
181
- id: 'fix-003',
182
- description: 'Low priority noise item',
183
- action: 'Ignore for now',
184
- priority: 30,
185
- surfaced: false,
186
- },
187
- ]);
188
-
189
- const { exitCode, stdout } = runHook({ cwd: tmpDir });
190
-
191
- assert.equal(exitCode, 0, 'hook must exit 0');
192
- assert.equal(stdout.trim(), '', 'stdout must be empty when issue priority is below 50');
193
- });
194
-
195
- test('telemetry: priority exactly 50 → outputs additionalContext (boundary value)', () => {
196
- const tmpDir = makeTmpDir();
197
- writePackageJson(tmpDir, 'nforma');
198
- writePendingFixes(tmpDir, [
199
- {
200
- id: 'fix-004',
201
- description: 'Boundary priority issue at exactly 50',
202
- action: 'Check threshold logic',
203
- priority: 50,
204
- surfaced: false,
205
- },
206
- ]);
207
-
208
- const { exitCode, parsed } = runHook({ cwd: tmpDir });
209
-
210
- assert.equal(exitCode, 0, 'hook must exit 0');
211
- assert.ok(parsed !== null, 'stdout should be valid JSON at boundary priority=50');
212
- const ctx = parsed.hookSpecificOutput.additionalContext;
213
- assert.ok(
214
- ctx.includes('Boundary priority issue at exactly 50'),
215
- 'additionalContext must include the boundary issue description'
216
- );
217
- });
218
-
219
- test('non-nForma repo (package.json name != "nforma") → no telemetry output', () => {
220
- const tmpDir = makeTmpDir();
221
- writePackageJson(tmpDir, 'some-other-project');
222
- writePendingFixes(tmpDir, [
223
- {
224
- id: 'fix-005',
225
- description: 'Should never be surfaced in non-nForma repo',
226
- action: 'N/A',
227
- priority: 99,
228
- surfaced: false,
229
- },
230
- ]);
231
-
232
- const { exitCode, stdout } = runHook({ cwd: tmpDir });
233
-
234
- assert.equal(exitCode, 0, 'hook must exit 0');
235
- assert.equal(stdout.trim(), '', 'stdout must be empty for non-nForma repo');
236
-
237
- // Verify the file was NOT modified (issue.surfaced remains false).
238
- const unchanged = readPendingFixes(tmpDir);
239
- assert.equal(
240
- unchanged.issues[0].surfaced,
241
- false,
242
- 'issue.surfaced must remain false when repo is not nForma'
243
- );
244
- });
245
-
246
- test('missing .planning/telemetry/pending-fixes.json → exits 0 silently', () => {
247
- const tmpDir = makeTmpDir();
248
- writePackageJson(tmpDir, 'nforma');
249
- // Do NOT write pending-fixes.json — directory does not even exist.
250
-
251
- const { exitCode, stdout, stderr } = runHook({ cwd: tmpDir });
252
-
253
- assert.equal(exitCode, 0, 'hook must exit 0 when pending-fixes.json is absent');
254
- assert.equal(stdout.trim(), '', 'stdout must be empty when pending-fixes.json is absent');
255
- assert.ok(
256
- !stderr.includes('TypeError') && !stderr.includes('ReferenceError'),
257
- 'no JS error on stderr when telemetry file is absent: ' + stderr
258
- );
259
- });
260
-
261
- test('telemetry: multiple issues, only first unsurfaced high-priority one is surfaced', () => {
262
- const tmpDir = makeTmpDir();
263
- writePackageJson(tmpDir, 'nforma');
264
- writePendingFixes(tmpDir, [
265
- {
266
- id: 'fix-low',
267
- description: 'Low priority skipped item',
268
- action: 'Skip me',
269
- priority: 20,
270
- surfaced: false,
271
- },
272
- {
273
- id: 'fix-surfaced',
274
- description: 'Already surfaced item',
275
- action: 'Already done',
276
- priority: 95,
277
- surfaced: true,
278
- surfacedAt: '2026-02-01T00:00:00.000Z',
279
- },
280
- {
281
- id: 'fix-high',
282
- description: 'High priority unsurfaced item that should be picked',
283
- action: 'Fix the high priority thing',
284
- priority: 75,
285
- surfaced: false,
286
- },
287
- {
288
- id: 'fix-second-high',
289
- description: 'Second high priority item that should NOT be picked this session',
290
- action: 'Fix the second thing',
291
- priority: 70,
292
- surfaced: false,
293
- },
294
- ]);
295
-
296
- const { exitCode, parsed } = runHook({ cwd: tmpDir });
297
-
298
- assert.equal(exitCode, 0, 'hook must exit 0');
299
- assert.ok(parsed !== null, 'stdout should be valid JSON');
300
- const ctx = parsed.hookSpecificOutput.additionalContext;
301
- assert.ok(
302
- ctx.includes('High priority unsurfaced item that should be picked'),
303
- 'additionalContext must include the first eligible unsurfaced issue'
304
- );
305
- assert.ok(
306
- !ctx.includes('Second high priority item that should NOT be picked'),
307
- 'additionalContext must NOT include the second unsurfaced issue'
308
- );
309
-
310
- // Verify only fix-high was marked surfaced on disk.
311
- const updated = readPendingFixes(tmpDir);
312
- const fixHigh = updated.issues.find(i => i.id === 'fix-high');
313
- const fixSecond = updated.issues.find(i => i.id === 'fix-second-high');
314
- assert.equal(fixHigh.surfaced, true, 'fix-high must be marked surfaced');
315
- assert.equal(fixSecond.surfaced, false, 'fix-second-high must remain unsurfaced');
316
- });
317
-
318
- test('cwd field absent in stdin JSON → exits 0 (defaults to process.cwd, no crash)', () => {
319
- // Pass an object with no cwd field. The hook should default to process.cwd()
320
- // and not crash regardless of whether that directory has a nForma package.json.
321
- const { exitCode } = runHook({});
322
- assert.equal(exitCode, 0, 'hook must exit 0 when cwd is absent from stdin');
323
- });
324
-
325
- test('stdout is either empty or valid JSON (never partial/corrupt output)', () => {
326
- const tmpDir = makeTmpDir();
327
- // Repo with a surfaceable issue to exercise the stdout write path.
328
- writePackageJson(tmpDir, 'nforma');
329
- writePendingFixes(tmpDir, [
330
- {
331
- id: 'fix-json-integrity',
332
- description: 'Test JSON output integrity',
333
- action: 'Verify output is parseable',
334
- priority: 60,
335
- surfaced: false,
336
- },
337
- ]);
338
-
339
- const { stdout, exitCode } = runHook({ cwd: tmpDir });
340
-
341
- assert.equal(exitCode, 0);
342
- if (stdout.trim().length > 0) {
343
- let parsed;
344
- try {
345
- parsed = JSON.parse(stdout);
346
- } catch (e) {
347
- assert.fail('stdout is non-empty but not valid JSON: ' + stdout);
348
- }
349
- assert.ok(
350
- parsed && typeof parsed === 'object',
351
- 'parsed stdout must be an object'
352
- );
353
- }
354
- });
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env node
2
- // Test suite for hooks/nf-slot-correlator.js
3
- // Uses Node.js built-in test runner: node --test hooks/nf-slot-correlator.test.js
4
-
5
- 'use strict';
6
-
7
- const { test } = require('node:test');
8
- const assert = require('node:assert/strict');
9
- const { spawnSync } = require('child_process');
10
- const fs = require('fs');
11
- const os = require('os');
12
- const path = require('path');
13
-
14
- const HOOK_PATH = path.join(__dirname, 'nf-slot-correlator.js');
15
-
16
- // Helper: create isolated tmpdir per test
17
- function makeTmpDir() {
18
- return path.join(os.tmpdir(), 'nf-sc-' + Date.now() + '-' + Math.random().toString(36).slice(2));
19
- }
20
-
21
- // Helper: run the hook with a given stdin JSON payload using tmpDir as cwd
22
- function runHook(stdinPayload, tmpDir) {
23
- fs.mkdirSync(tmpDir, { recursive: true });
24
- const result = spawnSync('node', [HOOK_PATH], {
25
- input: JSON.stringify(stdinPayload),
26
- cwd: tmpDir,
27
- encoding: 'utf8',
28
- timeout: 5000,
29
- });
30
- return {
31
- stdout: result.stdout || '',
32
- stderr: result.stderr || '',
33
- exitCode: result.status,
34
- };
35
- }
36
-
37
- test('writes correlation placeholder for nf-quorum-slot-worker', () => {
38
- const tmpDir = makeTmpDir();
39
-
40
- const payload = {
41
- agent_type: 'nf-quorum-slot-worker',
42
- agent_id: 'agent42',
43
- };
44
-
45
- const { exitCode } = runHook(payload, tmpDir);
46
- assert.equal(exitCode, 0);
47
-
48
- const corrFile = path.join(tmpDir, '.planning', 'quorum', 'correlations', 'quorum-slot-corr-agent42.json');
49
- assert.equal(fs.existsSync(corrFile), true, 'Correlation file should exist');
50
-
51
- const data = JSON.parse(fs.readFileSync(corrFile, 'utf8'));
52
- assert.equal(data.agent_id, 'agent42');
53
- assert.equal(data.slot, null, 'slot should be null in SubagentStart (prompt not available)');
54
- assert.ok(data.ts, 'ts field should be present');
55
- });
56
-
57
- test('non-nf agent type: exits 0, no file written', () => {
58
- const tmpDir = makeTmpDir();
59
-
60
- const payload = {
61
- agent_type: 'other',
62
- agent_id: 'agent99',
63
- };
64
-
65
- const { exitCode } = runHook(payload, tmpDir);
66
- assert.equal(exitCode, 0);
67
-
68
- const planningDir = path.join(tmpDir, '.planning');
69
- // No .planning dir should be created at all
70
- const anyFile = fs.existsSync(planningDir);
71
- assert.equal(anyFile, false, 'No files should be written for non-nf agents');
72
- });
73
-
74
- test('missing agent_id: exits 0 gracefully', () => {
75
- const tmpDir = makeTmpDir();
76
-
77
- const payload = {
78
- agent_type: 'nf-quorum-slot-worker',
79
- agent_id: null,
80
- };
81
-
82
- const { exitCode } = runHook(payload, tmpDir);
83
- assert.equal(exitCode, 0);
84
- // No crash — exits gracefully when agent_id is null
85
- });
@@ -1,73 +0,0 @@
1
- 'use strict';
2
- const { test } = require('node:test');
3
- const assert = require('node:assert');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
-
7
- // Wave 0 RED stubs for LOOP-02: hooks/nf-spec-regen.js
8
- // These tests define the contract. They will fail until Plan 03 implements the hook.
9
-
10
- function runHook(inputPayload) {
11
- const hookPath = path.join(__dirname, 'nf-spec-regen.js');
12
- const result = spawnSync(process.execPath, [hookPath], {
13
- input: JSON.stringify(inputPayload),
14
- encoding: 'utf8',
15
- timeout: 15000,
16
- });
17
- return result;
18
- }
19
-
20
- test('LOOP-02: nf-spec-regen.js exits 0 and is a no-op for non-Write tool events', () => {
21
- const result = runHook({
22
- tool_name: 'Read',
23
- tool_input: { file_path: '/some/file.ts' },
24
- tool_response: {},
25
- cwd: process.cwd(),
26
- context_window: {}
27
- });
28
- // RED: script does not exist yet
29
- assert.strictEqual(result.status, 0, 'LOOP-02: hook must exit 0 for non-Write tools. Not yet implemented.');
30
- });
31
-
32
- test('LOOP-02: nf-spec-regen.js exits 0 and is a no-op for Write to non-machine file', () => {
33
- const result = runHook({
34
- tool_name: 'Write',
35
- tool_input: { file_path: '/some/other-file.ts' },
36
- tool_response: {},
37
- cwd: process.cwd(),
38
- context_window: {}
39
- });
40
- assert.strictEqual(result.status, 0, 'LOOP-02: hook must exit 0 for Write to non-machine file. Not yet implemented.');
41
- // Should produce no output or empty additionalContext for non-matching files
42
- });
43
-
44
- test('LOOP-02: nf-spec-regen.js exits 0 for Write to nf-workflow.machine.ts and returns additionalContext', () => {
45
- const result = runHook({
46
- tool_name: 'Write',
47
- tool_input: { file_path: '/Users/jonathanborduas/code/QGSD/src/machines/nf-workflow.machine.ts' },
48
- tool_response: {},
49
- cwd: '/Users/jonathanborduas/code/QGSD',
50
- context_window: {}
51
- });
52
- assert.strictEqual(result.status, 0, 'LOOP-02: hook must exit 0 for machine file write. Not yet implemented.');
53
- // Output must be valid JSON with additionalContext field
54
- if (result.stdout && result.stdout.trim()) {
55
- let parsed;
56
- assert.doesNotThrow(() => { parsed = JSON.parse(result.stdout); },
57
- 'LOOP-02: stdout must be valid JSON when hook fires. Not yet implemented.');
58
- assert.ok(
59
- parsed && parsed.hookSpecificOutput && parsed.hookSpecificOutput.additionalContext,
60
- 'LOOP-02: output must contain hookSpecificOutput.additionalContext. Not yet implemented.'
61
- );
62
- }
63
- });
64
-
65
- test('LOOP-02: nf-spec-regen.js exits 0 (fail-open) on malformed stdin JSON', () => {
66
- const hookPath = path.join(__dirname, 'nf-spec-regen.js');
67
- const result = spawnSync(process.execPath, [hookPath], {
68
- input: 'not-valid-json',
69
- encoding: 'utf8',
70
- timeout: 10000,
71
- });
72
- assert.strictEqual(result.status, 0, 'LOOP-02: hook must exit 0 on malformed JSON (fail-open). Not yet implemented.');
73
- });
@@ -1,157 +0,0 @@
1
- #!/usr/bin/env node
2
- // Test suite for hooks/nf-statusline.js
3
- // Uses Node.js built-in test runner: node --test hooks/nf-statusline.test.js
4
- //
5
- // Each test spawns the hook as a child process with mock stdin (JSON payload).
6
- // Captures stdout + exit code. The hook reads JSON from stdin and writes
7
- // formatted statusline text to stdout.
8
-
9
- const { test } = require('node:test');
10
- const assert = require('node:assert/strict');
11
- const { spawnSync } = require('child_process');
12
- const fs = require('fs');
13
- const os = require('os');
14
- const path = require('path');
15
-
16
- const HOOK_PATH = path.join(__dirname, 'nf-statusline.js');
17
-
18
- // Helper: run the hook with a given stdin JSON payload and optional extra env vars
19
- function runHook(stdinPayload, extraEnv) {
20
- const input = typeof stdinPayload === 'string'
21
- ? stdinPayload
22
- : JSON.stringify(stdinPayload);
23
-
24
- const result = spawnSync('node', [HOOK_PATH], {
25
- input,
26
- encoding: 'utf8',
27
- timeout: 5000,
28
- env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
29
- });
30
- return {
31
- stdout: result.stdout || '',
32
- stderr: result.stderr || '',
33
- exitCode: result.status,
34
- };
35
- }
36
-
37
- // Helper: create a temp directory structure, write a file inside it, return tempDir
38
- function makeTempDir(suffix) {
39
- const dir = path.join(os.tmpdir(), `nf-sl-test-${Date.now()}-${suffix}`);
40
- fs.mkdirSync(dir, { recursive: true });
41
- return dir;
42
- }
43
-
44
- // --- Test Cases ---
45
-
46
- // TC1: Minimal payload — stdout contains model name and directory basename
47
- test('TC1: minimal payload includes model name and directory name', () => {
48
- const { stdout, exitCode } = runHook({
49
- model: { display_name: 'TestModel' },
50
- workspace: { current_dir: '/tmp/myproject' },
51
- });
52
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
53
- assert.ok(stdout.includes('TestModel'), 'stdout must include model name "TestModel"');
54
- assert.ok(stdout.includes('myproject'), 'stdout must include directory basename "myproject"');
55
- });
56
-
57
- // TC2: Context at 100% remaining (0% used) → green bar, 0%
58
- // rawUsed = 100 - 100 = 0; scaled = round(0 / 80 * 100) = 0; filled = 0 → all empty blocks
59
- test('TC2: context at 100% remaining shows all-empty bar at 0%', () => {
60
- const { stdout, exitCode } = runHook({
61
- model: { display_name: 'M' },
62
- context_window: { remaining_percentage: 100 },
63
- });
64
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
65
- assert.ok(stdout.includes('░░░░░░░░░░'), 'stdout must include all-empty bar (0% used)');
66
- assert.ok(stdout.includes('0%'), 'stdout must include 0%');
67
- });
68
-
69
- // TC3: Context at 20% remaining (80% used → scaled 100%) → full bar, 100%
70
- // rawUsed = 80; scaled = round(80/80 * 100) = 100; filled = 10 → full blocks
71
- // At 100% scaled, the hook uses the skull emoji and blink+red ANSI code
72
- test('TC3: context at 20% remaining shows full bar at 100% (skull zone)', () => {
73
- const { stdout, exitCode } = runHook({
74
- model: { display_name: 'M' },
75
- context_window: { remaining_percentage: 20 },
76
- });
77
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
78
- assert.ok(stdout.includes('100%'), 'stdout must include 100%');
79
- assert.ok(stdout.includes('██████████'), 'stdout must include full bar (10 filled segments)');
80
- });
81
-
82
- // TC4: Context at 51% remaining (49% used → scaled 61%) → green zone (scaled < 63)
83
- // rawUsed = 49; scaled = round(49/80 * 100) = round(61.25) = 61; 61 < 63 → green
84
- test('TC4: context at 51% remaining shows 61% in green (below 63% yellow threshold)', () => {
85
- const { stdout, exitCode } = runHook({
86
- model: { display_name: 'M' },
87
- context_window: { remaining_percentage: 51 },
88
- });
89
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
90
- assert.ok(stdout.includes('61%'), 'stdout must include 61%');
91
- assert.ok(stdout.includes('\x1b[32m'), 'stdout must include green ANSI code \\x1b[32m');
92
- });
93
-
94
- // TC5: Context at 36% remaining (64% used → scaled 80%) → yellow zone (63 <= scaled < 81)
95
- // rawUsed = 64; scaled = round(64/80 * 100) = round(80) = 80; 63 <= 80 < 81 → yellow
96
- test('TC5: context at 36% remaining shows 80% in yellow (63–80% yellow zone)', () => {
97
- const { stdout, exitCode } = runHook({
98
- model: { display_name: 'M' },
99
- context_window: { remaining_percentage: 36 },
100
- });
101
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
102
- assert.ok(stdout.includes('80%'), 'stdout must include 80%');
103
- assert.ok(stdout.includes('\x1b[33m'), 'stdout must include yellow ANSI code \\x1b[33m');
104
- });
105
-
106
- // TC6: Malformed JSON input → exits 0, stdout is empty (silent fail)
107
- test('TC6: malformed JSON input exits 0 with empty stdout (silent fail)', () => {
108
- const { stdout, exitCode } = runHook('this is not valid json');
109
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
110
- assert.strictEqual(stdout, '', 'stdout must be empty on malformed JSON input');
111
- });
112
-
113
- // TC7: Update available — output includes '/nf:update'
114
- test('TC7: update available banner shows /nf:update in output', () => {
115
- const tempHome = makeTempDir('tc7');
116
- const cacheDir = path.join(tempHome, '.claude', 'cache');
117
- fs.mkdirSync(cacheDir, { recursive: true });
118
- const cacheFile = path.join(cacheDir, 'nf-update-check.json');
119
- fs.writeFileSync(cacheFile, JSON.stringify({ update_available: true, latest: '1.0.1' }), 'utf8');
120
-
121
- try {
122
- const { stdout, exitCode } = runHook(
123
- { model: { display_name: 'M' } },
124
- { HOME: tempHome }
125
- );
126
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
127
- assert.ok(stdout.includes('/nf:update'), 'stdout must include /nf:update when update is available');
128
- } finally {
129
- fs.rmSync(tempHome, { recursive: true, force: true });
130
- }
131
- });
132
-
133
- // TC8: Task in progress — output includes the task's activeForm text
134
- test('TC8: in-progress task is shown in statusline output', () => {
135
- const tempHome = makeTempDir('tc8');
136
- const todosDir = path.join(tempHome, '.claude', 'todos');
137
- fs.mkdirSync(todosDir, { recursive: true });
138
-
139
- const sessionId = 'sess123';
140
- const todosFile = path.join(todosDir, `${sessionId}-agent-0.json`);
141
- fs.writeFileSync(
142
- todosFile,
143
- JSON.stringify([{ status: 'in_progress', activeForm: 'Fix the thing' }]),
144
- 'utf8'
145
- );
146
-
147
- try {
148
- const { stdout, exitCode } = runHook(
149
- { model: { display_name: 'M' }, session_id: sessionId },
150
- { HOME: tempHome }
151
- );
152
- assert.strictEqual(exitCode, 0, 'exit code must be 0');
153
- assert.ok(stdout.includes('Fix the thing'), 'stdout must include the in-progress task activeForm text');
154
- } finally {
155
- fs.rmSync(tempHome, { recursive: true, force: true });
156
- }
157
- });