@codename_inc/spectre 3.7.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/LICENSE +21 -0
- package/README.md +411 -0
- package/bin/spectre.js +8 -0
- package/package.json +23 -0
- package/plugins/spectre/.claude-plugin/plugin.json +5 -0
- package/plugins/spectre/agents/analyst.md +122 -0
- package/plugins/spectre/agents/dev.md +70 -0
- package/plugins/spectre/agents/finder.md +105 -0
- package/plugins/spectre/agents/patterns.md +207 -0
- package/plugins/spectre/agents/reviewer.md +128 -0
- package/plugins/spectre/agents/sync.md +151 -0
- package/plugins/spectre/agents/tester.md +209 -0
- package/plugins/spectre/agents/web-research.md +109 -0
- package/plugins/spectre/commands/architecture_review.md +120 -0
- package/plugins/spectre/commands/clean.md +313 -0
- package/plugins/spectre/commands/code_review.md +408 -0
- package/plugins/spectre/commands/create_plan.md +117 -0
- package/plugins/spectre/commands/create_tasks.md +374 -0
- package/plugins/spectre/commands/create_test_guide.md +120 -0
- package/plugins/spectre/commands/evaluate.md +50 -0
- package/plugins/spectre/commands/execute.md +87 -0
- package/plugins/spectre/commands/fix.md +61 -0
- package/plugins/spectre/commands/forget.md +58 -0
- package/plugins/spectre/commands/handoff.md +161 -0
- package/plugins/spectre/commands/kickoff.md +115 -0
- package/plugins/spectre/commands/learn.md +15 -0
- package/plugins/spectre/commands/plan.md +170 -0
- package/plugins/spectre/commands/plan_review.md +33 -0
- package/plugins/spectre/commands/quick_dev.md +101 -0
- package/plugins/spectre/commands/rebase.md +73 -0
- package/plugins/spectre/commands/recall.md +5 -0
- package/plugins/spectre/commands/research.md +159 -0
- package/plugins/spectre/commands/scope.md +119 -0
- package/plugins/spectre/commands/ship.md +172 -0
- package/plugins/spectre/commands/sweep.md +82 -0
- package/plugins/spectre/commands/test.md +380 -0
- package/plugins/spectre/commands/ux_spec.md +91 -0
- package/plugins/spectre/commands/validate.md +343 -0
- package/plugins/spectre/hooks/hooks.json +34 -0
- package/plugins/spectre/hooks/scripts/bootstrap.cjs +99 -0
- package/plugins/spectre/hooks/scripts/handoff-resume.cjs +410 -0
- package/plugins/spectre/hooks/scripts/lib.cjs +83 -0
- package/plugins/spectre/hooks/scripts/load-knowledge.cjs +120 -0
- package/plugins/spectre/hooks/scripts/precompact-warning.cjs +19 -0
- package/plugins/spectre/hooks/scripts/register_learning.cjs +144 -0
- package/plugins/spectre/hooks/scripts/test_bootstrap.cjs +84 -0
- package/plugins/spectre/hooks/scripts/test_handoff-resume.cjs +858 -0
- package/plugins/spectre/hooks/scripts/test_load-knowledge.cjs +285 -0
- package/plugins/spectre/hooks/scripts/test_register-learning.cjs +146 -0
- package/plugins/spectre/skills/spectre-apply/SKILL.md +189 -0
- package/plugins/spectre/skills/spectre-guide/SKILL.md +358 -0
- package/plugins/spectre/skills/spectre-learn/SKILL.md +635 -0
- package/plugins/spectre/skills/spectre-learn/references/recall-template.md +31 -0
- package/plugins/spectre/skills/spectre-tdd/SKILL.md +111 -0
- package/src/config.test.js +134 -0
- package/src/install.test.js +273 -0
- package/src/lib/config.js +516 -0
- package/src/lib/constants.js +60 -0
- package/src/lib/doctor.js +168 -0
- package/src/lib/install.js +482 -0
- package/src/lib/knowledge.js +217 -0
- package/src/lib/paths.js +98 -0
- package/src/lib/project.js +473 -0
- package/src/main.js +150 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for handoff-resume.cjs
|
|
6
|
+
*
|
|
7
|
+
* Run with: node --test plugins/spectre/hooks/scripts/test_handoff-resume.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { describe, it } = require('node:test');
|
|
11
|
+
const assert = require('node:assert/strict');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execFileSync, execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const SCRIPT_PATH = path.join(__dirname, 'handoff-resume.cjs');
|
|
18
|
+
|
|
19
|
+
function createTmpDir() {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'spectre-hr-'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanup(dir) {
|
|
24
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupGitRepo(tmpPath, branch) {
|
|
28
|
+
branch = branch || 'main';
|
|
29
|
+
|
|
30
|
+
execSync(`git init --initial-branch=${branch}`, { cwd: tmpPath, stdio: 'pipe' });
|
|
31
|
+
execSync('git config user.email "test@test.com"', { cwd: tmpPath, stdio: 'pipe' });
|
|
32
|
+
execSync('git config user.name "Test User"', { cwd: tmpPath, stdio: 'pipe' });
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(path.join(tmpPath, 'README.md'), '# Test');
|
|
35
|
+
execSync('git add README.md', { cwd: tmpPath, stdio: 'pipe' });
|
|
36
|
+
execSync('git commit -m "Initial commit"', { cwd: tmpPath, stdio: 'pipe' });
|
|
37
|
+
|
|
38
|
+
const sessionDir = path.join(tmpPath, 'docs', 'tasks', branch, 'session_logs');
|
|
39
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
return sessionDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runHook(cwd, opts) {
|
|
45
|
+
opts = opts || {};
|
|
46
|
+
const env = Object.assign({}, process.env);
|
|
47
|
+
env.CLAUDE_PROJECT_DIR = cwd;
|
|
48
|
+
// Remove plugin root to avoid side effects in tests
|
|
49
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
50
|
+
|
|
51
|
+
if (opts.env) {
|
|
52
|
+
Object.assign(env, opts.env);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const stdout = execFileSync(process.execPath, [SCRIPT_PATH], {
|
|
57
|
+
env,
|
|
58
|
+
cwd,
|
|
59
|
+
timeout: 10000,
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
input: opts.stdin || ''
|
|
62
|
+
});
|
|
63
|
+
return { stdout, exitCode: 0 };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return { stdout: err.stdout || '', exitCode: err.status };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ──────────────────────────────────────────────────────────────────
|
|
70
|
+
// Core tests
|
|
71
|
+
// ──────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
describe('HandoffResume', () => {
|
|
74
|
+
it('no session dir shows welcome banner', () => {
|
|
75
|
+
const tmp = createTmpDir();
|
|
76
|
+
try {
|
|
77
|
+
// No docs/tasks/{branch}/session_logs exists
|
|
78
|
+
const result = runHook(tmp);
|
|
79
|
+
assert.equal(result.exitCode, 0);
|
|
80
|
+
const output = JSON.parse(result.stdout);
|
|
81
|
+
assert.ok(output.systemMessage);
|
|
82
|
+
assert.ok(output.systemMessage.includes('/spectre:scope'));
|
|
83
|
+
assert.ok(output.systemMessage.includes('/spectre:handoff'));
|
|
84
|
+
assert.ok(output.systemMessage.includes('/spectre:forget'));
|
|
85
|
+
} finally {
|
|
86
|
+
cleanup(tmp);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('no handoff files shows welcome banner', () => {
|
|
91
|
+
const tmp = createTmpDir();
|
|
92
|
+
try {
|
|
93
|
+
setupGitRepo(tmp);
|
|
94
|
+
// session_dir exists but is empty
|
|
95
|
+
const result = runHook(tmp);
|
|
96
|
+
assert.equal(result.exitCode, 0);
|
|
97
|
+
const output = JSON.parse(result.stdout);
|
|
98
|
+
assert.ok(output.systemMessage);
|
|
99
|
+
assert.ok(output.systemMessage.includes('/spectre:scope'));
|
|
100
|
+
} finally {
|
|
101
|
+
cleanup(tmp);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('finds latest handoff by timestamp', () => {
|
|
106
|
+
const tmp = createTmpDir();
|
|
107
|
+
try {
|
|
108
|
+
const sessionDir = setupGitRepo(tmp);
|
|
109
|
+
|
|
110
|
+
const oldHandoff = {
|
|
111
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main',
|
|
112
|
+
task_name: 'old-task',
|
|
113
|
+
progress_update: { summary: 'Old summary', accomplished: ['old thing'], next_steps: ['old step'], decisions: [], blockers: [], confidence: 'low', risks: [] },
|
|
114
|
+
beads: { tasks: [] }, context: { last_commit: 'abc123', wip_state: 'clean' }
|
|
115
|
+
};
|
|
116
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(oldHandoff));
|
|
117
|
+
|
|
118
|
+
// Sleep briefly so mtime differs
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
while (Date.now() - start < 50) {} // busy wait for mtime difference
|
|
121
|
+
|
|
122
|
+
const newHandoff = {
|
|
123
|
+
version: '1.0', timestamp: '2024-01-02-120000', branch_name: 'main',
|
|
124
|
+
task_name: 'new-task',
|
|
125
|
+
progress_update: { summary: 'New summary', accomplished: ['new thing'], next_steps: ['new step'], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
126
|
+
beads: { tasks: [] }, context: { last_commit: 'def456', wip_state: 'uncommitted' }
|
|
127
|
+
};
|
|
128
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-02-120000_handoff.json'), JSON.stringify(newHandoff));
|
|
129
|
+
|
|
130
|
+
const result = runHook(tmp);
|
|
131
|
+
assert.equal(result.exitCode, 0);
|
|
132
|
+
const output = JSON.parse(result.stdout);
|
|
133
|
+
|
|
134
|
+
assert.ok(output.systemMessage.includes('new-task'));
|
|
135
|
+
assert.ok(output.hookSpecificOutput.additionalContext.includes('new-task'));
|
|
136
|
+
} finally {
|
|
137
|
+
cleanup(tmp);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('outputs valid hook JSON structure', () => {
|
|
142
|
+
const tmp = createTmpDir();
|
|
143
|
+
try {
|
|
144
|
+
const sessionDir = setupGitRepo(tmp, 'feature-branch');
|
|
145
|
+
|
|
146
|
+
const handoff = {
|
|
147
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'feature-branch',
|
|
148
|
+
task_name: 'Test Task',
|
|
149
|
+
progress_update: {
|
|
150
|
+
summary: 'We did stuff', accomplished: ['thing 1', 'thing 2'], next_steps: ['next 1'],
|
|
151
|
+
decisions: ['decided X'], blockers: [], confidence: 'high', risks: ['risk 1']
|
|
152
|
+
},
|
|
153
|
+
beads: { workspace_label: 'feature-branch', task_count: 0, epic_id: 'none', epic_title: 'No Epic', tasks: [] },
|
|
154
|
+
context: { last_commit: 'abc123', wip_state: 'clean', key_files: ['file1.py'] }
|
|
155
|
+
};
|
|
156
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
157
|
+
|
|
158
|
+
const result = runHook(tmp);
|
|
159
|
+
assert.equal(result.exitCode, 0);
|
|
160
|
+
const output = JSON.parse(result.stdout);
|
|
161
|
+
|
|
162
|
+
assert.ok('systemMessage' in output);
|
|
163
|
+
assert.ok('hookSpecificOutput' in output);
|
|
164
|
+
assert.ok('hookEventName' in output.hookSpecificOutput);
|
|
165
|
+
assert.equal(output.hookSpecificOutput.hookEventName, 'SessionStart');
|
|
166
|
+
assert.ok('additionalContext' in output.hookSpecificOutput);
|
|
167
|
+
} finally {
|
|
168
|
+
cleanup(tmp);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('system message contains task and branch', () => {
|
|
173
|
+
const tmp = createTmpDir();
|
|
174
|
+
try {
|
|
175
|
+
const sessionDir = setupGitRepo(tmp, 'my-feature');
|
|
176
|
+
|
|
177
|
+
const handoff = {
|
|
178
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'my-feature',
|
|
179
|
+
task_name: 'Implement Widget',
|
|
180
|
+
progress_update: { summary: 'Working on widget', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'medium', risks: [] },
|
|
181
|
+
beads: { tasks: [] }, context: {}
|
|
182
|
+
};
|
|
183
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
184
|
+
|
|
185
|
+
const result = runHook(tmp);
|
|
186
|
+
const output = JSON.parse(result.stdout);
|
|
187
|
+
|
|
188
|
+
assert.ok(output.systemMessage.includes('Implement Widget'));
|
|
189
|
+
assert.ok(output.systemMessage.includes('my-feature'));
|
|
190
|
+
} finally {
|
|
191
|
+
cleanup(tmp);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('additional context contains session-context tag', () => {
|
|
196
|
+
const tmp = createTmpDir();
|
|
197
|
+
try {
|
|
198
|
+
const sessionDir = setupGitRepo(tmp);
|
|
199
|
+
|
|
200
|
+
const handoff = {
|
|
201
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test',
|
|
202
|
+
progress_update: { summary: 'Summary here', accomplished: ['done'], next_steps: ['todo'], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
203
|
+
beads: { tasks: [] }, context: {}
|
|
204
|
+
};
|
|
205
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
206
|
+
|
|
207
|
+
const result = runHook(tmp);
|
|
208
|
+
const output = JSON.parse(result.stdout);
|
|
209
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
210
|
+
|
|
211
|
+
assert.ok(ctx.startsWith('<session-context>'));
|
|
212
|
+
assert.ok(ctx.includes('</session-context>'));
|
|
213
|
+
} finally {
|
|
214
|
+
cleanup(tmp);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('includes progress update sections', () => {
|
|
219
|
+
const tmp = createTmpDir();
|
|
220
|
+
try {
|
|
221
|
+
const sessionDir = setupGitRepo(tmp);
|
|
222
|
+
|
|
223
|
+
const handoff = {
|
|
224
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test',
|
|
225
|
+
progress_update: {
|
|
226
|
+
summary: 'We made good progress today',
|
|
227
|
+
accomplished: ['Finished auth', 'Added tests'],
|
|
228
|
+
next_steps: ['Deploy to staging', 'Review PR'],
|
|
229
|
+
decisions: ['Use JWT tokens'],
|
|
230
|
+
blockers: ['Waiting on API keys'],
|
|
231
|
+
confidence: 'medium',
|
|
232
|
+
risks: ['Timeline tight']
|
|
233
|
+
},
|
|
234
|
+
beads: { tasks: [] }, context: { last_commit: 'abc123', wip_state: 'uncommitted' }
|
|
235
|
+
};
|
|
236
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
237
|
+
|
|
238
|
+
const result = runHook(tmp);
|
|
239
|
+
const output = JSON.parse(result.stdout);
|
|
240
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
241
|
+
|
|
242
|
+
assert.ok(ctx.includes('We made good progress today'));
|
|
243
|
+
assert.ok(ctx.includes('Finished auth'));
|
|
244
|
+
assert.ok(ctx.includes('Added tests'));
|
|
245
|
+
assert.ok(ctx.includes('Deploy to staging'));
|
|
246
|
+
assert.ok(ctx.includes('Use JWT tokens'));
|
|
247
|
+
assert.ok(ctx.includes('Waiting on API keys'));
|
|
248
|
+
assert.ok(ctx.toLowerCase().includes('medium') || ctx.includes('Confidence'));
|
|
249
|
+
assert.ok(ctx.includes('Timeline tight'));
|
|
250
|
+
} finally {
|
|
251
|
+
cleanup(tmp);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('handles malformed JSON gracefully', () => {
|
|
256
|
+
const tmp = createTmpDir();
|
|
257
|
+
try {
|
|
258
|
+
const sessionDir = setupGitRepo(tmp);
|
|
259
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), '{ invalid json }');
|
|
260
|
+
|
|
261
|
+
const result = runHook(tmp);
|
|
262
|
+
assert.equal(result.exitCode, 0);
|
|
263
|
+
assert.equal(result.stdout.trim(), '');
|
|
264
|
+
} finally {
|
|
265
|
+
cleanup(tmp);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('ignores archived sessions', () => {
|
|
270
|
+
const tmp = createTmpDir();
|
|
271
|
+
try {
|
|
272
|
+
const sessionDir = setupGitRepo(tmp);
|
|
273
|
+
const archiveDir = path.join(sessionDir, 'archive');
|
|
274
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
// Put handoff in archive only
|
|
277
|
+
const handoff = {
|
|
278
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Archived Task',
|
|
279
|
+
progress_update: { summary: 'Old', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'low', risks: [] },
|
|
280
|
+
beads: { tasks: [] }, context: {}
|
|
281
|
+
};
|
|
282
|
+
fs.writeFileSync(path.join(archiveDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
283
|
+
|
|
284
|
+
const result = runHook(tmp);
|
|
285
|
+
assert.equal(result.exitCode, 0);
|
|
286
|
+
const output = JSON.parse(result.stdout);
|
|
287
|
+
assert.ok(output.systemMessage);
|
|
288
|
+
assert.ok(output.systemMessage.includes('/spectre:scope'));
|
|
289
|
+
} finally {
|
|
290
|
+
cleanup(tmp);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ──────────────────────────────────────────────────────────────────
|
|
296
|
+
// V1.1 Schema Fields
|
|
297
|
+
// ──────────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
describe('V1.1 Schema Fields', () => {
|
|
300
|
+
it('goal field appears in output', () => {
|
|
301
|
+
const tmp = createTmpDir();
|
|
302
|
+
try {
|
|
303
|
+
const sessionDir = setupGitRepo(tmp);
|
|
304
|
+
|
|
305
|
+
const handoff = {
|
|
306
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
307
|
+
progress_update: {
|
|
308
|
+
summary: 'Summary text', goal: 'Ship the authentication feature by EOD',
|
|
309
|
+
accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: []
|
|
310
|
+
},
|
|
311
|
+
beads: { tasks: [] }, context: {}
|
|
312
|
+
};
|
|
313
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
314
|
+
|
|
315
|
+
const result = runHook(tmp);
|
|
316
|
+
const output = JSON.parse(result.stdout);
|
|
317
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
318
|
+
|
|
319
|
+
assert.ok(ctx.includes('Goal'));
|
|
320
|
+
assert.ok(ctx.includes('Ship the authentication feature by EOD'));
|
|
321
|
+
} finally {
|
|
322
|
+
cleanup(tmp);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('now field creates active work section', () => {
|
|
327
|
+
const tmp = createTmpDir();
|
|
328
|
+
try {
|
|
329
|
+
const sessionDir = setupGitRepo(tmp);
|
|
330
|
+
|
|
331
|
+
const handoff = {
|
|
332
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
333
|
+
progress_update: {
|
|
334
|
+
summary: 'Summary text', now: 'Implementing the login form validation',
|
|
335
|
+
accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'medium', risks: []
|
|
336
|
+
},
|
|
337
|
+
beads: { tasks: [] }, context: {}
|
|
338
|
+
};
|
|
339
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
340
|
+
|
|
341
|
+
const result = runHook(tmp);
|
|
342
|
+
const output = JSON.parse(result.stdout);
|
|
343
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
344
|
+
|
|
345
|
+
assert.ok(ctx.includes('Active Work (Resume Here)'));
|
|
346
|
+
assert.ok(ctx.includes('**Implementing the login form validation**'));
|
|
347
|
+
} finally {
|
|
348
|
+
cleanup(tmp);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('constraints field appears in output', () => {
|
|
353
|
+
const tmp = createTmpDir();
|
|
354
|
+
try {
|
|
355
|
+
const sessionDir = setupGitRepo(tmp);
|
|
356
|
+
|
|
357
|
+
const handoff = {
|
|
358
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
359
|
+
progress_update: {
|
|
360
|
+
summary: 'Summary text',
|
|
361
|
+
constraints: ['Must use existing auth library', 'No breaking API changes'],
|
|
362
|
+
accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: []
|
|
363
|
+
},
|
|
364
|
+
beads: { tasks: [] }, context: {}
|
|
365
|
+
};
|
|
366
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
367
|
+
|
|
368
|
+
const result = runHook(tmp);
|
|
369
|
+
const output = JSON.parse(result.stdout);
|
|
370
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
371
|
+
|
|
372
|
+
assert.ok(ctx.includes('Constraints'));
|
|
373
|
+
assert.ok(ctx.includes('Must use existing auth library'));
|
|
374
|
+
assert.ok(ctx.includes('No breaking API changes'));
|
|
375
|
+
} finally {
|
|
376
|
+
cleanup(tmp);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('open_questions field appears in output', () => {
|
|
381
|
+
const tmp = createTmpDir();
|
|
382
|
+
try {
|
|
383
|
+
const sessionDir = setupGitRepo(tmp);
|
|
384
|
+
|
|
385
|
+
const handoff = {
|
|
386
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
387
|
+
progress_update: {
|
|
388
|
+
summary: 'Summary text',
|
|
389
|
+
open_questions: ['Should we support OAuth?', 'What timeout value to use?'],
|
|
390
|
+
accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'medium', risks: []
|
|
391
|
+
},
|
|
392
|
+
beads: { tasks: [] }, context: {}
|
|
393
|
+
};
|
|
394
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
395
|
+
|
|
396
|
+
const result = runHook(tmp);
|
|
397
|
+
const output = JSON.parse(result.stdout);
|
|
398
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
399
|
+
|
|
400
|
+
assert.ok(ctx.includes('Open Questions'));
|
|
401
|
+
assert.ok(ctx.includes('Should we support OAuth?'));
|
|
402
|
+
assert.ok(ctx.includes('What timeout value to use?'));
|
|
403
|
+
} finally {
|
|
404
|
+
cleanup(tmp);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ──────────────────────────────────────────────────────────────────
|
|
410
|
+
// Working Set Extraction
|
|
411
|
+
// ──────────────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
describe('Working Set Extraction', () => {
|
|
414
|
+
it('key_files from working_set', () => {
|
|
415
|
+
const tmp = createTmpDir();
|
|
416
|
+
try {
|
|
417
|
+
const sessionDir = setupGitRepo(tmp);
|
|
418
|
+
|
|
419
|
+
const handoff = {
|
|
420
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
421
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
422
|
+
working_set: { key_files: ['src/auth.py', 'tests/test_auth.py'], active_ids: [], recent_commands: [] },
|
|
423
|
+
beads: { tasks: [] }, context: {}
|
|
424
|
+
};
|
|
425
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
426
|
+
|
|
427
|
+
const result = runHook(tmp);
|
|
428
|
+
const output = JSON.parse(result.stdout);
|
|
429
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
430
|
+
|
|
431
|
+
assert.ok(ctx.includes('Working Set'));
|
|
432
|
+
assert.ok(ctx.includes('Key Files'));
|
|
433
|
+
assert.ok(ctx.includes('src/auth.py'));
|
|
434
|
+
assert.ok(ctx.includes('tests/test_auth.py'));
|
|
435
|
+
} finally {
|
|
436
|
+
cleanup(tmp);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('active_ids from working_set', () => {
|
|
441
|
+
const tmp = createTmpDir();
|
|
442
|
+
try {
|
|
443
|
+
const sessionDir = setupGitRepo(tmp);
|
|
444
|
+
|
|
445
|
+
const handoff = {
|
|
446
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
447
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
448
|
+
working_set: { key_files: [], active_ids: ['TASK-123', 'BUG-456'], recent_commands: [] },
|
|
449
|
+
beads: { tasks: [] }, context: {}
|
|
450
|
+
};
|
|
451
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
452
|
+
|
|
453
|
+
const result = runHook(tmp);
|
|
454
|
+
const output = JSON.parse(result.stdout);
|
|
455
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
456
|
+
|
|
457
|
+
assert.ok(ctx.includes('Active IDs'));
|
|
458
|
+
assert.ok(ctx.includes('TASK-123'));
|
|
459
|
+
assert.ok(ctx.includes('BUG-456'));
|
|
460
|
+
} finally {
|
|
461
|
+
cleanup(tmp);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('recent_commands from working_set', () => {
|
|
466
|
+
const tmp = createTmpDir();
|
|
467
|
+
try {
|
|
468
|
+
const sessionDir = setupGitRepo(tmp);
|
|
469
|
+
|
|
470
|
+
const handoff = {
|
|
471
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
472
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
473
|
+
working_set: { key_files: [], active_ids: [], recent_commands: ['npm test', 'npm run build'] },
|
|
474
|
+
beads: { tasks: [] }, context: {}
|
|
475
|
+
};
|
|
476
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
477
|
+
|
|
478
|
+
const result = runHook(tmp);
|
|
479
|
+
const output = JSON.parse(result.stdout);
|
|
480
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
481
|
+
|
|
482
|
+
assert.ok(ctx.includes('Recent Commands'));
|
|
483
|
+
assert.ok(ctx.includes('npm test'));
|
|
484
|
+
assert.ok(ctx.includes('npm run build'));
|
|
485
|
+
} finally {
|
|
486
|
+
cleanup(tmp);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ──────────────────────────────────────────────────────────────────
|
|
492
|
+
// V1.0 Fallback Behavior
|
|
493
|
+
// ──────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
describe('V1.0 Fallback Behavior', () => {
|
|
496
|
+
it('fallback to context.key_files when no working_set', () => {
|
|
497
|
+
const tmp = createTmpDir();
|
|
498
|
+
try {
|
|
499
|
+
const sessionDir = setupGitRepo(tmp);
|
|
500
|
+
|
|
501
|
+
const handoff = {
|
|
502
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
503
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
504
|
+
beads: { tasks: [] },
|
|
505
|
+
context: { key_files: ['legacy_file.py', 'old_test.py'], last_commit: 'abc123', wip_state: 'clean' }
|
|
506
|
+
};
|
|
507
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
508
|
+
|
|
509
|
+
const result = runHook(tmp);
|
|
510
|
+
const output = JSON.parse(result.stdout);
|
|
511
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
512
|
+
|
|
513
|
+
assert.ok(ctx.includes('Key Files'));
|
|
514
|
+
assert.ok(ctx.includes('legacy_file.py'));
|
|
515
|
+
assert.ok(ctx.includes('old_test.py'));
|
|
516
|
+
} finally {
|
|
517
|
+
cleanup(tmp);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('v1.0 handoff produces valid output', () => {
|
|
522
|
+
const tmp = createTmpDir();
|
|
523
|
+
try {
|
|
524
|
+
const sessionDir = setupGitRepo(tmp);
|
|
525
|
+
|
|
526
|
+
const handoff = {
|
|
527
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Legacy Task',
|
|
528
|
+
progress_update: { summary: 'Legacy summary', accomplished: ['Did something'], next_steps: ['Do more'], decisions: [], blockers: [], confidence: 'medium', risks: [] },
|
|
529
|
+
beads: { tasks: [] }, context: { last_commit: 'abc123', wip_state: 'clean' }
|
|
530
|
+
};
|
|
531
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
532
|
+
|
|
533
|
+
const result = runHook(tmp);
|
|
534
|
+
assert.equal(result.exitCode, 0);
|
|
535
|
+
const output = JSON.parse(result.stdout);
|
|
536
|
+
|
|
537
|
+
assert.ok('systemMessage' in output);
|
|
538
|
+
assert.ok('hookSpecificOutput' in output);
|
|
539
|
+
assert.ok('additionalContext' in output.hookSpecificOutput);
|
|
540
|
+
|
|
541
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
542
|
+
assert.ok(output.systemMessage.includes('Legacy Task'));
|
|
543
|
+
assert.ok(ctx.includes('Legacy summary'));
|
|
544
|
+
assert.ok(ctx.includes('Did something'));
|
|
545
|
+
assert.ok(ctx.includes('Do more'));
|
|
546
|
+
} finally {
|
|
547
|
+
cleanup(tmp);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('v1.0 handoff does not show empty v1.1 sections', () => {
|
|
552
|
+
const tmp = createTmpDir();
|
|
553
|
+
try {
|
|
554
|
+
const sessionDir = setupGitRepo(tmp);
|
|
555
|
+
|
|
556
|
+
const handoff = {
|
|
557
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test',
|
|
558
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
559
|
+
beads: { tasks: [] }, context: {}
|
|
560
|
+
};
|
|
561
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
562
|
+
|
|
563
|
+
const result = runHook(tmp);
|
|
564
|
+
const output = JSON.parse(result.stdout);
|
|
565
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
566
|
+
|
|
567
|
+
assert.ok(!ctx.includes('Active Work (Resume Here)'));
|
|
568
|
+
assert.ok(!ctx.includes('### Goal\n\n'));
|
|
569
|
+
} finally {
|
|
570
|
+
cleanup(tmp);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ──────────────────────────────────────────────────────────────────
|
|
576
|
+
// Beads Conditional Rendering
|
|
577
|
+
// ──────────────────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
describe('Beads Conditional Rendering', () => {
|
|
580
|
+
it('beads tasks rendered when available true', () => {
|
|
581
|
+
const tmp = createTmpDir();
|
|
582
|
+
try {
|
|
583
|
+
const sessionDir = setupGitRepo(tmp);
|
|
584
|
+
|
|
585
|
+
const handoff = {
|
|
586
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
587
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
588
|
+
beads: {
|
|
589
|
+
available: true,
|
|
590
|
+
tasks: [{ id: 'task-1', title: 'Implement feature', completed: false, status: 'open' }]
|
|
591
|
+
},
|
|
592
|
+
context: {}
|
|
593
|
+
};
|
|
594
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
595
|
+
|
|
596
|
+
const result = runHook(tmp);
|
|
597
|
+
const output = JSON.parse(result.stdout);
|
|
598
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
599
|
+
|
|
600
|
+
assert.ok(ctx.includes('Beads Tasks'));
|
|
601
|
+
assert.ok(ctx.includes('Implement feature'));
|
|
602
|
+
assert.ok(ctx.includes('task-1'));
|
|
603
|
+
} finally {
|
|
604
|
+
cleanup(tmp);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('beads tasks not rendered when available false', () => {
|
|
609
|
+
const tmp = createTmpDir();
|
|
610
|
+
try {
|
|
611
|
+
const sessionDir = setupGitRepo(tmp);
|
|
612
|
+
|
|
613
|
+
const handoff = {
|
|
614
|
+
version: '1.1', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
615
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
616
|
+
beads: {
|
|
617
|
+
available: false,
|
|
618
|
+
tasks: [{ id: 'task-1', title: 'Should not appear', completed: false, status: 'open' }]
|
|
619
|
+
},
|
|
620
|
+
context: {}
|
|
621
|
+
};
|
|
622
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
623
|
+
|
|
624
|
+
const result = runHook(tmp);
|
|
625
|
+
const output = JSON.parse(result.stdout);
|
|
626
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
627
|
+
|
|
628
|
+
assert.ok(!ctx.includes('Beads Tasks'));
|
|
629
|
+
assert.ok(!ctx.includes('Should not appear'));
|
|
630
|
+
} finally {
|
|
631
|
+
cleanup(tmp);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('beads available defaults true for v1.0 compat', () => {
|
|
636
|
+
const tmp = createTmpDir();
|
|
637
|
+
try {
|
|
638
|
+
const sessionDir = setupGitRepo(tmp);
|
|
639
|
+
|
|
640
|
+
const handoff = {
|
|
641
|
+
version: '1.0', timestamp: '2024-01-01-120000', branch_name: 'main', task_name: 'Test Task',
|
|
642
|
+
progress_update: { summary: 'Summary', accomplished: [], next_steps: [], decisions: [], blockers: [], confidence: 'high', risks: [] },
|
|
643
|
+
beads: {
|
|
644
|
+
tasks: [{ id: 'v10-task', title: 'V1.0 Task', completed: false, status: 'open' }]
|
|
645
|
+
},
|
|
646
|
+
context: {}
|
|
647
|
+
};
|
|
648
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-01-120000_handoff.json'), JSON.stringify(handoff));
|
|
649
|
+
|
|
650
|
+
const result = runHook(tmp);
|
|
651
|
+
const output = JSON.parse(result.stdout);
|
|
652
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
653
|
+
|
|
654
|
+
assert.ok(ctx.includes('Beads Tasks'));
|
|
655
|
+
assert.ok(ctx.includes('V1.0 Task'));
|
|
656
|
+
} finally {
|
|
657
|
+
cleanup(tmp);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ──────────────────────────────────────────────────────────────────
|
|
663
|
+
// Complete V1.1 Handoff Scenario
|
|
664
|
+
// ──────────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
describe('Complete V1.1 Handoff Scenario', () => {
|
|
667
|
+
it('full v1.1 handoff with all fields', () => {
|
|
668
|
+
const tmp = createTmpDir();
|
|
669
|
+
try {
|
|
670
|
+
const sessionDir = setupGitRepo(tmp, 'feature-auth');
|
|
671
|
+
|
|
672
|
+
const handoff = {
|
|
673
|
+
version: '1.1', timestamp: '2024-01-15-143000', branch_name: 'feature-auth',
|
|
674
|
+
task_name: 'Implement OAuth2 Authentication',
|
|
675
|
+
progress_update: {
|
|
676
|
+
summary: 'Made good progress on OAuth2 integration with Google provider.',
|
|
677
|
+
goal: 'Complete OAuth2 flow with refresh token handling',
|
|
678
|
+
constraints: ['Must support existing session middleware', 'Cannot change database schema'],
|
|
679
|
+
accomplished: ['Implemented authorization endpoint', 'Added token exchange logic'],
|
|
680
|
+
now: 'Implementing refresh token rotation',
|
|
681
|
+
next_steps: ['Add token refresh endpoint', 'Write integration tests', 'Update API documentation'],
|
|
682
|
+
decisions: ['Use PKCE for public clients', 'Store refresh tokens encrypted'],
|
|
683
|
+
blockers: ['Waiting on security review approval'],
|
|
684
|
+
open_questions: ['Should we support multiple OAuth providers?', 'Token expiry time for mobile vs web?'],
|
|
685
|
+
confidence: 'high',
|
|
686
|
+
risks: ['Security review might require changes']
|
|
687
|
+
},
|
|
688
|
+
working_set: {
|
|
689
|
+
key_files: ['src/auth/oauth.py', 'src/auth/tokens.py', 'tests/test_oauth.py'],
|
|
690
|
+
active_ids: ['AUTH-42', 'AUTH-43'],
|
|
691
|
+
recent_commands: ['pytest tests/test_oauth.py -v', 'flask run --debug']
|
|
692
|
+
},
|
|
693
|
+
beads: {
|
|
694
|
+
available: true, workspace_label: 'feature-auth',
|
|
695
|
+
tasks: [
|
|
696
|
+
{ id: 'auth-1', title: 'Setup OAuth config', completed: true, status: 'closed' },
|
|
697
|
+
{ id: 'auth-2', title: 'Implement token refresh', completed: false, status: 'in_progress' }
|
|
698
|
+
]
|
|
699
|
+
},
|
|
700
|
+
context: { last_commit: 'def789abc', wip_state: 'uncommitted' }
|
|
701
|
+
};
|
|
702
|
+
fs.writeFileSync(path.join(sessionDir, '2024-01-15-143000_handoff.json'), JSON.stringify(handoff));
|
|
703
|
+
|
|
704
|
+
const result = runHook(tmp);
|
|
705
|
+
assert.equal(result.exitCode, 0);
|
|
706
|
+
const output = JSON.parse(result.stdout);
|
|
707
|
+
|
|
708
|
+
// System message
|
|
709
|
+
assert.ok(output.systemMessage.includes('Implement OAuth2 Authentication'));
|
|
710
|
+
assert.ok(output.systemMessage.includes('feature-auth'));
|
|
711
|
+
|
|
712
|
+
const ctx = output.hookSpecificOutput.additionalContext;
|
|
713
|
+
|
|
714
|
+
// v1.1 specific fields
|
|
715
|
+
assert.ok(ctx.includes('Goal'));
|
|
716
|
+
assert.ok(ctx.includes('Complete OAuth2 flow with refresh token handling'));
|
|
717
|
+
assert.ok(ctx.includes('Active Work (Resume Here)'));
|
|
718
|
+
assert.ok(ctx.includes('**Implementing refresh token rotation**'));
|
|
719
|
+
assert.ok(ctx.includes('Constraints'));
|
|
720
|
+
assert.ok(ctx.includes('Must support existing session middleware'));
|
|
721
|
+
assert.ok(ctx.includes('Open Questions'));
|
|
722
|
+
assert.ok(ctx.includes('Should we support multiple OAuth providers?'));
|
|
723
|
+
|
|
724
|
+
// Working set
|
|
725
|
+
assert.ok(ctx.includes('Working Set'));
|
|
726
|
+
assert.ok(ctx.includes('src/auth/oauth.py'));
|
|
727
|
+
assert.ok(ctx.includes('AUTH-42'));
|
|
728
|
+
assert.ok(ctx.includes('pytest tests/test_oauth.py -v'));
|
|
729
|
+
|
|
730
|
+
// Standard fields
|
|
731
|
+
assert.ok(ctx.includes('Implemented authorization endpoint'));
|
|
732
|
+
assert.ok(ctx.includes('Add token refresh endpoint'));
|
|
733
|
+
assert.ok(ctx.includes('Use PKCE for public clients'));
|
|
734
|
+
assert.ok(ctx.includes('Waiting on security review approval'));
|
|
735
|
+
assert.ok(ctx.toLowerCase().includes('high'));
|
|
736
|
+
|
|
737
|
+
// Beads tasks
|
|
738
|
+
assert.ok(ctx.includes('Beads Tasks'));
|
|
739
|
+
assert.ok(ctx.includes('Setup OAuth config'));
|
|
740
|
+
assert.ok(ctx.includes('Implement token refresh'));
|
|
741
|
+
} finally {
|
|
742
|
+
cleanup(tmp);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// ──────────────────────────────────────────────────────────────────
|
|
748
|
+
// Copy Plugin References
|
|
749
|
+
// ──────────────────────────────────────────────────────────────────
|
|
750
|
+
|
|
751
|
+
describe('Copy Plugin References', () => {
|
|
752
|
+
it('copies md files from plugin references to .claude/spectre', () => {
|
|
753
|
+
const tmp = createTmpDir();
|
|
754
|
+
const pluginRoot = path.join(tmp, 'plugin');
|
|
755
|
+
const referencesDir = path.join(pluginRoot, 'references');
|
|
756
|
+
fs.mkdirSync(referencesDir, { recursive: true });
|
|
757
|
+
|
|
758
|
+
fs.writeFileSync(path.join(referencesDir, 'next_steps_guide.md'), '# Next Steps Guide\nContent here');
|
|
759
|
+
fs.writeFileSync(path.join(referencesDir, 'other_reference.md'), '# Other Reference\nMore content');
|
|
760
|
+
|
|
761
|
+
const projectDir = path.join(tmp, 'project');
|
|
762
|
+
fs.mkdirSync(projectDir);
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
// Run the bg-copy-refs mode directly
|
|
766
|
+
execFileSync(process.execPath, [SCRIPT_PATH, '--bg-copy-refs'], {
|
|
767
|
+
env: Object.assign({}, process.env, { CLAUDE_PLUGIN_ROOT: pluginRoot }),
|
|
768
|
+
cwd: projectDir,
|
|
769
|
+
timeout: 10000
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const spectreDir = path.join(projectDir, '.claude', 'spectre');
|
|
773
|
+
assert.ok(fs.existsSync(spectreDir));
|
|
774
|
+
assert.ok(fs.existsSync(path.join(spectreDir, 'next_steps_guide.md')));
|
|
775
|
+
assert.ok(fs.existsSync(path.join(spectreDir, 'other_reference.md')));
|
|
776
|
+
assert.equal(
|
|
777
|
+
fs.readFileSync(path.join(spectreDir, 'next_steps_guide.md'), 'utf8'),
|
|
778
|
+
'# Next Steps Guide\nContent here'
|
|
779
|
+
);
|
|
780
|
+
} finally {
|
|
781
|
+
cleanup(tmp);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('returns early when plugin root not set', () => {
|
|
786
|
+
const tmp = createTmpDir();
|
|
787
|
+
const projectDir = path.join(tmp, 'project');
|
|
788
|
+
fs.mkdirSync(projectDir);
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const env = Object.assign({}, process.env);
|
|
792
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
793
|
+
|
|
794
|
+
execFileSync(process.execPath, [SCRIPT_PATH, '--bg-copy-refs'], {
|
|
795
|
+
env,
|
|
796
|
+
cwd: projectDir,
|
|
797
|
+
timeout: 10000
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
assert.ok(!fs.existsSync(path.join(projectDir, '.claude', 'spectre')));
|
|
801
|
+
} finally {
|
|
802
|
+
cleanup(tmp);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('appends to gitignore when exists and .claude not ignored', () => {
|
|
807
|
+
const tmp = createTmpDir();
|
|
808
|
+
const pluginRoot = path.join(tmp, 'plugin');
|
|
809
|
+
const referencesDir = path.join(pluginRoot, 'references');
|
|
810
|
+
fs.mkdirSync(referencesDir, { recursive: true });
|
|
811
|
+
fs.writeFileSync(path.join(referencesDir, 'guide.md'), '# Guide');
|
|
812
|
+
|
|
813
|
+
const projectDir = path.join(tmp, 'project');
|
|
814
|
+
fs.mkdirSync(projectDir);
|
|
815
|
+
const gitignore = path.join(projectDir, '.gitignore');
|
|
816
|
+
fs.writeFileSync(gitignore, 'node_modules/\n*.log\n');
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
execFileSync(process.execPath, [SCRIPT_PATH, '--bg-copy-refs'], {
|
|
820
|
+
env: Object.assign({}, process.env, { CLAUDE_PLUGIN_ROOT: pluginRoot }),
|
|
821
|
+
cwd: projectDir,
|
|
822
|
+
timeout: 10000
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const content = fs.readFileSync(gitignore, 'utf8');
|
|
826
|
+
assert.ok(content.includes('.claude/spectre/'));
|
|
827
|
+
assert.ok(content.includes('node_modules/'));
|
|
828
|
+
} finally {
|
|
829
|
+
cleanup(tmp);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('skips gitignore when .claude already ignored', () => {
|
|
834
|
+
const tmp = createTmpDir();
|
|
835
|
+
const pluginRoot = path.join(tmp, 'plugin');
|
|
836
|
+
const referencesDir = path.join(pluginRoot, 'references');
|
|
837
|
+
fs.mkdirSync(referencesDir, { recursive: true });
|
|
838
|
+
fs.writeFileSync(path.join(referencesDir, 'guide.md'), '# Guide');
|
|
839
|
+
|
|
840
|
+
const projectDir = path.join(tmp, 'project');
|
|
841
|
+
fs.mkdirSync(projectDir);
|
|
842
|
+
const gitignore = path.join(projectDir, '.gitignore');
|
|
843
|
+
const originalContent = 'node_modules/\n.claude/\n*.log\n';
|
|
844
|
+
fs.writeFileSync(gitignore, originalContent);
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
execFileSync(process.execPath, [SCRIPT_PATH, '--bg-copy-refs'], {
|
|
848
|
+
env: Object.assign({}, process.env, { CLAUDE_PLUGIN_ROOT: pluginRoot }),
|
|
849
|
+
cwd: projectDir,
|
|
850
|
+
timeout: 10000
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
assert.equal(fs.readFileSync(gitignore, 'utf8'), originalContent);
|
|
854
|
+
} finally {
|
|
855
|
+
cleanup(tmp);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|