jettypod 4.1.2 → 4.1.4
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/.nvmrc +1 -0
- package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
- package/docs/DECISIONS.md +10 -12
- package/docs/NODE_VERSION.md +83 -0
- package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
- package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
- package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
- package/hooks/post-checkout +17 -0
- package/hooks/post-merge +17 -0
- package/hooks/pre-commit +30 -0
- package/jettypod.js +259 -120
- package/lib/coverage-tracker.js +218 -0
- package/lib/database.js +2 -0
- package/lib/db-export.js +192 -0
- package/lib/db-import.js +193 -0
- package/lib/external-transition-handler.js +32 -0
- package/lib/git-hook-helpers.js +174 -0
- package/lib/git-root.js +90 -0
- package/lib/infrastructure-chore-generator.js +45 -0
- package/lib/install-hooks.js +52 -0
- package/lib/jettypod-backup.js +238 -0
- package/lib/merge-lock.js +193 -0
- package/lib/migrations/012-add-worktree-path.js +38 -0
- package/lib/migrations/013-worktrees-table.js +86 -0
- package/lib/migrations/014-migrate-worktree-data.js +161 -0
- package/lib/migrations/015-merge-locks-table.js +67 -0
- package/lib/pattern-finder.js +152 -0
- package/lib/process-manager.js +140 -0
- package/lib/production-standards-reader.js +13 -2
- package/lib/production-standards-writer.js +85 -0
- package/lib/skills/feature-planning/dry-run-validator.js +135 -0
- package/lib/skills/feature-planning/validation-formatter.js +160 -0
- package/lib/smart-conflict-detection.js +168 -0
- package/lib/smart-fetch-rebase.js +614 -0
- package/lib/step-definition-parser.js +76 -0
- package/lib/unit-test-generator.js +232 -0
- package/lib/verification-command-generator.js +66 -0
- package/lib/worktree-diagnostics.js +413 -0
- package/lib/worktree-facade.js +174 -0
- package/lib/worktree-manager.js +636 -0
- package/lib/worktree-reconciler.js +429 -0
- package/package.json +30 -3
- package/skills-templates/external-transition/SKILL.md +34 -3
- package/skills-templates/feature-planning/SKILL.md +190 -24
- package/skills-templates/production-mode/SKILL.md +127 -9
- package/skills-templates/speed-mode/SKILL.md +454 -51
- package/skills-templates/stable-mode/SKILL.md +285 -76
- package/.claude/PROTECT_SKILLS.md +0 -28
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -16
- package/.claude/skills/epic-planning/SKILL.md +0 -297
- package/.claude/skills/external-transition/SKILL.md +0 -384
- package/.claude/skills/feature-planning/SKILL.md +0 -464
- package/.claude/skills/production-mode/SKILL.md +0 -369
- package/.claude/skills/speed-mode/SKILL.md +0 -481
- package/.claude/skills/stable-mode/SKILL.md +0 -713
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
- package/.devpod/current-work.json +0 -10
- package/.devpod/work.db +0 -0
- package/.github/workflows/test-safety.yml +0 -85
- package/.jettypod/config.json +0 -5
- package/.jettypod/current-work.json +0 -10
- package/.jettypod/hooks/README.md +0 -77
- package/.jettypod/hooks/protect-claude-md.js +0 -338
- package/.jettypod/test-work.db +0 -0
- package/.jettypod/work.db +0 -0
- package/CLAUDE.md +0 -49
- package/SPEED-STABLE-AUDIT.md +0 -853
- package/SYSTEM-BEHAVIOR.md +0 -2199
- package/TEST_SAFETY_AUDIT.md +0 -314
- package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
- package/cucumber-report.html +0 -45
- package/dist/devpod-linux +0 -0
- package/dist/devpod-macos +0 -0
- package/dist/devpod-win.exe +0 -0
- package/docs/features/jettypod-standards-explained.md +0 -543
- package/docs/features/standards-inventory.md +0 -257
- package/features/auto-generate-production-chores.feature +0 -13
- package/features/backlog-command.feature +0 -26
- package/features/backlog-filtering-production.feature +0 -10
- package/features/claude-md-protection/steps.js +0 -498
- package/features/decisions/index.js +0 -490
- package/features/decisions/index.test.js +0 -208
- package/features/fix-text-wrapping.feature +0 -42
- package/features/git-hooks/git-hooks.feature +0 -30
- package/features/git-hooks/index.js +0 -93
- package/features/git-hooks/index.test.js +0 -137
- package/features/git-hooks/post-commit +0 -56
- package/features/git-hooks/post-merge +0 -47
- package/features/git-hooks/pre-commit +0 -28
- package/features/git-hooks/simple-steps.js +0 -53
- package/features/git-hooks/simple-test.feature +0 -10
- package/features/git-hooks/steps.js +0 -196
- package/features/jettypod-update-command.feature +0 -46
- package/features/mode-prompts/index.js +0 -95
- package/features/mode-prompts/simple-steps.js +0 -44
- package/features/mode-prompts/simple-test.feature +0 -9
- package/features/mode-prompts/validation.test.js +0 -120
- package/features/multiple-claude-instances.feature +0 -121
- package/features/production-mode-skill.feature +0 -121
- package/features/refactor-mode/steps.js +0 -217
- package/features/refactor-mode.feature +0 -49
- package/features/simplify-external-transition.feature +0 -166
- package/features/skills-update/index.test.js +0 -216
- package/features/step_definitions/backlog-command.steps.js +0 -37
- package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
- package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
- package/features/step_definitions/production-mode-skill.steps.js +0 -862
- package/features/step_definitions/simplify-external-transition.steps.js +0 -370
- package/features/step_definitions/terminal-logo.steps.js +0 -145
- package/features/step_definitions/update-command.steps.js +0 -183
- package/features/support/hooks.js +0 -9
- package/features/terminal-logo/index.js +0 -39
- package/features/terminal-logo/terminal-logo.feature +0 -30
- package/features/update-command/index.js +0 -181
- package/features/update-command/index.test.js +0 -225
- package/features/work-commands/bug-workflow-display.feature +0 -22
- package/features/work-commands/index.js +0 -498
- package/features/work-commands/simple-steps.js +0 -69
- package/features/work-commands/stable-tests.feature +0 -57
- package/features/work-commands/steps.js +0 -1174
- package/features/work-commands/validation.test.js +0 -88
- package/features/work-commands/work-commands.feature +0 -13
- package/features/work-tracking/discovery-validation.test.js +0 -228
- package/features/work-tracking/index.js +0 -1921
- package/features/work-tracking/mode-required.feature +0 -112
- package/features/work-tracking/phase-tracking.test.js +0 -482
- package/features/work-tracking/prototype-tracking.test.js +0 -485
- package/features/work-tracking/tree-view.test.js +0 -310
- package/features/work-tracking/work-set-mode.feature +0 -71
- package/features/work-tracking/work-start-mode.feature +0 -88
- package/full-test.txt +0 -0
- package/lib/bug-workflow.test.js +0 -177
- package/lib/claudemd.test.js +0 -195
- package/lib/config.test.js +0 -511
- package/lib/constants.test.js +0 -164
- package/lib/current-work.test.js +0 -146
- package/lib/database-project-config.test.js +0 -111
- package/lib/database.test.js +0 -106
- package/lib/decisions-generator.test.js +0 -457
- package/lib/decisions-helpers.test.js +0 -310
- package/lib/git-coordinator.js +0 -167
- package/lib/git.test.js +0 -145
- package/lib/migrations/002-default-work-item-modes.test.js +0 -351
- package/lib/production-chore-generator.test.js +0 -432
- package/lib/production-context-detector.test.js +0 -277
- package/lib/production-scenario-appender.test.js +0 -235
- package/lib/production-scenario-validator.test.js +0 -246
- package/lib/production-standards-reader.test.js +0 -270
- package/lib/project-state.test.js +0 -92
- package/lib/push-queue.js +0 -417
- package/lib/queue-processor.js +0 -74
- package/lib/test-helpers.js +0 -202
- package/lib/test-helpers.test.js +0 -255
- package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
- package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
- package/prototypes/2025-01-11-production-mode-guided.js +0 -217
- package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
- package/prototypes/2025-01-11-production-standards-example.md +0 -204
- package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
- package/prototypes/test/index.html +0 -1
- package/setup-dist-repo.sh +0 -68
- package/test-production-standards-engine.js +0 -130
- package/test-results.json +0 -2195
- package/test-safety-check.sh +0 -80
- package/work-item-tracking-plan.md +0 -199
- /package/{.jettypod/devpod.db → jettypod.db} +0 -0
package/lib/claudemd.test.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { createTestEnvironment } = require('./test-helpers');
|
|
4
|
-
const { updateCurrentWork, getClaudeMdPath } = require('./claudemd');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
|
|
7
|
-
describe('CLAUDE.md Updater Module', () => {
|
|
8
|
-
let testEnv;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
testEnv = createTestEnvironment();
|
|
12
|
-
process.chdir(testEnv.testDir);
|
|
13
|
-
|
|
14
|
-
// Initialize config
|
|
15
|
-
const jettypodDir = path.join(testEnv.testDir, '.jettypod');
|
|
16
|
-
fs.mkdirSync(jettypodDir);
|
|
17
|
-
config.write({ mode: 'speed', stage: 'starting' });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
testEnv.cleanup();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const validWorkItem = {
|
|
25
|
-
id: 1,
|
|
26
|
-
title: 'Test Feature',
|
|
27
|
-
type: 'feature',
|
|
28
|
-
status: 'in_progress',
|
|
29
|
-
epic_id: 10,
|
|
30
|
-
epic_title: 'Test Epic'
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe('getClaudeMdPath()', () => {
|
|
34
|
-
test('should return path to CLAUDE.md in current directory', () => {
|
|
35
|
-
const claudePath = getClaudeMdPath();
|
|
36
|
-
expect(claudePath).toContain('CLAUDE.md');
|
|
37
|
-
expect(claudePath).toBe(path.join(process.cwd(), 'CLAUDE.md'));
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('updateCurrentWork()', () => {
|
|
42
|
-
const basicClaudeContent = `<claude_context project="test">
|
|
43
|
-
<project_summary>Test project</project_summary>
|
|
44
|
-
</claude_context>`;
|
|
45
|
-
|
|
46
|
-
test('should do nothing if CLAUDE.md does not exist', () => {
|
|
47
|
-
expect(() => updateCurrentWork(validWorkItem)).not.toThrow();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('should add current_work section if it does not exist', () => {
|
|
51
|
-
fs.writeFileSync('CLAUDE.md', basicClaudeContent);
|
|
52
|
-
|
|
53
|
-
updateCurrentWork(validWorkItem);
|
|
54
|
-
|
|
55
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
56
|
-
expect(content).toContain('<current_work>');
|
|
57
|
-
expect(content).toContain('Working on: [#1] Test Feature (feature)');
|
|
58
|
-
expect(content).toContain('Epic: [#10] Test Epic');
|
|
59
|
-
expect(content).toContain('Mode: speed');
|
|
60
|
-
expect(content).toContain('Status: in_progress');
|
|
61
|
-
expect(content).toContain('</current_work>');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('should replace existing current_work section', () => {
|
|
65
|
-
const existingContent = `<claude_context project="test">
|
|
66
|
-
<current_work>
|
|
67
|
-
Working on: [#999] Old Feature (bug)
|
|
68
|
-
Status: done
|
|
69
|
-
</current_work>
|
|
70
|
-
<project_summary>Test project</project_summary>
|
|
71
|
-
</claude_context>`;
|
|
72
|
-
|
|
73
|
-
fs.writeFileSync('CLAUDE.md', existingContent);
|
|
74
|
-
|
|
75
|
-
updateCurrentWork(validWorkItem);
|
|
76
|
-
|
|
77
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
78
|
-
expect(content).not.toContain('[#999] Old Feature');
|
|
79
|
-
expect(content).toContain('[#1] Test Feature');
|
|
80
|
-
expect(content).toContain('Status: in_progress');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('should not include epic if work item is the epic itself', () => {
|
|
84
|
-
const epicWorkItem = {
|
|
85
|
-
id: 10,
|
|
86
|
-
title: 'Test Epic',
|
|
87
|
-
type: 'epic',
|
|
88
|
-
status: 'in_progress',
|
|
89
|
-
epic_id: 10,
|
|
90
|
-
epic_title: 'Test Epic'
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
fs.writeFileSync('CLAUDE.md', basicClaudeContent);
|
|
94
|
-
|
|
95
|
-
updateCurrentWork(epicWorkItem);
|
|
96
|
-
|
|
97
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
98
|
-
expect(content).not.toContain('Epic: [#10]');
|
|
99
|
-
expect(content).toContain('[#10] Test Epic (epic)');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('should not include epic if epic_title is missing', () => {
|
|
103
|
-
const workItemNoEpic = {
|
|
104
|
-
id: 1,
|
|
105
|
-
title: 'Test Feature',
|
|
106
|
-
type: 'feature',
|
|
107
|
-
status: 'in_progress'
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
fs.writeFileSync('CLAUDE.md', basicClaudeContent);
|
|
111
|
-
|
|
112
|
-
updateCurrentWork(workItemNoEpic);
|
|
113
|
-
|
|
114
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
115
|
-
expect(content).not.toContain('Epic:');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('should use mode from config', () => {
|
|
119
|
-
config.write({ mode: 'stable', stage: 'building' });
|
|
120
|
-
fs.writeFileSync('CLAUDE.md', basicClaudeContent);
|
|
121
|
-
|
|
122
|
-
updateCurrentWork(validWorkItem);
|
|
123
|
-
|
|
124
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
125
|
-
expect(content).toContain('Mode: stable');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('should use mode from config even if malformed', () => {
|
|
129
|
-
// Config module caches config, so we just verify it uses whatever mode is set
|
|
130
|
-
const currentConfig = config.read();
|
|
131
|
-
fs.writeFileSync('CLAUDE.md', basicClaudeContent);
|
|
132
|
-
|
|
133
|
-
updateCurrentWork(validWorkItem);
|
|
134
|
-
|
|
135
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
136
|
-
expect(content).toContain(`Mode: ${currentConfig.mode || 'speed'}`);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('should throw error for invalid work item - not an object', () => {
|
|
140
|
-
expect(() => updateCurrentWork(null)).toThrow('Current work must be an object');
|
|
141
|
-
expect(() => updateCurrentWork('string')).toThrow('Current work must be an object');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('should throw error for missing id', () => {
|
|
145
|
-
const invalid = { ...validWorkItem, id: null };
|
|
146
|
-
expect(() => updateCurrentWork(invalid)).toThrow('Current work must have a numeric id');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('should throw error for non-numeric id', () => {
|
|
150
|
-
const invalid = { ...validWorkItem, id: 'not-a-number' };
|
|
151
|
-
expect(() => updateCurrentWork(invalid)).toThrow('Current work must have a numeric id');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('should throw error for missing title', () => {
|
|
155
|
-
const invalid = { ...validWorkItem, title: null };
|
|
156
|
-
expect(() => updateCurrentWork(invalid)).toThrow('Current work must have a string title');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('should throw error for missing type', () => {
|
|
160
|
-
const invalid = { ...validWorkItem, type: null };
|
|
161
|
-
expect(() => updateCurrentWork(invalid)).toThrow('Current work must have a string type');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test('should throw error for missing status', () => {
|
|
165
|
-
const invalid = { ...validWorkItem, status: null };
|
|
166
|
-
expect(() => updateCurrentWork(invalid)).toThrow('Current work must have a string status');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('should throw error if CLAUDE.md does not contain claude_context tag', () => {
|
|
170
|
-
fs.writeFileSync('CLAUDE.md', 'Just plain text, no tags');
|
|
171
|
-
|
|
172
|
-
expect(() => updateCurrentWork(validWorkItem)).toThrow('CLAUDE.md does not contain <claude_context> tag');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('should preserve other sections in CLAUDE.md', () => {
|
|
176
|
-
const fullContent = `<claude_context project="test">
|
|
177
|
-
<current_work>
|
|
178
|
-
Working on: [#999] Old Feature (bug)
|
|
179
|
-
</current_work>
|
|
180
|
-
<project_summary>Test project</project_summary>
|
|
181
|
-
<tech_stack>Node.js, Jest</tech_stack>
|
|
182
|
-
<mode>speed</mode>
|
|
183
|
-
</claude_context>`;
|
|
184
|
-
|
|
185
|
-
fs.writeFileSync('CLAUDE.md', fullContent);
|
|
186
|
-
|
|
187
|
-
updateCurrentWork(validWorkItem);
|
|
188
|
-
|
|
189
|
-
const content = fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
190
|
-
expect(content).toContain('<project_summary>Test project</project_summary>');
|
|
191
|
-
expect(content).toContain('<tech_stack>Node.js, Jest</tech_stack>');
|
|
192
|
-
expect(content).toContain('<mode>speed</mode>');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
});
|
package/lib/config.test.js
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const config = require('./config');
|
|
4
|
-
const { createTestEnvironment } = require('./test-helpers');
|
|
5
|
-
|
|
6
|
-
describe('Config Module', () => {
|
|
7
|
-
let testEnv;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
testEnv = createTestEnvironment();
|
|
11
|
-
process.chdir(testEnv.testDir);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
testEnv.cleanup();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('read()', () => {
|
|
19
|
-
test('should return default config if config file does not exist', () => {
|
|
20
|
-
const result = config.read();
|
|
21
|
-
expect(result).toEqual({
|
|
22
|
-
name: path.basename(testEnv.testDir),
|
|
23
|
-
stage: 'empty',
|
|
24
|
-
bundles: ['core'],
|
|
25
|
-
project_state: 'external',
|
|
26
|
-
project_discovery: {
|
|
27
|
-
status: 'not_started',
|
|
28
|
-
prototypes: [],
|
|
29
|
-
winner: null,
|
|
30
|
-
rationale: null,
|
|
31
|
-
started_date: null,
|
|
32
|
-
completed_date: null,
|
|
33
|
-
checkpoint: {
|
|
34
|
-
step: 1,
|
|
35
|
-
user_journey: null,
|
|
36
|
-
ux_approach: null,
|
|
37
|
-
epics_created: false
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('should read existing config file', () => {
|
|
44
|
-
const testConfig = { name: 'test-project', mode: 'speed' };
|
|
45
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
46
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
47
|
-
|
|
48
|
-
const result = config.read();
|
|
49
|
-
expect(result).toEqual({
|
|
50
|
-
...testConfig,
|
|
51
|
-
project_state: 'external',
|
|
52
|
-
project_discovery: {
|
|
53
|
-
status: 'not_started',
|
|
54
|
-
prototypes: [],
|
|
55
|
-
winner: null,
|
|
56
|
-
rationale: null,
|
|
57
|
-
started_date: null,
|
|
58
|
-
completed_date: null,
|
|
59
|
-
checkpoint: {
|
|
60
|
-
step: 1,
|
|
61
|
-
user_journey: null,
|
|
62
|
-
ux_approach: null,
|
|
63
|
-
epics_created: false
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('should handle malformed JSON gracefully', () => {
|
|
70
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
71
|
-
fs.writeFileSync('.jettypod/config.json', 'not valid json');
|
|
72
|
-
|
|
73
|
-
const result = config.read();
|
|
74
|
-
expect(result).toEqual({
|
|
75
|
-
name: path.basename(testEnv.testDir),
|
|
76
|
-
stage: 'empty',
|
|
77
|
-
bundles: ['core'],
|
|
78
|
-
project_state: 'external',
|
|
79
|
-
project_discovery: {
|
|
80
|
-
status: 'not_started',
|
|
81
|
-
prototypes: [],
|
|
82
|
-
winner: null,
|
|
83
|
-
rationale: null,
|
|
84
|
-
started_date: null,
|
|
85
|
-
completed_date: null,
|
|
86
|
-
checkpoint: {
|
|
87
|
-
step: 1,
|
|
88
|
-
user_journey: null,
|
|
89
|
-
ux_approach: null,
|
|
90
|
-
epics_created: false
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('write()', () => {
|
|
98
|
-
test('should create .jettypod directory if it does not exist', () => {
|
|
99
|
-
const testConfig = { name: 'test-project' };
|
|
100
|
-
|
|
101
|
-
config.write(testConfig);
|
|
102
|
-
|
|
103
|
-
expect(fs.existsSync('.jettypod')).toBe(true);
|
|
104
|
-
expect(fs.existsSync('.jettypod/config.json')).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('should write config as formatted JSON', () => {
|
|
108
|
-
const testConfig = { name: 'test-project', mode: 'speed' };
|
|
109
|
-
|
|
110
|
-
config.write(testConfig);
|
|
111
|
-
|
|
112
|
-
const written = fs.readFileSync('.jettypod/config.json', 'utf-8');
|
|
113
|
-
expect(JSON.parse(written)).toEqual(testConfig);
|
|
114
|
-
// Check it's formatted (has newlines and indentation)
|
|
115
|
-
expect(written).toContain('\n');
|
|
116
|
-
expect(written).toContain(' ');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('update()', () => {
|
|
121
|
-
test('should merge updates with existing config', () => {
|
|
122
|
-
const initial = { name: 'test-project', mode: 'discovery' };
|
|
123
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
124
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(initial));
|
|
125
|
-
|
|
126
|
-
config.update({ mode: 'speed', stage: 'growing' });
|
|
127
|
-
|
|
128
|
-
const result = config.read();
|
|
129
|
-
expect(result).toEqual({
|
|
130
|
-
name: 'test-project',
|
|
131
|
-
mode: 'speed',
|
|
132
|
-
stage: 'growing',
|
|
133
|
-
project_state: 'external',
|
|
134
|
-
project_discovery: {
|
|
135
|
-
status: 'not_started',
|
|
136
|
-
prototypes: [],
|
|
137
|
-
winner: null,
|
|
138
|
-
rationale: null,
|
|
139
|
-
started_date: null,
|
|
140
|
-
completed_date: null,
|
|
141
|
-
checkpoint: {
|
|
142
|
-
step: 1,
|
|
143
|
-
user_journey: null,
|
|
144
|
-
ux_approach: null,
|
|
145
|
-
epics_created: false
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('should create config if it does not exist', () => {
|
|
152
|
-
config.update({ mode: 'speed' });
|
|
153
|
-
|
|
154
|
-
const result = config.read();
|
|
155
|
-
expect(result).toEqual({
|
|
156
|
-
name: path.basename(testEnv.testDir),
|
|
157
|
-
mode: 'speed', // Updated value
|
|
158
|
-
stage: 'empty', // Default value
|
|
159
|
-
bundles: ['core'], // Default value
|
|
160
|
-
project_state: 'external', // Default value
|
|
161
|
-
project_discovery: {
|
|
162
|
-
status: 'not_started',
|
|
163
|
-
prototypes: [],
|
|
164
|
-
winner: null,
|
|
165
|
-
rationale: null,
|
|
166
|
-
started_date: null,
|
|
167
|
-
completed_date: null,
|
|
168
|
-
checkpoint: {
|
|
169
|
-
step: 1,
|
|
170
|
-
user_journey: null,
|
|
171
|
-
ux_approach: null,
|
|
172
|
-
epics_created: false
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe('exists()', () => {
|
|
180
|
-
test('should return false if config does not exist', () => {
|
|
181
|
-
expect(config.exists()).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('should return true if config exists', () => {
|
|
185
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
186
|
-
fs.writeFileSync('.jettypod/config.json', '{}');
|
|
187
|
-
|
|
188
|
-
expect(config.exists()).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('project_discovery', () => {
|
|
193
|
-
test('should include default project_discovery in new config', () => {
|
|
194
|
-
const result = config.read();
|
|
195
|
-
expect(result.project_discovery).toEqual({
|
|
196
|
-
status: 'not_started',
|
|
197
|
-
prototypes: [],
|
|
198
|
-
winner: null,
|
|
199
|
-
rationale: null,
|
|
200
|
-
started_date: null,
|
|
201
|
-
completed_date: null,
|
|
202
|
-
checkpoint: {
|
|
203
|
-
step: 1,
|
|
204
|
-
user_journey: null,
|
|
205
|
-
ux_approach: null,
|
|
206
|
-
epics_created: false
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('should add project_discovery to existing config without it', () => {
|
|
212
|
-
const testConfig = { name: 'test-project' };
|
|
213
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
214
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
215
|
-
|
|
216
|
-
const result = config.read();
|
|
217
|
-
expect(result.project_discovery).toBeDefined();
|
|
218
|
-
expect(result.project_discovery.status).toBe('not_started');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('should preserve existing project_discovery data', () => {
|
|
222
|
-
const testConfig = {
|
|
223
|
-
name: 'test-project',
|
|
224
|
-
project_discovery: {
|
|
225
|
-
status: 'in_progress',
|
|
226
|
-
prototypes: ['proto1', 'proto2'],
|
|
227
|
-
winner: null,
|
|
228
|
-
rationale: null,
|
|
229
|
-
started_date: '2025-10-29',
|
|
230
|
-
completed_date: null,
|
|
231
|
-
checkpoint: {
|
|
232
|
-
step: 3,
|
|
233
|
-
user_journey: 'test journey',
|
|
234
|
-
ux_approach: 'test approach',
|
|
235
|
-
epics_created: false
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
240
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
241
|
-
|
|
242
|
-
const result = config.read();
|
|
243
|
-
expect(result.project_discovery).toEqual(testConfig.project_discovery);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('should fix invalid discovery status', () => {
|
|
247
|
-
const testConfig = {
|
|
248
|
-
name: 'test-project',
|
|
249
|
-
project_discovery: {
|
|
250
|
-
status: 'invalid_status',
|
|
251
|
-
prototypes: [],
|
|
252
|
-
winner: null,
|
|
253
|
-
rationale: null,
|
|
254
|
-
started_date: null,
|
|
255
|
-
completed_date: null
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
259
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
260
|
-
|
|
261
|
-
const result = config.read();
|
|
262
|
-
expect(result.project_discovery.status).toBe('not_started');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('should validate discovery status', () => {
|
|
266
|
-
expect(config.isValidDiscoveryStatus('not_started')).toBe(true);
|
|
267
|
-
expect(config.isValidDiscoveryStatus('in_progress')).toBe(true);
|
|
268
|
-
expect(config.isValidDiscoveryStatus('completed')).toBe(true);
|
|
269
|
-
expect(config.isValidDiscoveryStatus('invalid')).toBe(false);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test('should update project_discovery via update()', () => {
|
|
273
|
-
config.update({
|
|
274
|
-
project_discovery: {
|
|
275
|
-
status: 'in_progress',
|
|
276
|
-
prototypes: ['prototype1'],
|
|
277
|
-
winner: null,
|
|
278
|
-
rationale: null,
|
|
279
|
-
started_date: '2025-10-29T10:00:00Z',
|
|
280
|
-
completed_date: null
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const result = config.read();
|
|
285
|
-
expect(result.project_discovery.status).toBe('in_progress');
|
|
286
|
-
expect(result.project_discovery.prototypes).toEqual(['prototype1']);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
describe('project_discovery edge cases', () => {
|
|
291
|
-
test('should handle project_discovery as array (not object)', () => {
|
|
292
|
-
const testConfig = {
|
|
293
|
-
name: 'test-project',
|
|
294
|
-
project_discovery: [] // Invalid: array not object
|
|
295
|
-
};
|
|
296
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
297
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
298
|
-
|
|
299
|
-
const result = config.read();
|
|
300
|
-
expect(result.project_discovery).toEqual(config.getDefaultProjectDiscovery());
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test('should handle project_discovery with prototypes as non-array', () => {
|
|
304
|
-
const testConfig = {
|
|
305
|
-
name: 'test-project',
|
|
306
|
-
project_discovery: {
|
|
307
|
-
status: 'in_progress',
|
|
308
|
-
prototypes: { bad: 'value' }, // Invalid: object not array
|
|
309
|
-
winner: null,
|
|
310
|
-
rationale: null
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
314
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
315
|
-
|
|
316
|
-
const result = config.read();
|
|
317
|
-
expect(Array.isArray(result.project_discovery.prototypes)).toBe(true);
|
|
318
|
-
expect(result.project_discovery.prototypes).toEqual([]);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test('should handle project_discovery with winner as number', () => {
|
|
322
|
-
const testConfig = {
|
|
323
|
-
name: 'test-project',
|
|
324
|
-
project_discovery: {
|
|
325
|
-
status: 'completed',
|
|
326
|
-
prototypes: [],
|
|
327
|
-
winner: 123, // Invalid: number not string
|
|
328
|
-
rationale: 'test'
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
332
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
333
|
-
|
|
334
|
-
const result = config.read();
|
|
335
|
-
expect(result.project_discovery.winner).toBe(null);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test('should handle project_discovery with rationale as number', () => {
|
|
339
|
-
const testConfig = {
|
|
340
|
-
name: 'test-project',
|
|
341
|
-
project_discovery: {
|
|
342
|
-
status: 'completed',
|
|
343
|
-
prototypes: [],
|
|
344
|
-
winner: 'proto.js',
|
|
345
|
-
rationale: 456 // Invalid: number not string
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
349
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
350
|
-
|
|
351
|
-
const result = config.read();
|
|
352
|
-
expect(result.project_discovery.rationale).toBe(null);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('should handle project_discovery with invalid date types', () => {
|
|
356
|
-
const testConfig = {
|
|
357
|
-
name: 'test-project',
|
|
358
|
-
project_discovery: {
|
|
359
|
-
status: 'in_progress',
|
|
360
|
-
prototypes: [],
|
|
361
|
-
winner: null,
|
|
362
|
-
rationale: null,
|
|
363
|
-
started_date: 12345, // Invalid: number not string
|
|
364
|
-
completed_date: true // Invalid: boolean not string
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
fs.mkdirSync('.jettypod', { recursive: true });
|
|
368
|
-
fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
|
|
369
|
-
|
|
370
|
-
const result = config.read();
|
|
371
|
-
expect(result.project_discovery.started_date).toBe(null);
|
|
372
|
-
expect(result.project_discovery.completed_date).toBe(null);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe('project_discovery update() validation', () => {
|
|
377
|
-
test('should reject project_discovery as non-object', () => {
|
|
378
|
-
expect(() => {
|
|
379
|
-
config.update({ project_discovery: 'not an object' });
|
|
380
|
-
}).toThrow('must be an object');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('should reject project_discovery as array', () => {
|
|
384
|
-
expect(() => {
|
|
385
|
-
config.update({ project_discovery: [] });
|
|
386
|
-
}).toThrow('must be an object');
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
test('should reject invalid discovery status', () => {
|
|
390
|
-
expect(() => {
|
|
391
|
-
config.update({
|
|
392
|
-
project_discovery: {
|
|
393
|
-
status: 'invalid_status',
|
|
394
|
-
prototypes: [],
|
|
395
|
-
winner: null,
|
|
396
|
-
rationale: null
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}).toThrow("Must be 'not_started', 'in_progress', or 'completed'");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test('should reject prototypes as non-array', () => {
|
|
403
|
-
expect(() => {
|
|
404
|
-
config.update({
|
|
405
|
-
project_discovery: {
|
|
406
|
-
status: 'in_progress',
|
|
407
|
-
prototypes: 'not an array',
|
|
408
|
-
winner: null,
|
|
409
|
-
rationale: null
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
}).toThrow('prototypes: must be an array');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test('should reject winner as non-string (when not null)', () => {
|
|
416
|
-
expect(() => {
|
|
417
|
-
config.update({
|
|
418
|
-
project_discovery: {
|
|
419
|
-
status: 'completed',
|
|
420
|
-
prototypes: [],
|
|
421
|
-
winner: 123,
|
|
422
|
-
rationale: 'test'
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
}).toThrow('winner: must be a string or null');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test('should reject rationale as non-string (when not null)', () => {
|
|
429
|
-
expect(() => {
|
|
430
|
-
config.update({
|
|
431
|
-
project_discovery: {
|
|
432
|
-
status: 'completed',
|
|
433
|
-
prototypes: [],
|
|
434
|
-
winner: 'proto.js',
|
|
435
|
-
rationale: 456
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}).toThrow('rationale: must be a string or null');
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
test('should accept valid project_discovery update', () => {
|
|
442
|
-
expect(() => {
|
|
443
|
-
config.update({
|
|
444
|
-
project_discovery: {
|
|
445
|
-
status: 'in_progress',
|
|
446
|
-
prototypes: ['proto1.js', 'proto2.js'],
|
|
447
|
-
winner: null,
|
|
448
|
-
rationale: null,
|
|
449
|
-
started_date: '2025-10-29T10:00:00Z',
|
|
450
|
-
completed_date: null
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
}).not.toThrow();
|
|
454
|
-
|
|
455
|
-
const result = config.read();
|
|
456
|
-
expect(result.project_discovery.status).toBe('in_progress');
|
|
457
|
-
expect(result.project_discovery.prototypes).toHaveLength(2);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
describe('project_discovery workflow', () => {
|
|
462
|
-
test('should handle complete discovery workflow', () => {
|
|
463
|
-
// Start discovery
|
|
464
|
-
config.update({
|
|
465
|
-
project_discovery: {
|
|
466
|
-
status: 'in_progress',
|
|
467
|
-
prototypes: [],
|
|
468
|
-
winner: null,
|
|
469
|
-
rationale: null,
|
|
470
|
-
started_date: '2025-10-29T10:00:00Z',
|
|
471
|
-
completed_date: null
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
let result = config.read();
|
|
476
|
-
expect(result.project_discovery.status).toBe('in_progress');
|
|
477
|
-
|
|
478
|
-
// Add prototypes
|
|
479
|
-
config.update({
|
|
480
|
-
project_discovery: {
|
|
481
|
-
status: 'in_progress',
|
|
482
|
-
prototypes: ['proto1.js', 'proto2.js', 'proto3.js'],
|
|
483
|
-
winner: null,
|
|
484
|
-
rationale: null,
|
|
485
|
-
started_date: '2025-10-29T10:00:00Z',
|
|
486
|
-
completed_date: null
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
result = config.read();
|
|
491
|
-
expect(result.project_discovery.prototypes).toHaveLength(3);
|
|
492
|
-
|
|
493
|
-
// Complete discovery
|
|
494
|
-
config.update({
|
|
495
|
-
project_discovery: {
|
|
496
|
-
status: 'completed',
|
|
497
|
-
prototypes: ['proto1.js', 'proto2.js', 'proto3.js'],
|
|
498
|
-
winner: 'proto2.js',
|
|
499
|
-
rationale: 'Balanced approach won - best mix of features and simplicity',
|
|
500
|
-
started_date: '2025-10-29T10:00:00Z',
|
|
501
|
-
completed_date: '2025-10-29T15:30:00Z'
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
result = config.read();
|
|
506
|
-
expect(result.project_discovery.status).toBe('completed');
|
|
507
|
-
expect(result.project_discovery.winner).toBe('proto2.js');
|
|
508
|
-
expect(result.project_discovery.rationale).toContain('Balanced approach');
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
});
|