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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit test generator
|
|
3
|
+
* Automatically scaffolds and generates Jest unit tests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scaffold empty unit test file with describe blocks
|
|
11
|
+
* @param {string} jsFilePath - Path to JS file to create tests for
|
|
12
|
+
* @returns {string} Path to created test file
|
|
13
|
+
*/
|
|
14
|
+
function scaffoldUnitTestFile(jsFilePath) {
|
|
15
|
+
const testFilePath = getTestFilePath(jsFilePath);
|
|
16
|
+
|
|
17
|
+
// Don't overwrite existing test files
|
|
18
|
+
if (fs.existsSync(testFilePath)) {
|
|
19
|
+
return testFilePath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fileName = path.basename(jsFilePath);
|
|
23
|
+
const moduleName = path.basename(jsFilePath, '.js');
|
|
24
|
+
|
|
25
|
+
const template = `/**
|
|
26
|
+
* Unit tests for ${fileName}
|
|
27
|
+
* Auto-generated by JettyPod unit-test-generator
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
describe('${moduleName}', () => {
|
|
31
|
+
// TODO: Add test cases
|
|
32
|
+
|
|
33
|
+
it('should be implemented', () => {
|
|
34
|
+
expect(true).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// Ensure test directory exists
|
|
40
|
+
const testDir = path.dirname(testFilePath);
|
|
41
|
+
if (!fs.existsSync(testDir)) {
|
|
42
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(testFilePath, template, 'utf8');
|
|
46
|
+
return testFilePath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate tests for functions in a JS file
|
|
51
|
+
* @param {string} jsFilePath - Path to JS file
|
|
52
|
+
* @returns {string} Generated test code
|
|
53
|
+
*/
|
|
54
|
+
function generateTestsForFunctions(jsFilePath) {
|
|
55
|
+
const functions = extractFunctions(jsFilePath);
|
|
56
|
+
|
|
57
|
+
if (functions.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fileName = path.basename(jsFilePath);
|
|
62
|
+
const moduleName = path.basename(jsFilePath, '.js');
|
|
63
|
+
const relativePath = path.relative(path.join(process.cwd(), 'test'), jsFilePath);
|
|
64
|
+
|
|
65
|
+
let testCode = `/**
|
|
66
|
+
* Unit tests for ${fileName}
|
|
67
|
+
* Auto-generated by JettyPod unit-test-generator
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
const ${moduleName} = require('${relativePath.replace(/\\/g, '/')}');
|
|
71
|
+
|
|
72
|
+
describe('${moduleName}', () => {
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
for (const func of functions) {
|
|
76
|
+
testCode += `
|
|
77
|
+
describe('${func.name}()', () => {
|
|
78
|
+
it('should handle happy path', () => {
|
|
79
|
+
// TODO: Implement happy path test
|
|
80
|
+
expect(${moduleName}.${func.name}).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle error cases', () => {
|
|
84
|
+
// TODO: Implement error case tests
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle edge cases', () => {
|
|
88
|
+
// TODO: Implement edge case tests
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
testCode += `});
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
return testCode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract function names from JS file using regex
|
|
102
|
+
* @param {string} jsFilePath - Path to JS file
|
|
103
|
+
* @returns {Array<{name: string, line: number}>} Function info
|
|
104
|
+
*/
|
|
105
|
+
function extractFunctions(jsFilePath) {
|
|
106
|
+
if (!fs.existsSync(jsFilePath)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const content = fs.readFileSync(jsFilePath, 'utf8');
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
const functions = [];
|
|
113
|
+
|
|
114
|
+
// Match function declarations and expressions
|
|
115
|
+
const patterns = [
|
|
116
|
+
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/, // function name()
|
|
117
|
+
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*function/, // const name = function
|
|
118
|
+
/const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*\(/, // const name = ()
|
|
119
|
+
/async\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/, // async function name()
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
|
|
125
|
+
for (const pattern of patterns) {
|
|
126
|
+
const match = line.match(pattern);
|
|
127
|
+
if (match) {
|
|
128
|
+
functions.push({ name: match[1], line: i + 1 });
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return functions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get test file path for a JS file
|
|
139
|
+
* @param {string} jsFilePath - Path to JS file
|
|
140
|
+
* @returns {string} Path to test file
|
|
141
|
+
*/
|
|
142
|
+
function getTestFilePath(jsFilePath) {
|
|
143
|
+
const parsed = path.parse(jsFilePath);
|
|
144
|
+
return path.join('test', parsed.dir, `${parsed.name}.test.js`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if test file exists for a JS file
|
|
149
|
+
* @param {string} jsFilePath - Path to JS file
|
|
150
|
+
* @returns {string|null} Path to test file or null
|
|
151
|
+
*/
|
|
152
|
+
function getExistingTestFile(jsFilePath) {
|
|
153
|
+
const testPath = getTestFilePath(jsFilePath);
|
|
154
|
+
return fs.existsSync(testPath) ? testPath : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate or update unit tests for new functions after GREEN
|
|
159
|
+
* @param {string} jsFilePath - Path to JS file
|
|
160
|
+
* @returns {{created: boolean, testFile: string, functionsAdded: number}} Result
|
|
161
|
+
*/
|
|
162
|
+
function generateTestsAfterGreen(jsFilePath) {
|
|
163
|
+
const testFilePath = getTestFilePath(jsFilePath);
|
|
164
|
+
const functions = extractFunctions(jsFilePath);
|
|
165
|
+
|
|
166
|
+
if (functions.length === 0) {
|
|
167
|
+
return { created: false, testFile: null, functionsAdded: 0 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If test file doesn't exist, generate full test file
|
|
171
|
+
if (!fs.existsSync(testFilePath)) {
|
|
172
|
+
const testCode = generateTestsForFunctions(jsFilePath);
|
|
173
|
+
const testDir = path.dirname(testFilePath);
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(testDir)) {
|
|
176
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(testFilePath, testCode, 'utf8');
|
|
180
|
+
return { created: true, testFile: testFilePath, functionsAdded: functions.length };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Test file exists - check which functions need tests
|
|
184
|
+
const existingTests = fs.readFileSync(testFilePath, 'utf8');
|
|
185
|
+
const missingFunctions = functions.filter(func => {
|
|
186
|
+
const testPattern = new RegExp(`describe\\(['"\`]${func.name}\\(\\)['"\`]`);
|
|
187
|
+
return !testPattern.test(existingTests);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (missingFunctions.length === 0) {
|
|
191
|
+
return { created: false, testFile: testFilePath, functionsAdded: 0 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add tests for missing functions (append before closing brace)
|
|
195
|
+
const moduleName = path.basename(jsFilePath, '.js');
|
|
196
|
+
let additions = '';
|
|
197
|
+
|
|
198
|
+
for (const func of missingFunctions) {
|
|
199
|
+
additions += `
|
|
200
|
+
describe('${func.name}()', () => {
|
|
201
|
+
it('should handle happy path', () => {
|
|
202
|
+
// TODO: Implement happy path test
|
|
203
|
+
expect(${moduleName}.${func.name}).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle error cases', () => {
|
|
207
|
+
// TODO: Implement error case tests
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle edge cases', () => {
|
|
211
|
+
// TODO: Implement edge case tests
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Insert before final closing brace
|
|
218
|
+
const updatedTests = existingTests.replace(/}\);[\s]*$/, `${additions}});
|
|
219
|
+
`);
|
|
220
|
+
|
|
221
|
+
fs.writeFileSync(testFilePath, updatedTests, 'utf8');
|
|
222
|
+
return { created: false, testFile: testFilePath, functionsAdded: missingFunctions.length };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
scaffoldUnitTestFile,
|
|
227
|
+
generateTestsForFunctions,
|
|
228
|
+
generateTestsAfterGreen,
|
|
229
|
+
extractFunctions,
|
|
230
|
+
getTestFilePath,
|
|
231
|
+
getExistingTestFile
|
|
232
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate verification commands from scenario file and line number
|
|
5
|
+
* @param {string} scenarioFile - Path to the feature file
|
|
6
|
+
* @param {number} scenarioLine - Line number where the scenario starts
|
|
7
|
+
* @returns {{command: string, scenarioName: string}} Verification command and scenario name
|
|
8
|
+
*/
|
|
9
|
+
function generateVerificationCommands(scenarioFile, scenarioLine) {
|
|
10
|
+
// Handle missing scenario file
|
|
11
|
+
if (!fs.existsSync(scenarioFile)) {
|
|
12
|
+
console.warn(`⚠️ Scenario file not found: ${scenarioFile}`);
|
|
13
|
+
return {
|
|
14
|
+
command: '',
|
|
15
|
+
scenarioName: ''
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate scenarioLine is a positive number
|
|
20
|
+
if (!scenarioLine || scenarioLine < 1) {
|
|
21
|
+
console.warn(`⚠️ Invalid scenario line number: ${scenarioLine}`);
|
|
22
|
+
scenarioLine = 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
// Read the scenario file to extract scenario name
|
|
28
|
+
content = fs.readFileSync(scenarioFile, 'utf8');
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn(`⚠️ Cannot read scenario file ${scenarioFile}: ${err.message}`);
|
|
31
|
+
return {
|
|
32
|
+
command: '',
|
|
33
|
+
scenarioName: ''
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
|
|
39
|
+
// Handle line number beyond file length
|
|
40
|
+
if (scenarioLine > lines.length) {
|
|
41
|
+
console.warn(`⚠️ Line number ${scenarioLine} exceeds file length (${lines.length} lines). Using line 1 as fallback.`);
|
|
42
|
+
scenarioLine = 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Generate the test command with line number
|
|
46
|
+
const command = `npm run test:bdd -- ${scenarioFile}:${scenarioLine}`;
|
|
47
|
+
|
|
48
|
+
// Find the scenario name at the given line
|
|
49
|
+
let scenarioName = '';
|
|
50
|
+
for (let i = scenarioLine - 1; i < lines.length; i++) {
|
|
51
|
+
const line = lines[i].trim();
|
|
52
|
+
if (line.startsWith('Scenario:')) {
|
|
53
|
+
scenarioName = line.replace('Scenario:', '').trim();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
command,
|
|
60
|
+
scenarioName
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
generateVerificationCommands
|
|
66
|
+
};
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Diagnostics - Health Check and Issue Detection
|
|
3
|
+
*
|
|
4
|
+
* This module provides comprehensive diagnostic capabilities for worktree health
|
|
5
|
+
* and integrity. It detects issues, provides actionable recommendations, and
|
|
6
|
+
* generates diagnostic reports.
|
|
7
|
+
*
|
|
8
|
+
* Diagnostic categories:
|
|
9
|
+
* 1. Database integrity (orphaned entries, missing references)
|
|
10
|
+
* 2. Git state (orphaned worktrees, stale branches)
|
|
11
|
+
* 3. Filesystem state (missing directories, permission issues)
|
|
12
|
+
* 4. Configuration issues (missing .jettypod symlinks, incorrect permissions)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const worktreeReconciler = require('./worktree-reconciler');
|
|
16
|
+
const worktreeManager = require('./worktree-manager');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Issue severity levels
|
|
23
|
+
*/
|
|
24
|
+
const SEVERITY = {
|
|
25
|
+
CRITICAL: 'critical', // Prevents work from starting
|
|
26
|
+
HIGH: 'high', // May cause data loss or corruption
|
|
27
|
+
MEDIUM: 'medium', // May cause confusion or workflow issues
|
|
28
|
+
LOW: 'low', // Minor issues that don't affect functionality
|
|
29
|
+
INFO: 'info' // Informational, not an issue
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run comprehensive diagnostics on worktree state
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} db - SQLite database connection
|
|
36
|
+
* @param {string} repoPath - Path to main git repository
|
|
37
|
+
* @param {Object} options - Diagnostic options
|
|
38
|
+
* @param {string[]} options.checks - Specific checks to run (defaults to all)
|
|
39
|
+
* @param {string} options.minSeverity - Minimum severity to report (defaults to 'info')
|
|
40
|
+
* @returns {Promise<Object>} Diagnostic report with issues, recommendations, and summary
|
|
41
|
+
*/
|
|
42
|
+
async function runDiagnostics(db, repoPath, options = {}) {
|
|
43
|
+
const checks = options.checks || ['all'];
|
|
44
|
+
const minSeverity = options.minSeverity || 'info';
|
|
45
|
+
|
|
46
|
+
const report = {
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
repoPath: repoPath,
|
|
49
|
+
summary: {
|
|
50
|
+
totalIssues: 0,
|
|
51
|
+
critical: 0,
|
|
52
|
+
high: 0,
|
|
53
|
+
medium: 0,
|
|
54
|
+
low: 0,
|
|
55
|
+
info: 0
|
|
56
|
+
},
|
|
57
|
+
issues: [],
|
|
58
|
+
recommendations: [],
|
|
59
|
+
health: 'unknown' // 'healthy', 'warning', 'critical'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Run reconciliation to detect state issues
|
|
63
|
+
if (checks.includes('all') || checks.includes('state')) {
|
|
64
|
+
try {
|
|
65
|
+
const reconcileResults = await worktreeReconciler.reconcileState(db, repoPath, { cleanup: false });
|
|
66
|
+
|
|
67
|
+
// Orphaned worktrees
|
|
68
|
+
if (reconcileResults.orphanedWorktrees.length > 0) {
|
|
69
|
+
report.issues.push({
|
|
70
|
+
severity: SEVERITY.MEDIUM,
|
|
71
|
+
category: 'state',
|
|
72
|
+
type: 'orphaned_worktrees',
|
|
73
|
+
count: reconcileResults.orphanedWorktrees.length,
|
|
74
|
+
message: `Found ${reconcileResults.orphanedWorktrees.length} orphaned worktree(s) in git but not in database`,
|
|
75
|
+
details: reconcileResults.orphanedWorktrees,
|
|
76
|
+
recommendation: 'Run reconciliation with cleanup=true to remove orphaned worktrees'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Orphaned branches
|
|
81
|
+
if (reconcileResults.orphanedBranches.length > 0) {
|
|
82
|
+
report.issues.push({
|
|
83
|
+
severity: SEVERITY.LOW,
|
|
84
|
+
category: 'state',
|
|
85
|
+
type: 'orphaned_branches',
|
|
86
|
+
count: reconcileResults.orphanedBranches.length,
|
|
87
|
+
message: `Found ${reconcileResults.orphanedBranches.length} orphaned branch(es) with no worktree`,
|
|
88
|
+
details: reconcileResults.orphanedBranches,
|
|
89
|
+
recommendation: 'Run reconciliation with cleanup=true to delete empty branches'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Stale database entries
|
|
94
|
+
if (reconcileResults.staleDbEntries.length > 0) {
|
|
95
|
+
report.issues.push({
|
|
96
|
+
severity: SEVERITY.HIGH,
|
|
97
|
+
category: 'state',
|
|
98
|
+
type: 'stale_db_entries',
|
|
99
|
+
count: reconcileResults.staleDbEntries.length,
|
|
100
|
+
message: `Found ${reconcileResults.staleDbEntries.length} database entries with missing filesystem directories`,
|
|
101
|
+
details: reconcileResults.staleDbEntries,
|
|
102
|
+
recommendation: 'Run reconciliation with cleanup=true to mark as corrupted'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Git-filesystem mismatches
|
|
107
|
+
if (reconcileResults.gitFilesystemMismatches.length > 0) {
|
|
108
|
+
report.issues.push({
|
|
109
|
+
severity: SEVERITY.CRITICAL,
|
|
110
|
+
category: 'state',
|
|
111
|
+
type: 'git_filesystem_mismatch',
|
|
112
|
+
count: reconcileResults.gitFilesystemMismatches.length,
|
|
113
|
+
message: `Found ${reconcileResults.gitFilesystemMismatches.length} directories that exist but git doesn't track`,
|
|
114
|
+
details: reconcileResults.gitFilesystemMismatches,
|
|
115
|
+
recommendation: 'Run reconciliation with cleanup=true to re-register with git'
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} catch (err) {
|
|
120
|
+
report.issues.push({
|
|
121
|
+
severity: SEVERITY.CRITICAL,
|
|
122
|
+
category: 'state',
|
|
123
|
+
type: 'reconciliation_error',
|
|
124
|
+
message: `Failed to run state reconciliation: ${err.message}`,
|
|
125
|
+
details: { error: err.message, stack: err.stack },
|
|
126
|
+
recommendation: 'Check git repository integrity and database connection'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check database integrity
|
|
132
|
+
if (checks.includes('all') || checks.includes('database')) {
|
|
133
|
+
try {
|
|
134
|
+
const dbIssues = await checkDatabaseIntegrity(db);
|
|
135
|
+
report.issues.push(...dbIssues);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
report.issues.push({
|
|
138
|
+
severity: SEVERITY.CRITICAL,
|
|
139
|
+
category: 'database',
|
|
140
|
+
type: 'database_check_error',
|
|
141
|
+
message: `Failed to check database integrity: ${err.message}`,
|
|
142
|
+
details: { error: err.message, stack: err.stack },
|
|
143
|
+
recommendation: 'Check database file permissions and integrity'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check filesystem health
|
|
149
|
+
if (checks.includes('all') || checks.includes('filesystem')) {
|
|
150
|
+
try {
|
|
151
|
+
const fsIssues = await checkFilesystemHealth(db, repoPath);
|
|
152
|
+
report.issues.push(...fsIssues);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
report.issues.push({
|
|
155
|
+
severity: SEVERITY.HIGH,
|
|
156
|
+
category: 'filesystem',
|
|
157
|
+
type: 'filesystem_check_error',
|
|
158
|
+
message: `Failed to check filesystem health: ${err.message}`,
|
|
159
|
+
details: { error: err.message, stack: err.stack },
|
|
160
|
+
recommendation: 'Check filesystem permissions and disk space'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Filter by minimum severity
|
|
166
|
+
report.issues = filterBySeverity(report.issues, minSeverity);
|
|
167
|
+
|
|
168
|
+
// Calculate summary
|
|
169
|
+
for (const issue of report.issues) {
|
|
170
|
+
report.summary.totalIssues++;
|
|
171
|
+
report.summary[issue.severity]++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Determine overall health
|
|
175
|
+
if (report.summary.critical > 0) {
|
|
176
|
+
report.health = 'critical';
|
|
177
|
+
} else if (report.summary.high > 0 || report.summary.medium > 0) {
|
|
178
|
+
report.health = 'warning';
|
|
179
|
+
} else {
|
|
180
|
+
report.health = 'healthy';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Generate recommendations
|
|
184
|
+
report.recommendations = generateRecommendations(report);
|
|
185
|
+
|
|
186
|
+
return report;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check database integrity
|
|
191
|
+
*/
|
|
192
|
+
async function checkDatabaseIntegrity(db) {
|
|
193
|
+
const issues = [];
|
|
194
|
+
|
|
195
|
+
// Check for corrupted worktrees
|
|
196
|
+
const corruptedCount = await new Promise((resolve, reject) => {
|
|
197
|
+
db.get('SELECT COUNT(*) as count FROM worktrees WHERE status = ?', ['corrupted'], (err, row) => {
|
|
198
|
+
if (err) reject(err);
|
|
199
|
+
else resolve(row.count);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (corruptedCount > 0) {
|
|
204
|
+
issues.push({
|
|
205
|
+
severity: SEVERITY.INFO,
|
|
206
|
+
category: 'database',
|
|
207
|
+
type: 'corrupted_worktrees',
|
|
208
|
+
count: corruptedCount,
|
|
209
|
+
message: `Found ${corruptedCount} corrupted worktree(s) in database`,
|
|
210
|
+
details: { count: corruptedCount },
|
|
211
|
+
recommendation: 'These worktrees can be safely cleaned up or ignored'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check for worktrees with missing work items
|
|
216
|
+
const orphanedWorktrees = await new Promise((resolve, reject) => {
|
|
217
|
+
db.all(`
|
|
218
|
+
SELECT w.* FROM worktrees w
|
|
219
|
+
LEFT JOIN work_items wi ON w.work_item_id = wi.id
|
|
220
|
+
WHERE wi.id IS NULL
|
|
221
|
+
`, (err, rows) => {
|
|
222
|
+
if (err) reject(err);
|
|
223
|
+
else resolve(rows);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (orphanedWorktrees.length > 0) {
|
|
228
|
+
issues.push({
|
|
229
|
+
severity: SEVERITY.HIGH,
|
|
230
|
+
category: 'database',
|
|
231
|
+
type: 'orphaned_worktree_entries',
|
|
232
|
+
count: orphanedWorktrees.length,
|
|
233
|
+
message: `Found ${orphanedWorktrees.length} worktree(s) with missing work items`,
|
|
234
|
+
details: orphanedWorktrees,
|
|
235
|
+
recommendation: 'Mark these worktrees as corrupted'
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check filesystem health
|
|
244
|
+
*/
|
|
245
|
+
async function checkFilesystemHealth(db, repoPath) {
|
|
246
|
+
const issues = [];
|
|
247
|
+
|
|
248
|
+
// Get all active worktrees
|
|
249
|
+
const activeWorktrees = await new Promise((resolve, reject) => {
|
|
250
|
+
db.all('SELECT * FROM worktrees WHERE status = ?', ['active'], (err, rows) => {
|
|
251
|
+
if (err) reject(err);
|
|
252
|
+
else resolve(rows || []);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Check each worktree directory
|
|
257
|
+
for (const worktree of activeWorktrees) {
|
|
258
|
+
const worktreePath = worktree.worktree_path;
|
|
259
|
+
|
|
260
|
+
// Check if directory exists
|
|
261
|
+
if (!fs.existsSync(worktreePath)) {
|
|
262
|
+
issues.push({
|
|
263
|
+
severity: SEVERITY.CRITICAL,
|
|
264
|
+
category: 'filesystem',
|
|
265
|
+
type: 'missing_worktree_directory',
|
|
266
|
+
message: `Active worktree directory missing: ${worktreePath}`,
|
|
267
|
+
details: { worktree },
|
|
268
|
+
recommendation: 'Mark worktree as corrupted and recreate if needed'
|
|
269
|
+
});
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check .jettypod symlink
|
|
274
|
+
const jettypodLink = path.join(worktreePath, '.jettypod');
|
|
275
|
+
if (!fs.existsSync(jettypodLink)) {
|
|
276
|
+
issues.push({
|
|
277
|
+
severity: SEVERITY.MEDIUM,
|
|
278
|
+
category: 'filesystem',
|
|
279
|
+
type: 'missing_jettypod_symlink',
|
|
280
|
+
message: `.jettypod symlink missing in worktree: ${worktreePath}`,
|
|
281
|
+
details: { worktree, expectedLink: jettypodLink },
|
|
282
|
+
recommendation: 'Recreate symlink to .jettypod directory'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check directory permissions
|
|
287
|
+
try {
|
|
288
|
+
fs.accessSync(worktreePath, fs.constants.R_OK | fs.constants.W_OK);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
issues.push({
|
|
291
|
+
severity: SEVERITY.CRITICAL,
|
|
292
|
+
category: 'filesystem',
|
|
293
|
+
type: 'permission_issue',
|
|
294
|
+
message: `Cannot read/write worktree directory: ${worktreePath}`,
|
|
295
|
+
details: { worktree, error: err.message },
|
|
296
|
+
recommendation: 'Fix directory permissions or mark worktree as corrupted'
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return issues;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Filter issues by minimum severity
|
|
306
|
+
*/
|
|
307
|
+
function filterBySeverity(issues, minSeverity) {
|
|
308
|
+
const severityOrder = ['info', 'low', 'medium', 'high', 'critical'];
|
|
309
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
310
|
+
|
|
311
|
+
return issues.filter(issue => {
|
|
312
|
+
const issueIndex = severityOrder.indexOf(issue.severity);
|
|
313
|
+
return issueIndex >= minIndex;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Generate actionable recommendations based on issues
|
|
319
|
+
*/
|
|
320
|
+
function generateRecommendations(report) {
|
|
321
|
+
const recommendations = [];
|
|
322
|
+
|
|
323
|
+
if (report.summary.critical > 0) {
|
|
324
|
+
recommendations.push({
|
|
325
|
+
priority: 'urgent',
|
|
326
|
+
action: 'Run worktree reconciliation with cleanup enabled',
|
|
327
|
+
reason: 'Critical issues detected that may prevent work from starting'
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (report.summary.high > 0) {
|
|
332
|
+
recommendations.push({
|
|
333
|
+
priority: 'high',
|
|
334
|
+
action: 'Review and fix database integrity issues',
|
|
335
|
+
reason: 'High-severity issues may cause data loss or corruption'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (report.issues.some(i => i.type === 'missing_jettypod_symlink')) {
|
|
340
|
+
recommendations.push({
|
|
341
|
+
priority: 'medium',
|
|
342
|
+
action: 'Recreate .jettypod symlinks in affected worktrees',
|
|
343
|
+
reason: 'Missing symlinks may cause configuration and database issues'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (report.issues.some(i => i.type === 'corrupted_worktrees')) {
|
|
348
|
+
recommendations.push({
|
|
349
|
+
priority: 'low',
|
|
350
|
+
action: 'Clean up corrupted worktree entries from database',
|
|
351
|
+
reason: 'Keeping database clean improves query performance'
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (recommendations.length === 0) {
|
|
356
|
+
recommendations.push({
|
|
357
|
+
priority: 'none',
|
|
358
|
+
action: 'No action required',
|
|
359
|
+
reason: 'All worktree systems are healthy'
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return recommendations;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Quick health check (lightweight version of full diagnostics)
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} db - SQLite database connection
|
|
370
|
+
* @param {string} repoPath - Path to main git repository
|
|
371
|
+
* @returns {Promise<Object>} Health status object
|
|
372
|
+
*/
|
|
373
|
+
async function quickHealthCheck(db, repoPath) {
|
|
374
|
+
const health = {
|
|
375
|
+
status: 'unknown', // 'healthy', 'degraded', 'critical'
|
|
376
|
+
message: '',
|
|
377
|
+
issueCount: 0
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Just check for critical issues
|
|
382
|
+
const report = await runDiagnostics(db, repoPath, {
|
|
383
|
+
checks: ['state'],
|
|
384
|
+
minSeverity: 'high'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
health.issueCount = report.summary.totalIssues;
|
|
388
|
+
|
|
389
|
+
if (report.summary.critical > 0) {
|
|
390
|
+
health.status = 'critical';
|
|
391
|
+
health.message = `Found ${report.summary.critical} critical issue(s)`;
|
|
392
|
+
} else if (report.summary.high > 0) {
|
|
393
|
+
health.status = 'degraded';
|
|
394
|
+
health.message = `Found ${report.summary.high} high-severity issue(s)`;
|
|
395
|
+
} else {
|
|
396
|
+
health.status = 'healthy';
|
|
397
|
+
health.message = 'All systems operational';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
} catch (err) {
|
|
401
|
+
health.status = 'critical';
|
|
402
|
+
health.message = `Health check failed: ${err.message}`;
|
|
403
|
+
health.error = err.message;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return health;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
runDiagnostics,
|
|
411
|
+
quickHealthCheck,
|
|
412
|
+
SEVERITY
|
|
413
|
+
};
|