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
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
const decisionsHelpers = require('./decisions-helpers');
|
|
2
|
-
const { createTestEnvironment } = require('./test-helpers');
|
|
3
|
-
const { getDb, closeDb, resetDb } = require('./database');
|
|
4
|
-
const config = require('./config');
|
|
5
|
-
|
|
6
|
-
describe('Decisions Helpers', () => {
|
|
7
|
-
let testEnv;
|
|
8
|
-
let db;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
resetDb();
|
|
12
|
-
testEnv = createTestEnvironment();
|
|
13
|
-
process.chdir(testEnv.testDir);
|
|
14
|
-
db = getDb();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(async () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
await closeDb();
|
|
21
|
-
testEnv.cleanup();
|
|
22
|
-
resetDb();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('getProjectDecision', () => {
|
|
26
|
-
test('returns null when no project decision exists', () => {
|
|
27
|
-
const decision = decisionsHelpers.getProjectDecision();
|
|
28
|
-
expect(decision).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('returns project decision when it exists', () => {
|
|
32
|
-
// Mock config with decision
|
|
33
|
-
const originalRead = config.read;
|
|
34
|
-
config.read = () => ({
|
|
35
|
-
project_discovery: {
|
|
36
|
-
winner: 'prototypes/test',
|
|
37
|
-
rationale: 'Testing approach',
|
|
38
|
-
started_date: '2025-10-31T00:00:00.000Z'
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const decision = decisionsHelpers.getProjectDecision();
|
|
43
|
-
|
|
44
|
-
expect(decision).toEqual({
|
|
45
|
-
winner: 'prototypes/test',
|
|
46
|
-
rationale: 'Testing approach',
|
|
47
|
-
started_date: '2025-10-31T00:00:00.000Z'
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
config.read = originalRead;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('handles missing rationale and date gracefully', () => {
|
|
54
|
-
const originalRead = config.read;
|
|
55
|
-
config.read = () => ({
|
|
56
|
-
project_discovery: {
|
|
57
|
-
winner: 'prototypes/test'
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const decision = decisionsHelpers.getProjectDecision();
|
|
62
|
-
|
|
63
|
-
expect(decision).toEqual({
|
|
64
|
-
winner: 'prototypes/test',
|
|
65
|
-
rationale: null,
|
|
66
|
-
started_date: null
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
config.read = originalRead;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('returns null on config read error', () => {
|
|
73
|
-
const originalRead = config.read;
|
|
74
|
-
config.read = () => {
|
|
75
|
-
throw new Error('Config error');
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const decision = decisionsHelpers.getProjectDecision();
|
|
79
|
-
|
|
80
|
-
expect(decision).toBeNull();
|
|
81
|
-
|
|
82
|
-
config.read = originalRead;
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('getDecisionsForEpic', () => {
|
|
87
|
-
test('returns empty array when epic has no decisions', async () => {
|
|
88
|
-
// Create epic without decisions
|
|
89
|
-
await new Promise((resolve) => {
|
|
90
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
94
|
-
|
|
95
|
-
expect(decisions).toEqual([]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('returns decisions for specific epic', async () => {
|
|
99
|
-
// Create epic
|
|
100
|
-
await new Promise((resolve) => {
|
|
101
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Add decisions
|
|
105
|
-
await new Promise((resolve) => {
|
|
106
|
-
db.run(
|
|
107
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
108
|
-
[1, 'Architecture', 'REST API', 'Simple and widely understood'],
|
|
109
|
-
resolve
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await new Promise((resolve) => {
|
|
114
|
-
db.run(
|
|
115
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
116
|
-
[1, 'Database', 'PostgreSQL', 'Robust and feature-rich'],
|
|
117
|
-
resolve
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
122
|
-
|
|
123
|
-
expect(decisions).toHaveLength(2);
|
|
124
|
-
expect(decisions[0].aspect).toBe('Architecture');
|
|
125
|
-
expect(decisions[0].decision).toBe('REST API');
|
|
126
|
-
expect(decisions[1].aspect).toBe('Database');
|
|
127
|
-
expect(decisions[1].decision).toBe('PostgreSQL');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('only returns decisions for specified epic', async () => {
|
|
131
|
-
// Create two epics
|
|
132
|
-
await new Promise((resolve) => {
|
|
133
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 1'], resolve);
|
|
134
|
-
});
|
|
135
|
-
await new Promise((resolve) => {
|
|
136
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 2'], resolve);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Add decision to epic 1
|
|
140
|
-
await new Promise((resolve) => {
|
|
141
|
-
db.run(
|
|
142
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
143
|
-
[1, 'Architecture', 'REST API', 'Simple'],
|
|
144
|
-
resolve
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Add decision to epic 2
|
|
149
|
-
await new Promise((resolve) => {
|
|
150
|
-
db.run(
|
|
151
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
152
|
-
[2, 'Architecture', 'GraphQL', 'Flexible'],
|
|
153
|
-
resolve
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
158
|
-
|
|
159
|
-
expect(decisions).toHaveLength(1);
|
|
160
|
-
expect(decisions[0].decision).toBe('REST API');
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('getAllEpicDecisions', () => {
|
|
165
|
-
test('returns empty array when no decisions exist', async () => {
|
|
166
|
-
// Ensure db is ready
|
|
167
|
-
await new Promise((resolve) => {
|
|
168
|
-
db.get('SELECT 1', [], resolve);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const decisions = await decisionsHelpers.getAllEpicDecisions();
|
|
172
|
-
expect(decisions).toEqual([]);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('returns all epic decisions across all epics', async () => {
|
|
176
|
-
// Create two epics
|
|
177
|
-
await new Promise((resolve) => {
|
|
178
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 1'], resolve);
|
|
179
|
-
});
|
|
180
|
-
await new Promise((resolve) => {
|
|
181
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 2'], resolve);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Add decisions
|
|
185
|
-
await new Promise((resolve) => {
|
|
186
|
-
db.run(
|
|
187
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
188
|
-
[1, 'Architecture', 'REST API', 'Simple'],
|
|
189
|
-
resolve
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await new Promise((resolve) => {
|
|
194
|
-
db.run(
|
|
195
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
196
|
-
[2, 'Architecture', 'GraphQL', 'Flexible'],
|
|
197
|
-
resolve
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const decisions = await decisionsHelpers.getAllEpicDecisions();
|
|
202
|
-
|
|
203
|
-
expect(decisions).toHaveLength(2);
|
|
204
|
-
expect(decisions[0].epic_id).toBe(1);
|
|
205
|
-
expect(decisions[1].epic_id).toBe(2);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe('getAllDecisions', () => {
|
|
210
|
-
test('returns structured object with project and epic decisions', async () => {
|
|
211
|
-
// Mock project decision
|
|
212
|
-
const originalRead = config.read;
|
|
213
|
-
config.read = () => ({
|
|
214
|
-
project_discovery: {
|
|
215
|
-
winner: 'prototypes/test',
|
|
216
|
-
rationale: 'Testing'
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Create epic with decision
|
|
221
|
-
await new Promise((resolve) => {
|
|
222
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await new Promise((resolve) => {
|
|
226
|
-
db.run(
|
|
227
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
228
|
-
[1, 'Architecture', 'REST API', 'Simple'],
|
|
229
|
-
resolve
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const allDecisions = await decisionsHelpers.getAllDecisions();
|
|
234
|
-
|
|
235
|
-
expect(allDecisions.project).toEqual({
|
|
236
|
-
winner: 'prototypes/test',
|
|
237
|
-
rationale: 'Testing',
|
|
238
|
-
started_date: null
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
expect(allDecisions.epics).toHaveLength(1);
|
|
242
|
-
expect(allDecisions.epics[0].id).toBe(1);
|
|
243
|
-
expect(allDecisions.epics[0].title).toBe('Test Epic');
|
|
244
|
-
expect(allDecisions.epics[0].decisions).toHaveLength(1);
|
|
245
|
-
expect(allDecisions.epics[0].decisions[0].aspect).toBe('Architecture');
|
|
246
|
-
|
|
247
|
-
config.read = originalRead;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('groups decisions by epic', async () => {
|
|
251
|
-
// Create epic with multiple decisions
|
|
252
|
-
await new Promise((resolve) => {
|
|
253
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
await new Promise((resolve) => {
|
|
257
|
-
db.run(
|
|
258
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
259
|
-
[1, 'Architecture', 'REST API', 'Simple'],
|
|
260
|
-
resolve
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
await new Promise((resolve) => {
|
|
265
|
-
db.run(
|
|
266
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
267
|
-
[1, 'Database', 'PostgreSQL', 'Robust'],
|
|
268
|
-
resolve
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const allDecisions = await decisionsHelpers.getAllDecisions();
|
|
273
|
-
|
|
274
|
-
expect(allDecisions.epics).toHaveLength(1);
|
|
275
|
-
expect(allDecisions.epics[0].decisions).toHaveLength(2);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe('hasDecisions', () => {
|
|
280
|
-
test('returns false when epic has no decisions', async () => {
|
|
281
|
-
// Create epic without decisions
|
|
282
|
-
await new Promise((resolve) => {
|
|
283
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const result = await decisionsHelpers.hasDecisions(1);
|
|
287
|
-
|
|
288
|
-
expect(result).toBe(false);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test('returns true when epic has decisions', async () => {
|
|
292
|
-
// Create epic with decision
|
|
293
|
-
await new Promise((resolve) => {
|
|
294
|
-
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
await new Promise((resolve) => {
|
|
298
|
-
db.run(
|
|
299
|
-
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
300
|
-
[1, 'Architecture', 'REST API', 'Simple'],
|
|
301
|
-
resolve
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const result = await decisionsHelpers.hasDecisions(1);
|
|
306
|
-
|
|
307
|
-
expect(result).toBe(true);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
});
|
package/lib/git-coordinator.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
2
|
-
const { getCurrentBranch } = require('./git');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Check if main branch has moved ahead of local branch
|
|
6
|
-
* @returns {boolean} True if main has new commits
|
|
7
|
-
*/
|
|
8
|
-
function checkMainAhead() {
|
|
9
|
-
try {
|
|
10
|
-
// Fetch latest from remote
|
|
11
|
-
execSync('git fetch origin main', { stdio: 'pipe' });
|
|
12
|
-
|
|
13
|
-
// Get commit count between local and remote main
|
|
14
|
-
const result = execSync('git rev-list --count HEAD..origin/main', { encoding: 'utf-8' });
|
|
15
|
-
const commitCount = parseInt(result.trim());
|
|
16
|
-
|
|
17
|
-
return commitCount > 0;
|
|
18
|
-
} catch (err) {
|
|
19
|
-
const errorMessage = err.message || err.stderr?.toString() || '';
|
|
20
|
-
|
|
21
|
-
// Network error detection
|
|
22
|
-
const networkPatterns = [
|
|
23
|
-
'Could not resolve host',
|
|
24
|
-
'Connection refused',
|
|
25
|
-
'Connection timed out',
|
|
26
|
-
'Failed to connect',
|
|
27
|
-
'Network is unreachable',
|
|
28
|
-
'ENOTFOUND',
|
|
29
|
-
'ETIMEDOUT',
|
|
30
|
-
'ECONNREFUSED'
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const isNetworkError = networkPatterns.some(pattern =>
|
|
34
|
-
errorMessage.includes(pattern)
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (isNetworkError) {
|
|
38
|
-
console.error('❌ Network failure while checking main branch');
|
|
39
|
-
console.error(' Cannot connect to remote repository');
|
|
40
|
-
console.error('');
|
|
41
|
-
console.error('To fix:');
|
|
42
|
-
console.error(' 1. Check your internet connection');
|
|
43
|
-
console.error(' 2. Verify remote repository is accessible');
|
|
44
|
-
console.error(' 3. Try again after network is restored');
|
|
45
|
-
console.error('');
|
|
46
|
-
console.error('Command: git fetch origin main');
|
|
47
|
-
throw new Error('Network failure - cannot check main branch status');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
throw new Error(`Failed to check main branch status: ${err.message}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Determine if queue should be used for push
|
|
56
|
-
* @returns {boolean} True if queue coordination needed
|
|
57
|
-
*/
|
|
58
|
-
function shouldUseQueue() {
|
|
59
|
-
return checkMainAhead();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Push current branch directly to main
|
|
64
|
-
*/
|
|
65
|
-
function pushDirectlyToMain() {
|
|
66
|
-
try {
|
|
67
|
-
const currentBranch = getCurrentBranch();
|
|
68
|
-
|
|
69
|
-
if (currentBranch === 'main') {
|
|
70
|
-
// Already on main, just push
|
|
71
|
-
execSync('git push origin main', { stdio: 'inherit' });
|
|
72
|
-
} else {
|
|
73
|
-
// Push current branch to main
|
|
74
|
-
execSync(`git push origin ${currentBranch}:main`, { stdio: 'inherit' });
|
|
75
|
-
}
|
|
76
|
-
} catch (err) {
|
|
77
|
-
const errorMessage = err.message || err.stderr?.toString() || '';
|
|
78
|
-
|
|
79
|
-
// Push rejection detection
|
|
80
|
-
const rejectionPatterns = [
|
|
81
|
-
'rejected',
|
|
82
|
-
'non-fast-forward',
|
|
83
|
-
'Updates were rejected',
|
|
84
|
-
'failed to push',
|
|
85
|
-
'error: failed to push some refs'
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
const isRejected = rejectionPatterns.some(pattern =>
|
|
89
|
-
errorMessage.includes(pattern)
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (isRejected) {
|
|
93
|
-
console.error('❌ Push rejected by remote');
|
|
94
|
-
console.error(' Remote has changes that you do not have locally');
|
|
95
|
-
console.error('');
|
|
96
|
-
console.error('To fix:');
|
|
97
|
-
console.error(' 1. Pull latest changes: git pull --rebase');
|
|
98
|
-
console.error(' 2. Resolve any conflicts if they occur');
|
|
99
|
-
console.error(' 3. Try pushing again');
|
|
100
|
-
console.error('');
|
|
101
|
-
console.error('Your queue entry will remain for retry.');
|
|
102
|
-
throw new Error('Push rejected - remote has newer commits');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
throw new Error(`Failed to push to main: ${err.message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Rebase current branch on top of main
|
|
111
|
-
*/
|
|
112
|
-
function rebaseOnMain() {
|
|
113
|
-
try {
|
|
114
|
-
const currentBranch = getCurrentBranch();
|
|
115
|
-
|
|
116
|
-
// Fetch latest main
|
|
117
|
-
execSync('git fetch origin main', { stdio: 'pipe' });
|
|
118
|
-
|
|
119
|
-
if (currentBranch === 'main') {
|
|
120
|
-
// Already on main, just pull with rebase
|
|
121
|
-
execSync('git pull --rebase origin main', { stdio: 'inherit' });
|
|
122
|
-
} else {
|
|
123
|
-
// Rebase current branch on origin/main
|
|
124
|
-
execSync('git rebase origin/main', { stdio: 'inherit' });
|
|
125
|
-
}
|
|
126
|
-
} catch (err) {
|
|
127
|
-
const errorMessage = err.message || err.stderr?.toString() || '';
|
|
128
|
-
const exitCode = err.status;
|
|
129
|
-
|
|
130
|
-
// Rebase conflict detection
|
|
131
|
-
const conflictPatterns = [
|
|
132
|
-
'CONFLICT',
|
|
133
|
-
'Merge conflict',
|
|
134
|
-
'conflict',
|
|
135
|
-
'both modified',
|
|
136
|
-
'both added'
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
const hasConflict = conflictPatterns.some(pattern =>
|
|
140
|
-
errorMessage.includes(pattern)
|
|
141
|
-
) || exitCode === 1;
|
|
142
|
-
|
|
143
|
-
if (hasConflict) {
|
|
144
|
-
console.error('❌ Rebase conflicts detected');
|
|
145
|
-
console.error(' Your changes conflict with changes on main');
|
|
146
|
-
console.error('');
|
|
147
|
-
console.error('To resolve conflicts:');
|
|
148
|
-
console.error(' 1. Run: git status');
|
|
149
|
-
console.error(' 2. Edit conflicting files to resolve conflicts');
|
|
150
|
-
console.error(' 3. Run: git add <resolved-files>');
|
|
151
|
-
console.error(' 4. Run: git rebase --continue');
|
|
152
|
-
console.error(' 5. Or abort: git rebase --abort');
|
|
153
|
-
console.error('');
|
|
154
|
-
console.error('Your queue entry will remain until conflicts are resolved.');
|
|
155
|
-
throw new Error('Rebase conflicts - manual resolution required');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
throw new Error(`Failed to rebase on main: ${err.message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = {
|
|
163
|
-
checkMainAhead,
|
|
164
|
-
shouldUseQueue,
|
|
165
|
-
pushDirectlyToMain,
|
|
166
|
-
rebaseOnMain
|
|
167
|
-
};
|
package/lib/git.test.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { execSync } = require('child_process');
|
|
4
|
-
const { createTestEnvironment } = require('./test-helpers');
|
|
5
|
-
const {
|
|
6
|
-
isGitInstalled,
|
|
7
|
-
isGitRepo,
|
|
8
|
-
createOrCheckoutBranch,
|
|
9
|
-
getCurrentBranch,
|
|
10
|
-
slugify,
|
|
11
|
-
createFeatureBranchName
|
|
12
|
-
} = require('./git');
|
|
13
|
-
|
|
14
|
-
describe('Git Module', () => {
|
|
15
|
-
let testEnv;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
testEnv = createTestEnvironment();
|
|
19
|
-
process.chdir(testEnv.testDir);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
testEnv.cleanup();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('isGitInstalled()', () => {
|
|
27
|
-
test('should return true when git is installed', () => {
|
|
28
|
-
expect(isGitInstalled()).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('isGitRepo()', () => {
|
|
33
|
-
test('should return false in non-git directory', () => {
|
|
34
|
-
expect(isGitRepo()).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('should return true in git repository', () => {
|
|
38
|
-
execSync('git init', { stdio: 'pipe' });
|
|
39
|
-
expect(isGitRepo()).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('slugify()', () => {
|
|
44
|
-
test('should convert text to lowercase with hyphens', () => {
|
|
45
|
-
expect(slugify('Hello World')).toBe('hello-world');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('should remove special characters', () => {
|
|
49
|
-
expect(slugify('Fix: Bug #123')).toBe('fix-bug-123');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('should handle multiple spaces', () => {
|
|
53
|
-
expect(slugify('Multiple Spaces')).toBe('multiple-spaces');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('should throw error for non-string input', () => {
|
|
57
|
-
expect(() => slugify(123)).toThrow('slugify requires a string input');
|
|
58
|
-
expect(() => slugify(null)).toThrow('slugify requires a string input');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('createFeatureBranchName()', () => {
|
|
63
|
-
test('should create valid branch name', () => {
|
|
64
|
-
const branch = createFeatureBranchName(42, 'Add user auth');
|
|
65
|
-
expect(branch).toBe('feature/work-42-add-user-auth');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('should throw error for invalid work item ID', () => {
|
|
69
|
-
expect(() => createFeatureBranchName('invalid', 'Title')).toThrow('workItemId must be a positive number');
|
|
70
|
-
expect(() => createFeatureBranchName(0, 'Title')).toThrow('workItemId must be a positive number');
|
|
71
|
-
expect(() => createFeatureBranchName(-5, 'Title')).toThrow('workItemId must be a positive number');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('should throw error for invalid title', () => {
|
|
75
|
-
expect(() => createFeatureBranchName(1, 123)).toThrow('title must be a non-empty string');
|
|
76
|
-
expect(() => createFeatureBranchName(1, '')).toThrow('title must be a non-empty string');
|
|
77
|
-
expect(() => createFeatureBranchName(1, ' ')).toThrow('title must be a non-empty string');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('createOrCheckoutBranch()', () => {
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
execSync('git init', { stdio: 'pipe' });
|
|
84
|
-
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
85
|
-
execSync('git config user.name "Test User"', { stdio: 'pipe' });
|
|
86
|
-
// Create initial commit
|
|
87
|
-
fs.writeFileSync('README.md', '# Test');
|
|
88
|
-
execSync('git add .', { stdio: 'pipe' });
|
|
89
|
-
execSync('git commit -m "Initial commit"', { stdio: 'pipe' });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('should create new branch', () => {
|
|
93
|
-
const result = createOrCheckoutBranch('feature/test');
|
|
94
|
-
expect(result).toBe(true);
|
|
95
|
-
expect(getCurrentBranch()).toBe('feature/test');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('should checkout existing branch', () => {
|
|
99
|
-
execSync('git checkout -b feature/existing', { stdio: 'pipe' });
|
|
100
|
-
execSync('git checkout main || git checkout master', { stdio: 'pipe' });
|
|
101
|
-
|
|
102
|
-
const result = createOrCheckoutBranch('feature/existing');
|
|
103
|
-
expect(result).toBe(true);
|
|
104
|
-
expect(getCurrentBranch()).toBe('feature/existing');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('should throw error for invalid branch name', () => {
|
|
108
|
-
expect(() => createOrCheckoutBranch('branch name')).toThrow('Branch name cannot contain spaces');
|
|
109
|
-
expect(() => createOrCheckoutBranch('-invalid')).toThrow('Branch name cannot start with - or .');
|
|
110
|
-
expect(() => createOrCheckoutBranch('invalid~name')).toThrow('Branch name contains invalid characters');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('should throw error in non-git repository', () => {
|
|
114
|
-
testEnv.cleanup();
|
|
115
|
-
testEnv = createTestEnvironment();
|
|
116
|
-
process.chdir(testEnv.testDir);
|
|
117
|
-
|
|
118
|
-
expect(() => createOrCheckoutBranch('feature/test')).toThrow('Not in a git repository');
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe('getCurrentBranch()', () => {
|
|
123
|
-
beforeEach(() => {
|
|
124
|
-
execSync('git init', { stdio: 'pipe' });
|
|
125
|
-
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
126
|
-
execSync('git config user.name "Test User"', { stdio: 'pipe' });
|
|
127
|
-
fs.writeFileSync('README.md', '# Test');
|
|
128
|
-
execSync('git add .', { stdio: 'pipe' });
|
|
129
|
-
execSync('git commit -m "Initial commit"', { stdio: 'pipe' });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('should return current branch name', () => {
|
|
133
|
-
const branch = getCurrentBranch();
|
|
134
|
-
expect(['main', 'master']).toContain(branch);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('should return null in non-git directory', () => {
|
|
138
|
-
testEnv.cleanup();
|
|
139
|
-
testEnv = createTestEnvironment();
|
|
140
|
-
process.chdir(testEnv.testDir);
|
|
141
|
-
|
|
142
|
-
expect(getCurrentBranch()).toBe(null);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|