jettypod 3.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/PROTECT_SKILLS.md +28 -0
- package/.claude/settings.json +24 -0
- package/.claude/settings.local.json +16 -0
- package/.claude/skills/epic-discover/SKILL.md +262 -0
- package/.claude/skills/feature-discover/SKILL.md +393 -0
- package/.claude/skills/speed-mode/SKILL.md +364 -0
- package/.claude/skills/stable-mode/SKILL.md +591 -0
- package/.github/workflows/test-safety.yml +85 -0
- package/README.md +25 -0
- package/SPEED-STABLE-AUDIT.md +853 -0
- package/SYSTEM-BEHAVIOR.md +1241 -0
- package/TEST_SAFETY_AUDIT.md +314 -0
- package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
- package/cucumber.js +8 -0
- package/docs/COMMAND_REFERENCE.md +903 -0
- package/docs/DECISIONS.md +68 -0
- package/docs/README.md +48 -0
- package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
- package/docs/TEST-REWRITE-PLAN.md +261 -0
- package/docs/ai-test-writing-requirements.md +219 -0
- package/docs/claude-code-skills.md +607 -0
- package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
- package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
- package/docs/features/jettypod-standards-explained.md +543 -0
- package/docs/features/standards-inventory.md +257 -0
- package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
- package/docs/jettypod-system-overview.md +409 -0
- package/features/auto-generate-production-chores.feature +14 -0
- package/features/claude-md-protection/steps.js +487 -0
- package/features/decisions/index.js +490 -0
- package/features/decisions/index.test.js +208 -0
- package/features/git-hooks/git-hooks.feature +30 -0
- package/features/git-hooks/index.js +93 -0
- package/features/git-hooks/index.test.js +137 -0
- package/features/git-hooks/post-commit +56 -0
- package/features/git-hooks/post-merge +47 -0
- package/features/git-hooks/pre-commit +28 -0
- package/features/git-hooks/simple-steps.js +53 -0
- package/features/git-hooks/simple-test.feature +10 -0
- package/features/git-hooks/steps.js +196 -0
- package/features/jettypod-update-command.feature +46 -0
- package/features/mode-prompts/index.js +95 -0
- package/features/mode-prompts/simple-steps.js +44 -0
- package/features/mode-prompts/simple-test.feature +9 -0
- package/features/mode-prompts/validation.test.js +120 -0
- package/features/refactor-mode/steps.js +217 -0
- package/features/refactor-mode.feature +49 -0
- package/features/skills-update/index.test.js +216 -0
- package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
- package/features/step_definitions/terminal-logo.steps.js +145 -0
- package/features/step_definitions/update-command.steps.js +183 -0
- package/features/terminal-logo/index.js +39 -0
- package/features/terminal-logo/terminal-logo.feature +30 -0
- package/features/update-command/index.js +181 -0
- package/features/update-command/index.test.js +225 -0
- package/features/work-commands/bug-workflow-display.feature +22 -0
- package/features/work-commands/index.js +311 -0
- package/features/work-commands/simple-steps.js +69 -0
- package/features/work-commands/stable-tests.feature +57 -0
- package/features/work-commands/steps.js +1120 -0
- package/features/work-commands/validation.test.js +88 -0
- package/features/work-commands/work-commands.feature +13 -0
- package/features/work-tracking/discovery-validation.test.js +228 -0
- package/features/work-tracking/index.js +1511 -0
- package/features/work-tracking/mode-required.feature +112 -0
- package/features/work-tracking/phase-tracking.test.js +482 -0
- package/features/work-tracking/prototype-tracking.test.js +485 -0
- package/features/work-tracking/tree-view.test.js +310 -0
- package/features/work-tracking/work-set-mode.feature +71 -0
- package/features/work-tracking/work-start-mode.feature +88 -0
- package/full-test.txt +0 -0
- package/install.sh +89 -0
- package/jettypod.js +1640 -0
- package/lib/bug-workflow.js +94 -0
- package/lib/bug-workflow.test.js +177 -0
- package/lib/claudemd.js +130 -0
- package/lib/claudemd.test.js +195 -0
- package/lib/comprehensive-standards-full.json +1778 -0
- package/lib/config.js +181 -0
- package/lib/config.test.js +511 -0
- package/lib/constants.js +107 -0
- package/lib/constants.test.js +164 -0
- package/lib/current-work.js +130 -0
- package/lib/current-work.test.js +146 -0
- package/lib/database-project-config.test.js +107 -0
- package/lib/database.js +256 -0
- package/lib/database.test.js +106 -0
- package/lib/decisions-generator.js +102 -0
- package/lib/decisions-generator.test.js +457 -0
- package/lib/decisions-helpers.js +119 -0
- package/lib/decisions-helpers.test.js +310 -0
- package/lib/discovery-checkpoint.js +83 -0
- package/lib/docs-generator.js +280 -0
- package/lib/external-checklist.js +177 -0
- package/lib/git.js +142 -0
- package/lib/git.test.js +145 -0
- package/lib/logo.js +3 -0
- package/lib/migrations/001-epic-to-parent.js +24 -0
- package/lib/migrations/002-default-work-item-modes.js +37 -0
- package/lib/migrations/002-default-work-item-modes.test.js +351 -0
- package/lib/migrations/003-epic-discovery-fields.js +52 -0
- package/lib/migrations/004-discovery-decisions-table.js +32 -0
- package/lib/migrations/005-migrate-decision-data.js +62 -0
- package/lib/migrations/006-feature-phase-field.js +61 -0
- package/lib/migrations/007-prototype-tracking.js +38 -0
- package/lib/migrations/008-scenario-file-field.js +24 -0
- package/lib/migrations/index.js +74 -0
- package/lib/production-helpers.js +69 -0
- package/lib/project-state.test.js +92 -0
- package/lib/test-helpers.js +184 -0
- package/lib/test-helpers.test.js +255 -0
- package/package.json +36 -0
- package/prototypes/test/index.html +1 -0
- package/setup-dist-repo.sh +68 -0
- package/test-safety-check.sh +80 -0
- package/work-item-tracking-plan.md +199 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
const { Given, When, Then, Before, After } = require('@cucumber/cucumber');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const testDir = path.join('/tmp', 'jettypod-hook-test-' + Date.now());
|
|
8
|
+
let originalDir;
|
|
9
|
+
let hookResponse;
|
|
10
|
+
let claudeMdContent;
|
|
11
|
+
let editAttempt;
|
|
12
|
+
|
|
13
|
+
Before(function () {
|
|
14
|
+
originalDir = process.cwd();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
After(function () {
|
|
18
|
+
if (originalDir) {
|
|
19
|
+
process.chdir(originalDir);
|
|
20
|
+
}
|
|
21
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
22
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
23
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Given('a JettyPod project with hook installed', function () {
|
|
28
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
29
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
30
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
33
|
+
process.chdir(testDir);
|
|
34
|
+
|
|
35
|
+
execSync('git init', { stdio: 'pipe' });
|
|
36
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
37
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
38
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
39
|
+
|
|
40
|
+
// Verify hook was installed
|
|
41
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
42
|
+
assert(fs.existsSync(hookPath), 'Hook file not installed');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
Given('CLAUDE.md contains {string}', function (content) {
|
|
46
|
+
claudeMdContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
47
|
+
assert(claudeMdContent.includes(content), `CLAUDE.md does not contain: ${content}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
Given('CLAUDE.md has the default content after init', function () {
|
|
51
|
+
claudeMdContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
52
|
+
// Check that there's no actual current_work section with content (not just the reference in jettypod_workflow)
|
|
53
|
+
const hasCurrentWorkSection = claudeMdContent.match(/<current_work>\s*Working on:/);
|
|
54
|
+
assert(!hasCurrentWorkSection, 'CLAUDE.md should not have an active current_work section after init');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
Given('CLAUDE.md has stage {string} after init', function (stage) {
|
|
58
|
+
claudeMdContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
59
|
+
assert(claudeMdContent.includes(`<stage>${stage}</stage>`), `CLAUDE.md should contain stage ${stage}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
Given('CLAUDE.md has the default mission', function () {
|
|
63
|
+
claudeMdContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
64
|
+
assert(claudeMdContent.includes('<mission>'), 'CLAUDE.md should have a mission section');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
Given('CLAUDE.md has a tech_stack section', function () {
|
|
68
|
+
claudeMdContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
69
|
+
assert(claudeMdContent.includes('<tech_stack>'), 'CLAUDE.md should have a tech_stack section');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
Given('a file {string} exists', function (filePath) {
|
|
73
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
74
|
+
fs.writeFileSync(filePath, 'const x = 1;');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
Given('CLAUDE.md does not exist', function () {
|
|
78
|
+
if (fs.existsSync('CLAUDE.md')) {
|
|
79
|
+
fs.unlinkSync('CLAUDE.md');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
Given('a new empty directory', function () {
|
|
84
|
+
if (fs.existsSync(testDir)) {
|
|
85
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
88
|
+
process.chdir(testDir);
|
|
89
|
+
|
|
90
|
+
// Set testDir on this context so other step files can use it
|
|
91
|
+
this.testDir = testDir;
|
|
92
|
+
|
|
93
|
+
// Initialize git for jettypod init
|
|
94
|
+
execSync('git init', { stdio: 'pipe' });
|
|
95
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
96
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Given('the hook receives invalid JSON input', function () {
|
|
100
|
+
editAttempt = {
|
|
101
|
+
invalidInput: true,
|
|
102
|
+
input: 'not valid json {'
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
Given('a JettyPod project in discovery mode', function () {
|
|
107
|
+
if (fs.existsSync(testDir)) {
|
|
108
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
111
|
+
process.chdir(testDir);
|
|
112
|
+
|
|
113
|
+
execSync('git init', { stdio: 'pipe' });
|
|
114
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
115
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
116
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
|
|
117
|
+
|
|
118
|
+
// Mode is now managed by work items, not project-level config
|
|
119
|
+
const config = JSON.parse(fs.readFileSync('.jettypod/config.json', 'utf-8'));
|
|
120
|
+
assert(config.name, 'Config should have project name');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
Given('the Claude Code hook is active', function () {
|
|
124
|
+
const settingsPath = path.join(testDir, '.claude', 'settings.json');
|
|
125
|
+
assert(fs.existsSync(settingsPath), 'Claude Code settings not found');
|
|
126
|
+
|
|
127
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
128
|
+
assert(settings.hooks, 'No hooks in settings');
|
|
129
|
+
assert(settings.hooks.PreToolUse, 'No PreToolUse hooks');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
When('Claude Code attempts to edit it to {string}', function (newContent) {
|
|
133
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
134
|
+
|
|
135
|
+
// Find the old content from the last Given step
|
|
136
|
+
let oldContent;
|
|
137
|
+
if (claudeMdContent.includes('<mode>')) {
|
|
138
|
+
oldContent = claudeMdContent.match(/<mode>\w+<\/mode>/)?.[0] || '<mode>discovery</mode>';
|
|
139
|
+
} else if (claudeMdContent.includes('Status:')) {
|
|
140
|
+
oldContent = claudeMdContent.match(/Status: \w+/)?.[0] || 'Status: in_progress';
|
|
141
|
+
} else if (claudeMdContent.includes('<stage>')) {
|
|
142
|
+
oldContent = claudeMdContent.match(/<stage>\w+<\/stage>/)?.[0] || '<stage>mature</stage>';
|
|
143
|
+
} else if (claudeMdContent.includes('<mission>')) {
|
|
144
|
+
oldContent = '<mission>Old mission</mission>';
|
|
145
|
+
} else if (claudeMdContent.includes('<standards>')) {
|
|
146
|
+
oldContent = '<standards>Old standards</standards>';
|
|
147
|
+
} else {
|
|
148
|
+
oldContent = '<project_summary>test</project_summary>';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hookInput = JSON.stringify({
|
|
152
|
+
session_id: 'test-session',
|
|
153
|
+
transcript_path: '/tmp/transcript.md',
|
|
154
|
+
cwd: testDir,
|
|
155
|
+
hook_event_name: 'PreToolUse',
|
|
156
|
+
tool_name: 'Edit',
|
|
157
|
+
tool_input: {
|
|
158
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
159
|
+
old_string: oldContent,
|
|
160
|
+
new_string: newContent
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = spawnSync('node', [hookPath], {
|
|
165
|
+
input: hookInput,
|
|
166
|
+
encoding: 'utf-8'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
hookResponse = JSON.parse(result.stdout);
|
|
170
|
+
editAttempt = { oldContent, newContent };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
When('Claude Code attempts to add {string}', function (newContent) {
|
|
174
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
175
|
+
|
|
176
|
+
const hookInput = JSON.stringify({
|
|
177
|
+
session_id: 'test-session',
|
|
178
|
+
transcript_path: '/tmp/transcript.md',
|
|
179
|
+
cwd: testDir,
|
|
180
|
+
hook_event_name: 'PreToolUse',
|
|
181
|
+
tool_name: 'Edit',
|
|
182
|
+
tool_input: {
|
|
183
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
184
|
+
old_string: '<project_summary>test</project_summary>',
|
|
185
|
+
new_string: newContent
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const result = spawnSync('node', [hookPath], {
|
|
190
|
+
input: hookInput,
|
|
191
|
+
encoding: 'utf-8'
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
hookResponse = JSON.parse(result.stdout);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
When('Claude Code attempts to add a current_work section', function () {
|
|
198
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
199
|
+
|
|
200
|
+
const hookInput = JSON.stringify({
|
|
201
|
+
session_id: 'test-session',
|
|
202
|
+
transcript_path: '/tmp/transcript.md',
|
|
203
|
+
cwd: testDir,
|
|
204
|
+
hook_event_name: 'PreToolUse',
|
|
205
|
+
tool_name: 'Edit',
|
|
206
|
+
tool_input: {
|
|
207
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
208
|
+
old_string: '</claude_context>',
|
|
209
|
+
new_string: '<current_work>Working on: [#75]</current_work>\n</claude_context>'
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = spawnSync('node', [hookPath], {
|
|
214
|
+
input: hookInput,
|
|
215
|
+
encoding: 'utf-8'
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
hookResponse = JSON.parse(result.stdout);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
When('Claude Code attempts to change stage to {string}', function (newStage) {
|
|
222
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
223
|
+
|
|
224
|
+
const claudeContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
225
|
+
const oldStage = claudeContent.match(/<stage>\w+<\/stage>/)?.[0];
|
|
226
|
+
|
|
227
|
+
const hookInput = JSON.stringify({
|
|
228
|
+
session_id: 'test-session',
|
|
229
|
+
transcript_path: '/tmp/transcript.md',
|
|
230
|
+
cwd: testDir,
|
|
231
|
+
hook_event_name: 'PreToolUse',
|
|
232
|
+
tool_name: 'Edit',
|
|
233
|
+
tool_input: {
|
|
234
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
235
|
+
old_string: oldStage,
|
|
236
|
+
new_string: `<stage>${newStage}</stage>`
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const result = spawnSync('node', [hookPath], {
|
|
241
|
+
input: hookInput,
|
|
242
|
+
encoding: 'utf-8'
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
hookResponse = JSON.parse(result.stdout);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
When('Claude Code attempts to change the mission', function () {
|
|
249
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
250
|
+
|
|
251
|
+
const claudeContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
252
|
+
const oldMission = claudeContent.match(/<mission>[\s\S]*?<\/mission>/)?.[0];
|
|
253
|
+
|
|
254
|
+
const hookInput = JSON.stringify({
|
|
255
|
+
session_id: 'test-session',
|
|
256
|
+
transcript_path: '/tmp/transcript.md',
|
|
257
|
+
cwd: testDir,
|
|
258
|
+
hook_event_name: 'PreToolUse',
|
|
259
|
+
tool_name: 'Edit',
|
|
260
|
+
tool_input: {
|
|
261
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
262
|
+
old_string: oldMission,
|
|
263
|
+
new_string: '<mission>New mission text</mission>'
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = spawnSync('node', [hookPath], {
|
|
268
|
+
input: hookInput,
|
|
269
|
+
encoding: 'utf-8'
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
hookResponse = JSON.parse(result.stdout);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
When('Claude Code attempts to change the tech_stack', function () {
|
|
276
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
277
|
+
|
|
278
|
+
const claudeContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
279
|
+
const oldTechStack = claudeContent.match(/<tech_stack>[\s\S]*?<\/tech_stack>/)?.[0];
|
|
280
|
+
|
|
281
|
+
const hookInput = JSON.stringify({
|
|
282
|
+
session_id: 'test-session',
|
|
283
|
+
transcript_path: '/tmp/transcript.md',
|
|
284
|
+
cwd: testDir,
|
|
285
|
+
hook_event_name: 'PreToolUse',
|
|
286
|
+
tool_name: 'Edit',
|
|
287
|
+
tool_input: {
|
|
288
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
289
|
+
old_string: oldTechStack,
|
|
290
|
+
new_string: '<tech_stack>React, TypeScript</tech_stack>'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const result = spawnSync('node', [hookPath], {
|
|
295
|
+
input: hookInput,
|
|
296
|
+
encoding: 'utf-8'
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
hookResponse = JSON.parse(result.stdout);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
When('Claude Code attempts to edit any content in it', function () {
|
|
303
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
304
|
+
|
|
305
|
+
const hookInput = JSON.stringify({
|
|
306
|
+
session_id: 'test-session',
|
|
307
|
+
transcript_path: '/tmp/transcript.md',
|
|
308
|
+
cwd: testDir,
|
|
309
|
+
hook_event_name: 'PreToolUse',
|
|
310
|
+
tool_name: 'Edit',
|
|
311
|
+
tool_input: {
|
|
312
|
+
file_path: path.join(testDir, 'src', 'index.js'),
|
|
313
|
+
old_string: 'const x = 1;',
|
|
314
|
+
new_string: 'const x = 2;'
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = spawnSync('node', [hookPath], {
|
|
319
|
+
input: hookInput,
|
|
320
|
+
encoding: 'utf-8'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
hookResponse = JSON.parse(result.stdout);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
When('Claude Code attempts to Write a new CLAUDE.md file', function () {
|
|
327
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
328
|
+
|
|
329
|
+
const hookInput = JSON.stringify({
|
|
330
|
+
session_id: 'test-session',
|
|
331
|
+
transcript_path: '/tmp/transcript.md',
|
|
332
|
+
cwd: testDir,
|
|
333
|
+
hook_event_name: 'PreToolUse',
|
|
334
|
+
tool_name: 'Write',
|
|
335
|
+
tool_input: {
|
|
336
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
337
|
+
content: '<claude_context>new file</claude_context>'
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const result = spawnSync('node', [hookPath], {
|
|
342
|
+
input: hookInput,
|
|
343
|
+
encoding: 'utf-8'
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
hookResponse = JSON.parse(result.stdout);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
When('I run jettypod command {string}', function (command) {
|
|
350
|
+
const result = execSync(`node ${path.join(originalDir, 'jettypod.js')} ${command.replace('jettypod ', '')}`, {
|
|
351
|
+
stdio: 'pipe',
|
|
352
|
+
encoding: 'utf-8'
|
|
353
|
+
});
|
|
354
|
+
this.commandOutput = result;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
When('the hook executes', function () {
|
|
358
|
+
const hookPath = path.join(originalDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
359
|
+
|
|
360
|
+
const result = spawnSync('node', [hookPath], {
|
|
361
|
+
input: editAttempt.input,
|
|
362
|
+
encoding: 'utf-8'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Should not crash, should fail open with allow
|
|
366
|
+
hookResponse = result.stdout ? JSON.parse(result.stdout) : { hookSpecificOutput: { permissionDecision: 'allow' } };
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
When('Claude Code tries to change mode to speed by editing CLAUDE.md', function () {
|
|
370
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
371
|
+
|
|
372
|
+
const claudeContent = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
373
|
+
const oldMode = claudeContent.match(/<mode>discovery<\/mode>/)?.[0];
|
|
374
|
+
|
|
375
|
+
const hookInput = JSON.stringify({
|
|
376
|
+
session_id: 'test-session',
|
|
377
|
+
transcript_path: '/tmp/transcript.md',
|
|
378
|
+
cwd: testDir,
|
|
379
|
+
hook_event_name: 'PreToolUse',
|
|
380
|
+
tool_name: 'Edit',
|
|
381
|
+
tool_input: {
|
|
382
|
+
file_path: path.join(testDir, 'CLAUDE.md'),
|
|
383
|
+
old_string: oldMode,
|
|
384
|
+
new_string: '<mode>speed</mode>'
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const result = spawnSync('node', [hookPath], {
|
|
389
|
+
input: hookInput,
|
|
390
|
+
encoding: 'utf-8'
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
hookResponse = JSON.parse(result.stdout);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
When('I run jettypod command {string} instead', function (command) {
|
|
397
|
+
execSync(`node ${path.join(originalDir, 'jettypod.js')} ${command.replace('jettypod ', '')}`, {
|
|
398
|
+
stdio: 'pipe'
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
Then('the edit is blocked with permission decision {string}', function (decision) {
|
|
403
|
+
assert(hookResponse, 'No hook response received');
|
|
404
|
+
assert(hookResponse.hookSpecificOutput, 'No hookSpecificOutput in response');
|
|
405
|
+
assert.strictEqual(hookResponse.hookSpecificOutput.permissionDecision, decision);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
Then('the error message suggests {string}', function (suggestion) {
|
|
409
|
+
assert(hookResponse.hookSpecificOutput.permissionDecisionReason, 'No reason provided');
|
|
410
|
+
assert(hookResponse.hookSpecificOutput.permissionDecisionReason.includes(suggestion),
|
|
411
|
+
`Expected suggestion to include "${suggestion}", got: ${hookResponse.hookSpecificOutput.permissionDecisionReason}`);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
Then('the edit is allowed with permission decision {string}', function (decision) {
|
|
415
|
+
assert(hookResponse, 'No hook response received');
|
|
416
|
+
assert(hookResponse.hookSpecificOutput, 'No hookSpecificOutput in response');
|
|
417
|
+
assert.strictEqual(hookResponse.hookSpecificOutput.permissionDecision, decision);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
Then('the hook file {string} should exist', function (filePath) {
|
|
421
|
+
const fullPath = path.join(testDir, filePath);
|
|
422
|
+
assert(fs.existsSync(fullPath), `File ${filePath} does not exist`);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
Then('the hook file should be executable', function () {
|
|
426
|
+
const hookPath = path.join(testDir, '.jettypod', 'hooks', 'protect-claude-md.js');
|
|
427
|
+
const stats = fs.statSync(hookPath);
|
|
428
|
+
// Check if executable bit is set (mode & 0o111 checks user/group/other exec bits)
|
|
429
|
+
assert((stats.mode & 0o111) !== 0, 'Hook file is not executable');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
Then('the file {string} should exist', function (filePath) {
|
|
433
|
+
const fullPath = path.join(testDir, filePath);
|
|
434
|
+
assert(fs.existsSync(fullPath), `File ${filePath} does not exist`);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
Then('{string} should contain PreToolUse configuration for Edit', function (filePath) {
|
|
438
|
+
const fullPath = path.join(testDir, filePath);
|
|
439
|
+
const settings = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
440
|
+
|
|
441
|
+
assert(settings.hooks, 'No hooks in settings');
|
|
442
|
+
assert(settings.hooks.PreToolUse, 'No PreToolUse hooks');
|
|
443
|
+
|
|
444
|
+
const editHook = settings.hooks.PreToolUse.find(h => h.matcher === 'Edit');
|
|
445
|
+
assert(editHook, 'No Edit hook found');
|
|
446
|
+
assert(editHook.hooks[0].command.includes('protect-claude-md.js'), 'Hook command not correct');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
Then('{string} should contain PreToolUse configuration for Write', function (filePath) {
|
|
450
|
+
const fullPath = path.join(testDir, filePath);
|
|
451
|
+
const settings = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
452
|
+
|
|
453
|
+
const writeHook = settings.hooks.PreToolUse.find(h => h.matcher === 'Write');
|
|
454
|
+
assert(writeHook, 'No Write hook found');
|
|
455
|
+
assert(writeHook.hooks[0].command.includes('protect-claude-md.js'), 'Hook command not correct');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
Then('the command output should contain {string}', function (expectedOutput) {
|
|
459
|
+
// Output is captured in this.commandOutput from When step
|
|
460
|
+
assert(this.commandOutput && this.commandOutput.includes(expectedOutput),
|
|
461
|
+
`Expected output to contain "${expectedOutput}"`);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
Then('no error is raised', function () {
|
|
465
|
+
// If we got here, no error was raised
|
|
466
|
+
assert(true);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
Then('the edit is blocked', function () {
|
|
470
|
+
assert(hookResponse, 'No hook response received');
|
|
471
|
+
assert.strictEqual(hookResponse.hookSpecificOutput.permissionDecision, 'deny');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
Then('Claude Code receives error {string}', function (errorText) {
|
|
475
|
+
assert(hookResponse.hookSpecificOutput.permissionDecisionReason.includes(errorText),
|
|
476
|
+
`Expected error to include "${errorText}"`);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
Then('CLAUDE.md is updated to speed mode', function () {
|
|
480
|
+
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
481
|
+
assert(content.includes('<mode>speed</mode>'), 'CLAUDE.md not updated to speed mode');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
Then('the update succeeds', function () {
|
|
485
|
+
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
486
|
+
assert(content.includes('<mode>speed</mode>'), 'Mode change did not succeed');
|
|
487
|
+
});
|