@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.
- package/bin/quorum-preflight.cjs +89 -0
- package/commands/nf/quorum.md +3 -53
- package/package.json +4 -2
- package/hooks/dist/nf-circuit-breaker.test.js +0 -1002
- package/hooks/dist/nf-precompact.test.js +0 -227
- package/hooks/dist/nf-prompt.test.js +0 -698
- package/hooks/dist/nf-session-start.test.js +0 -354
- package/hooks/dist/nf-slot-correlator.test.js +0 -85
- package/hooks/dist/nf-spec-regen.test.js +0 -73
- package/hooks/dist/nf-statusline.test.js +0 -157
- package/hooks/dist/nf-stop.test.js +0 -1388
- package/hooks/dist/nf-token-collector.test.js +0 -262
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Test suite for hooks/nf-precompact.js
|
|
3
|
-
// Uses Node.js built-in test runner: node --test hooks/nf-precompact.test.js
|
|
4
|
-
//
|
|
5
|
-
// Unit tests target exported helpers (extractCurrentPosition, readPendingTasks).
|
|
6
|
-
// Integration tests spawn the hook as a child process with mock stdin.
|
|
7
|
-
|
|
8
|
-
'use strict';
|
|
9
|
-
|
|
10
|
-
const { test } = require('node:test');
|
|
11
|
-
const assert = require('node:assert/strict');
|
|
12
|
-
const { spawnSync } = require('child_process');
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
|
|
17
|
-
const HOOK_PATH = path.join(__dirname, 'nf-precompact.js');
|
|
18
|
-
const { extractCurrentPosition, readPendingTasks } = require(HOOK_PATH);
|
|
19
|
-
|
|
20
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
function makeTmpDir() {
|
|
23
|
-
const dir = path.join(os.tmpdir(), 'nf-pc-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
|
24
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
-
return dir;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function runHook(stdinPayload, opts = {}) {
|
|
29
|
-
const result = spawnSync('node', [HOOK_PATH], {
|
|
30
|
-
input: JSON.stringify(stdinPayload),
|
|
31
|
-
encoding: 'utf8',
|
|
32
|
-
timeout: 5000,
|
|
33
|
-
...opts,
|
|
34
|
-
});
|
|
35
|
-
return {
|
|
36
|
-
stdout: result.stdout || '',
|
|
37
|
-
stderr: result.stderr || '',
|
|
38
|
-
exitCode: result.status,
|
|
39
|
-
parsed: (() => { try { return JSON.parse(result.stdout); } catch { return null; } })(),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeStateFile(dir, content) {
|
|
44
|
-
const planningDir = path.join(dir, '.planning');
|
|
45
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
46
|
-
fs.writeFileSync(path.join(planningDir, 'STATE.md'), content, 'utf8');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function writeClaudeFile(dir, filename, content) {
|
|
50
|
-
const claudeDir = path.join(dir, '.claude');
|
|
51
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
52
|
-
fs.writeFileSync(path.join(claudeDir, filename), content, 'utf8');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─── extractCurrentPosition unit tests ──────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
test('extractCurrentPosition: returns section between marker and next header', () => {
|
|
58
|
-
const content = [
|
|
59
|
-
'# Project State',
|
|
60
|
-
'',
|
|
61
|
-
'## Current Position',
|
|
62
|
-
'',
|
|
63
|
-
'Phase: v0.19-04 — COMPLETE',
|
|
64
|
-
'Status: ready to plan v0.19-05',
|
|
65
|
-
'',
|
|
66
|
-
'## Performance Metrics',
|
|
67
|
-
'',
|
|
68
|
-
'some other section',
|
|
69
|
-
].join('\n');
|
|
70
|
-
|
|
71
|
-
const result = extractCurrentPosition(content);
|
|
72
|
-
assert.ok(result.includes('Phase: v0.19-04'), 'Should include phase line');
|
|
73
|
-
assert.ok(!result.includes('Performance Metrics'), 'Should not bleed into next section');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('extractCurrentPosition: returns to EOF when no following header', () => {
|
|
77
|
-
const content = [
|
|
78
|
-
'## Other Section',
|
|
79
|
-
'irrelevant',
|
|
80
|
-
'',
|
|
81
|
-
'## Current Position',
|
|
82
|
-
'',
|
|
83
|
-
'Last section content here',
|
|
84
|
-
].join('\n');
|
|
85
|
-
|
|
86
|
-
const result = extractCurrentPosition(content);
|
|
87
|
-
assert.ok(result.includes('Last section content here'));
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('extractCurrentPosition: returns null when marker is absent', () => {
|
|
91
|
-
const content = '## Something Else\ncontent\n## Another\nmore content';
|
|
92
|
-
assert.equal(extractCurrentPosition(content), null);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('extractCurrentPosition: returns null when section is empty', () => {
|
|
96
|
-
const content = '## Current Position\n\n## Next Section\ncontent';
|
|
97
|
-
assert.equal(extractCurrentPosition(content), null);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ─── readPendingTasks unit tests ─────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
test('readPendingTasks: returns empty array when .claude dir does not exist', () => {
|
|
103
|
-
const tmpDir = makeTmpDir();
|
|
104
|
-
const results = readPendingTasks(tmpDir);
|
|
105
|
-
assert.deepEqual(results, []);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('readPendingTasks: returns generic pending-task.txt', () => {
|
|
109
|
-
const tmpDir = makeTmpDir();
|
|
110
|
-
writeClaudeFile(tmpDir, 'pending-task.txt', '/qgsd:execute-phase v0.19-05');
|
|
111
|
-
|
|
112
|
-
const results = readPendingTasks(tmpDir);
|
|
113
|
-
assert.equal(results.length, 1);
|
|
114
|
-
assert.equal(results[0].filename, 'pending-task.txt');
|
|
115
|
-
assert.equal(results[0].content, '/qgsd:execute-phase v0.19-05');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('readPendingTasks: returns session-scoped pending-task-SESSION.txt', () => {
|
|
119
|
-
const tmpDir = makeTmpDir();
|
|
120
|
-
writeClaudeFile(tmpDir, 'pending-task-abc123.txt', '/qgsd:quick --full fix tests');
|
|
121
|
-
|
|
122
|
-
const results = readPendingTasks(tmpDir);
|
|
123
|
-
assert.equal(results.length, 1);
|
|
124
|
-
assert.equal(results[0].filename, 'pending-task-abc123.txt');
|
|
125
|
-
assert.equal(results[0].content, '/qgsd:quick --full fix tests');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('readPendingTasks: excludes .claimed files', () => {
|
|
129
|
-
const tmpDir = makeTmpDir();
|
|
130
|
-
writeClaudeFile(tmpDir, 'pending-task-abc123.txt.claimed', 'already consumed');
|
|
131
|
-
|
|
132
|
-
const results = readPendingTasks(tmpDir);
|
|
133
|
-
assert.deepEqual(results, []);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('readPendingTasks: skips empty pending-task files', () => {
|
|
137
|
-
const tmpDir = makeTmpDir();
|
|
138
|
-
writeClaudeFile(tmpDir, 'pending-task.txt', ' \n ');
|
|
139
|
-
|
|
140
|
-
const results = readPendingTasks(tmpDir);
|
|
141
|
-
assert.deepEqual(results, []);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('readPendingTasks: does NOT delete files (non-consuming)', () => {
|
|
145
|
-
const tmpDir = makeTmpDir();
|
|
146
|
-
writeClaudeFile(tmpDir, 'pending-task.txt', 'some task');
|
|
147
|
-
const filePath = path.join(tmpDir, '.claude', 'pending-task.txt');
|
|
148
|
-
|
|
149
|
-
readPendingTasks(tmpDir);
|
|
150
|
-
|
|
151
|
-
assert.ok(fs.existsSync(filePath), 'File should still exist after read (non-consuming)');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// ─── Full subprocess (stdin→stdout) integration tests ───────────────────────
|
|
155
|
-
|
|
156
|
-
test('subprocess: exits 0 and emits additionalContext when STATE.md has Current Position', () => {
|
|
157
|
-
const tmpDir = makeTmpDir();
|
|
158
|
-
writeStateFile(tmpDir, [
|
|
159
|
-
'# Project State',
|
|
160
|
-
'',
|
|
161
|
-
'## Current Position',
|
|
162
|
-
'',
|
|
163
|
-
'Phase: v0.19-05 — in progress',
|
|
164
|
-
'Plan: 01 — DONE',
|
|
165
|
-
'',
|
|
166
|
-
'## Performance Metrics',
|
|
167
|
-
'other stuff',
|
|
168
|
-
].join('\n'));
|
|
169
|
-
|
|
170
|
-
const { exitCode, parsed } = runHook({ cwd: tmpDir });
|
|
171
|
-
|
|
172
|
-
assert.equal(exitCode, 0);
|
|
173
|
-
assert.ok(parsed, 'stdout should be valid JSON');
|
|
174
|
-
assert.ok(parsed.hookSpecificOutput, 'should have hookSpecificOutput');
|
|
175
|
-
assert.equal(parsed.hookSpecificOutput.hookEventName, 'PreCompact');
|
|
176
|
-
const ctx = parsed.hookSpecificOutput.additionalContext;
|
|
177
|
-
assert.ok(ctx.includes('nForma CONTINUATION CONTEXT'), 'should include header');
|
|
178
|
-
assert.ok(ctx.includes('v0.19-05'), 'should include current position content');
|
|
179
|
-
assert.ok(ctx.includes('Resume Instructions'), 'should include resume instructions');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('subprocess: includes pending task when pending-task.txt exists', () => {
|
|
183
|
-
const tmpDir = makeTmpDir();
|
|
184
|
-
writeStateFile(tmpDir, '## Current Position\n\nPhase: v0.19-05\n\n## Other\nstuff');
|
|
185
|
-
writeClaudeFile(tmpDir, 'pending-task.txt', '/qgsd:execute-phase v0.19-05');
|
|
186
|
-
|
|
187
|
-
const { exitCode, parsed } = runHook({ cwd: tmpDir });
|
|
188
|
-
|
|
189
|
-
assert.equal(exitCode, 0);
|
|
190
|
-
const ctx = parsed.hookSpecificOutput.additionalContext;
|
|
191
|
-
assert.ok(ctx.includes('Pending Task'), 'should include Pending Task section');
|
|
192
|
-
assert.ok(ctx.includes('/qgsd:execute-phase v0.19-05'), 'should include task content');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test('subprocess: emits minimal fallback when STATE.md is absent', () => {
|
|
196
|
-
const tmpDir = makeTmpDir(); // no STATE.md written
|
|
197
|
-
|
|
198
|
-
const { exitCode, parsed } = runHook({ cwd: tmpDir });
|
|
199
|
-
|
|
200
|
-
assert.equal(exitCode, 0);
|
|
201
|
-
assert.ok(parsed.hookSpecificOutput.additionalContext.includes('resumed after compaction'));
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('subprocess: fails open on invalid JSON stdin (exit 0)', () => {
|
|
205
|
-
const result = spawnSync('node', [HOOK_PATH], {
|
|
206
|
-
input: 'not valid json {{',
|
|
207
|
-
encoding: 'utf8',
|
|
208
|
-
timeout: 5000,
|
|
209
|
-
});
|
|
210
|
-
assert.equal(result.status, 0, 'should exit 0 on JSON parse error (fail-open)');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('subprocess: falls back to process.cwd() when cwd field is absent', () => {
|
|
214
|
-
// Pass empty object — no cwd field. Hook should default to process.cwd() and not crash.
|
|
215
|
-
const { exitCode, parsed } = runHook({});
|
|
216
|
-
|
|
217
|
-
assert.equal(exitCode, 0);
|
|
218
|
-
assert.ok(parsed || true, 'Should produce valid output or minimal fallback');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('subprocess: hookEventName is PreCompact', () => {
|
|
222
|
-
const tmpDir = makeTmpDir();
|
|
223
|
-
writeStateFile(tmpDir, '## Current Position\n\nsome state\n\n## Next\nmore');
|
|
224
|
-
|
|
225
|
-
const { parsed } = runHook({ cwd: tmpDir });
|
|
226
|
-
assert.equal(parsed.hookSpecificOutput.hookEventName, 'PreCompact');
|
|
227
|
-
});
|