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.
Files changed (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -1,1174 +0,0 @@
1
- const { Given, When, Then, AfterAll } = require('@cucumber/cucumber');
2
- const assert = require('assert');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
6
- const sqlite3 = require('sqlite3').verbose();
7
- const workCommands = require('./index');
8
- const { resetDb } = require('../../lib/database');
9
- const { runMigrations } = require('../../lib/migrations');
10
-
11
- const testDir = path.join('/tmp', 'jettypod-work-commands-test-' + Date.now());
12
-
13
- // Dynamic getters for paths that update when directory changes
14
- function getJettypodDir() {
15
- return path.join(process.cwd(), '.jettypod');
16
- }
17
-
18
- function getDbPath() {
19
- const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
20
- return path.join(getJettypodDir(), dbFileName);
21
- }
22
-
23
- function getCurrentWorkPath() {
24
- return path.join(getJettypodDir(), 'current-work.json');
25
- }
26
-
27
- // Helper to get database connection (always use singleton)
28
- function getTestDb() {
29
- const { getDb } = require('../../lib/database');
30
- return getDb();
31
- }
32
-
33
- // Keep these for backward compatibility in some steps that use testDir
34
- const jettypodDir = path.join(testDir, '.jettypod');
35
- const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
36
- const dbPath = path.join(jettypodDir, dbFileName);
37
- const currentWorkPath = path.join(jettypodDir, 'current-work.json');
38
- const claudePath = path.join(testDir, 'CLAUDE.md');
39
-
40
- // Test state
41
- let testContext = {};
42
- let originalDir = process.cwd(); // Store original directory for CLI tests
43
-
44
- // Helper to ensure NODE_ENV=test is always set for test execSync calls
45
- function testExecSync(command, options = {}) {
46
- const env = { ...process.env, NODE_ENV: 'test', ...options.env };
47
- return execSync(command, { ...options, env });
48
- }
49
-
50
- // Setup test environment
51
- async function setupTestEnv() {
52
- // Reset singleton db connection to avoid stale connections
53
- const { closeDb } = require('../../lib/database');
54
- await closeDb();
55
- resetDb();
56
-
57
- // SAFETY: Only delete if testDir is in /tmp
58
- if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
59
- fs.rmSync(testDir, { recursive: true, force: true });
60
- }
61
- fs.mkdirSync(testDir, { recursive: true });
62
- fs.mkdirSync(getJettypodDir(), { recursive: true });
63
-
64
- // Change to test directory
65
- process.chdir(testDir);
66
-
67
- // Initialize git
68
- try {
69
- execSync('git init', { stdio: 'pipe' });
70
- execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
71
- execSync('git config user.name "Test"', { stdio: 'pipe' });
72
- execSync('git checkout -b main', { stdio: 'pipe' });
73
- } catch (e) {
74
- // Git already initialized
75
- }
76
-
77
- // Use singleton getDb() to initialize database
78
- const { getDb } = require('../../lib/database');
79
- const db = getDb();
80
-
81
- // Run all migrations to ensure schema is up to date
82
- await runMigrations(db);
83
-
84
- // Don't close - let singleton manage it
85
- }
86
-
87
- // Cleanup test environment
88
- async function cleanupTestEnv() {
89
- const { closeDb } = require('../../lib/database');
90
- await closeDb();
91
- resetDb();
92
-
93
- // SAFETY: Only delete if testDir is in /tmp
94
- if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
95
- process.chdir(originalDir);
96
- fs.rmSync(testDir, { recursive: true, force: true });
97
- }
98
- testContext = {};
99
- }
100
-
101
- // Before each scenario
102
- Given('a work item exists with id {string} and title {string}', async function (id, title) {
103
- await setupTestEnv();
104
- return new Promise((resolve) => {
105
- const db = getTestDb();
106
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (?, 'feature', ?, 'todo')`, [parseInt(id), title], () => {
107
- // Removed db.close() - let Node.js handle cleanup
108
- resolve();
109
- });
110
- });
111
- });
112
-
113
- Given('a work item exists with id {string} title {string} parent {string} and type {string}', async function (id, title, parentId, type) {
114
- if (!fs.existsSync(testDir)) await setupTestEnv();
115
- return new Promise((resolve) => {
116
- const db = getTestDb();
117
- db.run(`INSERT INTO work_items (id, type, title, status, parent_id) VALUES (?, ?, ?, 'todo', ?)`,
118
- [parseInt(id), type, title, parseInt(parentId)], () => {
119
- // Removed db.close() - let Node.js handle cleanup
120
- resolve();
121
- });
122
- });
123
- });
124
-
125
- Given('a work item exists with id {string} title {string} and type {string}', async function (id, title, type) {
126
- if (!fs.existsSync(testDir)) await setupTestEnv();
127
- return new Promise((resolve) => {
128
- const db = getTestDb();
129
- const idInt = parseInt(id);
130
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (?, ?, ?, 'todo')`,
131
- [idInt, type, title], () => {
132
- // Removed db.close() - let Node.js handle cleanup
133
- resolve();
134
- });
135
- });
136
- });
137
-
138
- Given('a work item exists with id {string} title {string} and status {string}', async function (id, title, status) {
139
- if (!fs.existsSync(testDir)) await setupTestEnv();
140
- return new Promise((resolve) => {
141
- const db = getTestDb();
142
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (?, 'feature', ?, ?)`, [parseInt(id), title, status], () => {
143
- // Removed db.close() - let Node.js handle cleanup
144
- resolve();
145
- });
146
- });
147
- });
148
-
149
- Given('work item {string} is currently active with status {string}', async function (id, status) {
150
- await setupTestEnv();
151
- return new Promise((resolve) => {
152
- const db = getTestDb();
153
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (?, 'feature', 'Test Item', ?)`, [parseInt(id), status], () => {
154
- // Removed db.close() - let Node.js handle cleanup
155
- const currentWork = {
156
- id: parseInt(id),
157
- title: 'Test Item',
158
- type: 'feature',
159
- status: status
160
- };
161
- fs.writeFileSync(getCurrentWorkPath(), JSON.stringify(currentWork, null, 2));
162
- resolve();
163
- });
164
- });
165
- });
166
-
167
- Given('CLAUDE.md exists with mode {string}', async function (mode) {
168
- if (!fs.existsSync(testDir)) await setupTestEnv();
169
- const content = `<claude_context project="test">
170
- <project_summary>
171
- Test project
172
- </project_summary>
173
- <mode>${mode}</mode>
174
- </claude_context>`;
175
- fs.writeFileSync(claudePath, content);
176
- });
177
-
178
- Given('I am on branch {string}', function (branch) {
179
- try {
180
- execSync(`git checkout -b ${branch}`, { stdio: 'pipe' });
181
- } catch (e) {
182
- execSync(`git checkout ${branch}`, { stdio: 'pipe' });
183
- }
184
- });
185
-
186
- When('I run {string}', async function (command) {
187
- const parts = command.split(' ');
188
- if (parts[0] === 'jettypod' && parts[1] === 'work' && parts[2] === 'start') {
189
- const id = parseInt(parts[3]);
190
- const result = await workCommands.startWork(id);
191
- testContext.output = `Working on: [#${result.workItem.id}] ${result.workItem.title} (${result.workItem.type})`;
192
- if (result.workItem.parent_title) {
193
- testContext.output = `Working on: [#${result.workItem.id}] ${result.workItem.title} (${result.workItem.type} of #${result.workItem.parent_id} ${result.workItem.parent_title})`;
194
- }
195
- } else if (parts[0] === 'jettypod' && (parts[1] === 'init' || parts.length === 1)) {
196
- // Capture output for jettypod init
197
- const originalLog = console.log;
198
- let capturedOutput = '';
199
- console.log = (...args) => {
200
- capturedOutput += args.join(' ') + '\n';
201
- originalLog(...args);
202
- };
203
-
204
- // Ensure testDir exists or use current directory
205
- const workDir = (testDir && fs.existsSync(testDir)) ? testDir : process.cwd();
206
-
207
- try {
208
- const output = execSync(
209
- `node ${path.join(__dirname, '../../jettypod.js')} ${parts.slice(1).join(' ')}`,
210
- { cwd: workDir, encoding: 'utf-8' }
211
- );
212
- capturedOutput += output;
213
- } catch (err) {
214
- capturedOutput += err.stdout || '';
215
- } finally {
216
- console.log = originalLog;
217
- }
218
-
219
- testContext.commandOutput = capturedOutput;
220
- testContext.initOutput = capturedOutput;
221
- // Also set on 'this' for other step files (e.g., terminal-logo tests)
222
- this.commandOutput = capturedOutput;
223
- this.initOutput = capturedOutput;
224
- } else if (parts[0] === 'jettypod') {
225
- // Handle other jettypod commands (backlog, etc.)
226
- const workDir = (testDir && fs.existsSync(testDir)) ? testDir : process.cwd();
227
-
228
- try {
229
- const output = execSync(
230
- `node ${path.join(__dirname, '../../jettypod.js')} ${parts.slice(1).join(' ')}`,
231
- { cwd: workDir, encoding: 'utf-8' }
232
- );
233
- testContext.commandOutput = output;
234
- this.commandOutput = output;
235
- } catch (err) {
236
- testContext.commandOutput = err.stdout || '';
237
- this.commandOutput = err.stdout || '';
238
- }
239
- }
240
- });
241
-
242
- When('I run {string} and enter status {string}', async function (command, status) {
243
- await workCommands.stopWork(status);
244
- });
245
-
246
- Then('the current work file should contain work item {string}', function (id) {
247
- assert(fs.existsSync(getCurrentWorkPath()), 'Current work file does not exist');
248
- const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
249
- assert.strictEqual(currentWork.id, parseInt(id));
250
- });
251
-
252
- Then('the work item {string} status should be {string}', function (id, status) {
253
- const db = getTestDb();
254
- return new Promise((resolve) => {
255
- db.get(`SELECT status FROM work_items WHERE id = ?`, [parseInt(id)], (err, row) => {
256
- // Removed db.close() - let Node.js handle cleanup
257
- assert.strictEqual(row.status, status);
258
- resolve();
259
- });
260
- });
261
- });
262
-
263
- Then('a feature branch {string} should be created', function (branchName) {
264
- const branches = execSync('git branch', { encoding: 'utf-8' });
265
- assert(branches.includes(branchName), `Branch ${branchName} not found`);
266
- });
267
-
268
- Then('CLAUDE.md current_work should show {string}', function (text) {
269
- const content = fs.readFileSync(claudePath, 'utf-8');
270
- assert(content.includes(text), `CLAUDE.md does not contain: ${text}`);
271
- });
272
-
273
- Then('the current work file should be empty', function () {
274
- assert(!fs.existsSync(getCurrentWorkPath()), 'Current work file still exists');
275
- });
276
-
277
- Then('the output should contain {string}', function (text) {
278
- assert(testContext.output && testContext.output.includes(text), `Output does not contain: ${text}`);
279
- });
280
-
281
- Then('CLAUDE.md mode should still be {string}', function (mode) {
282
- const content = fs.readFileSync(claudePath, 'utf-8');
283
- const modeMatch = content.match(/<mode>(.*?)<\/mode>/);
284
- assert(modeMatch && modeMatch[1] === mode, `Mode is not ${mode}`);
285
- });
286
-
287
- Then('I should be on branch {string}', function (branchName) {
288
- const currentBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
289
- assert.strictEqual(currentBranch, branchName);
290
- });
291
-
292
- // Stable tests steps
293
-
294
- Given('jettypod is initialized', async function () {
295
- await setupTestEnv();
296
- });
297
-
298
- Given('jettypod is not initialized', function () {
299
- // SAFETY: Only delete if testDir is in /tmp
300
- if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
301
- fs.rmSync(testDir, { recursive: true, force: true });
302
- }
303
- fs.mkdirSync(testDir, { recursive: true });
304
- process.chdir(testDir);
305
- // Don't create .jettypod directory
306
- });
307
-
308
- Given('I have current work', async function () {
309
- await setupTestEnv();
310
- return new Promise((resolve) => {
311
- const db = getTestDb();
312
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (1, 'feature', 'Test Work', 'in_progress')`, () => {
313
- // Removed db.close() - let Node.js handle cleanup
314
- const currentWork = {
315
- id: 1,
316
- title: 'Test Work',
317
- type: 'feature',
318
- status: 'in_progress'
319
- };
320
- fs.writeFileSync(getCurrentWorkPath(), JSON.stringify(currentWork, null, 2));
321
- resolve();
322
- });
323
- });
324
- });
325
-
326
- Given('no work is active', function () {
327
- if (fs.existsSync(getCurrentWorkPath())) {
328
- fs.unlinkSync(getCurrentWorkPath());
329
- }
330
- });
331
-
332
- Given('current work file is corrupted', async function () {
333
- await setupTestEnv();
334
- fs.writeFileSync(getCurrentWorkPath(), 'invalid json {{{');
335
- });
336
-
337
- Given('jettypod is initialized without git', function (done) {
338
- // Reset database singleton to avoid issues with previous tests
339
- resetDb();
340
-
341
- // SAFETY: Only delete if testDir is in /tmp
342
- if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
343
- fs.rmSync(testDir, { recursive: true, force: true });
344
- }
345
- fs.mkdirSync(testDir, { recursive: true });
346
- process.chdir(testDir); // Change directory BEFORE creating .jettypod
347
- fs.mkdirSync(getJettypodDir(), { recursive: true });
348
-
349
- // Initialize database but no git - use proper migrations
350
- const db = getTestDb();
351
- runMigrations(db).then(() => done()).catch(done);
352
- });
353
-
354
- Given('I have a work item', async function () {
355
- // Check if we need full setup (including git) or just database
356
- const dbPath = path.join(getJettypodDir(), process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db');
357
- const needsSetup = !fs.existsSync(dbPath);
358
-
359
- if (needsSetup) {
360
- // Check if we're in a git-free test (testDir exists but no .git)
361
- const isWithoutGit = fs.existsSync(testDir) && !fs.existsSync(path.join(testDir, '.git'));
362
- if (!isWithoutGit) {
363
- await setupTestEnv();
364
- }
365
- }
366
-
367
- return new Promise((resolve) => {
368
- const db = getTestDb();
369
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (1, 'feature', 'Test Item', 'todo')`, () => {
370
- testContext.workItemId = 1;
371
- this.workItemId = 1; // Also set on this for git-hooks steps compatibility
372
- resolve();
373
- });
374
- });
375
- });
376
-
377
- Given('I start work on it', function () {
378
- // Check if workItemId is in testContext or this (from other step files)
379
- const workItemId = testContext.workItemId || this.workItemId;
380
-
381
- // Use CLI to avoid database singleton issues across test contexts
382
- try {
383
- testExecSync(`node ${path.join(__dirname, '../../jettypod.js')} work start ${workItemId}`, { cwd: testDir, stdio: 'pipe' });
384
- testContext.firstWorkItemId = workItemId;
385
- } catch (err) {
386
- // If CLI fails, try direct module call
387
- return workCommands.startWork(workItemId).then(result => {
388
- testContext.firstWorkItemId = result.workItem.id;
389
- });
390
- }
391
- });
392
-
393
- When('I try to start work with ID {string}', async function (id) {
394
- try {
395
- await workCommands.startWork(id);
396
- testContext.error = null;
397
- } catch (err) {
398
- testContext.error = err.message;
399
- }
400
- });
401
-
402
- When('I try to stop work with status {string}', async function (status) {
403
- try {
404
- await workCommands.stopWork(status);
405
- testContext.error = null;
406
- } catch (err) {
407
- testContext.error = err.message;
408
- }
409
- });
410
-
411
- When('I try to stop work', async function () {
412
- try {
413
- await workCommands.stopWork();
414
- testContext.error = null;
415
- } catch (err) {
416
- testContext.error = err.message;
417
- }
418
- });
419
-
420
- When('I get current work', async function () {
421
- try {
422
- testContext.currentWork = await workCommands.getCurrentWork();
423
- } catch (err) {
424
- testContext.currentWork = null;
425
- }
426
- });
427
-
428
- // Note: "I start work on the item" is defined in git-hooks/steps.js
429
-
430
- When('I start work on a different item', async function () {
431
- // Create a second work item
432
- return new Promise((resolve) => {
433
- const db = getTestDb();
434
- db.run(`INSERT INTO work_items (id, type, title, status) VALUES (2, 'feature', 'Second Item', 'todo')`, async () => {
435
- // Removed db.close() - let Node.js handle cleanup
436
- testContext.secondWorkItemId = 2;
437
- await workCommands.startWork(testContext.secondWorkItemId);
438
- resolve();
439
- });
440
- });
441
- });
442
-
443
- Then('I get an error {string}', function (expectedError) {
444
- assert(testContext.error, 'No error was thrown');
445
- assert(testContext.error.includes(expectedError), `Expected error "${expectedError}" but got "${testContext.error}"`);
446
- });
447
-
448
- Then('operation succeeds with no changes', function () {
449
- assert.strictEqual(testContext.error, null, 'Operation should not error');
450
- });
451
-
452
- Then('it returns null', function () {
453
- assert.strictEqual(testContext.currentWork, null);
454
- });
455
-
456
- Then('it succeeds without creating branch', function () {
457
- assert(!fs.existsSync(path.join(testDir, '.git')), 'Git directory should not exist');
458
- assert(fs.existsSync(getCurrentWorkPath()), 'Current work file should exist');
459
- });
460
-
461
- Then('the first item stops being current', function () {
462
- const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
463
- assert.notStrictEqual(currentWork.id, testContext.firstWorkItemId);
464
- });
465
-
466
- Then('the second item becomes current', function () {
467
- const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
468
- assert.strictEqual(currentWork.id, testContext.secondWorkItemId);
469
- });
470
-
471
- Then('the status remains {string}', function (expectedStatus) {
472
- const db = getTestDb();
473
- return new Promise((resolve) => {
474
- db.get(`SELECT status FROM work_items WHERE id = ?`, [testContext.workItemId], (err, row) => {
475
- // Removed db.close() - let Node.js handle cleanup
476
- assert.strictEqual(row.status, expectedStatus);
477
- resolve();
478
- });
479
- });
480
- });
481
-
482
- // Mode-required steps - Epic
483
- When('I create an epic {string} without mode', function(title) {
484
- try {
485
- const output = execSync(
486
- `node ${path.join(__dirname, '../../jettypod.js')} work create epic "${title}"`,
487
- { cwd: testDir, encoding: 'utf-8' }
488
- );
489
- testContext.lastOutput = output;
490
-
491
- const match = output.match(/Created \w+ #(\d+):/);
492
- if (match) {
493
- testContext.createdItemId = parseInt(match[1]);
494
- testContext.epicId = parseInt(match[1]); // Also set epicId for parent references
495
- testContext.lastCreatedId = parseInt(match[1]); // For start work steps
496
- }
497
- } catch (err) {
498
- testContext.error = err.stderr || err.message;
499
- }
500
- });
501
-
502
- // Removed duplicate: When('I create an epic {string} with mode {string}')
503
- // Using the Given version at line 538 instead
504
-
505
- // Mode-required steps - Feature
506
- When('I create a feature {string} with mode {string}', function(title, mode) {
507
- try {
508
- const output = execSync(
509
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}" "" --mode=${mode}`,
510
- { cwd: testDir, encoding: 'utf-8' }
511
- );
512
- testContext.lastOutput = output;
513
-
514
- const match = output.match(/Created \w+ #(\d+):/);
515
- if (match) {
516
- testContext.createdItemId = parseInt(match[1]);
517
- testContext.lastCreatedId = parseInt(match[1]); // For start work steps
518
- testContext.lastFeatureId = parseInt(match[1]); // For type-specific start work steps
519
- if (!testContext.createdItemIds) testContext.createdItemIds = [];
520
- testContext.createdItemIds.push(testContext.createdItemId);
521
- }
522
- } catch (err) {
523
- testContext.error = err.stderr || err.message;
524
- }
525
- });
526
-
527
- When('I create a feature {string} without mode', function(title) {
528
- try {
529
- const output = execSync(
530
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
531
- { cwd: testDir, encoding: 'utf-8' }
532
- );
533
- testContext.lastOutput = output;
534
-
535
- const match = output.match(/Created \w+ #(\d+):/);
536
- if (match) {
537
- testContext.createdItemId = parseInt(match[1]);
538
- }
539
- } catch (err) {
540
- testContext.error = err.stderr || err.message;
541
- }
542
- });
543
-
544
- When('I try to create a feature {string} without mode', function(title) {
545
- try {
546
- const output = execSync(
547
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
548
- { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
549
- );
550
- testContext.lastOutput = output;
551
- } catch (err) {
552
- testContext.error = err.stderr || err.message;
553
- }
554
- });
555
-
556
- When('I try to create a feature {string} with mode {string}', function(title, mode) {
557
- try {
558
- const output = execSync(
559
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}" "" --mode=${mode}`,
560
- { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
561
- );
562
- testContext.lastOutput = output;
563
- } catch (err) {
564
- testContext.error = err.stderr || err.message;
565
- }
566
- });
567
-
568
- Then('the work item is created successfully', function() {
569
- assert(testContext.lastOutput.includes('Created'));
570
- assert(typeof testContext.createdItemId === 'number');
571
- });
572
-
573
- Then('the work item has mode {string}', function(mode) {
574
- const db = getTestDb();
575
- return new Promise((resolve) => {
576
- db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
577
- // Removed db.close() - let Node.js handle cleanup
578
- assert.strictEqual(row.mode, mode);
579
- resolve();
580
- });
581
- });
582
- });
583
-
584
- Then('the work item has no mode', function() {
585
- const db = getTestDb();
586
- return new Promise((resolve) => {
587
- db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
588
- // Removed db.close() - let Node.js handle cleanup
589
- assert.strictEqual(row.mode, null);
590
- resolve();
591
- });
592
- });
593
- });
594
-
595
- Then('no work item is created', function() {
596
- const db = getTestDb();
597
- return new Promise((resolve) => {
598
- db.get('SELECT COUNT(*) as count FROM work_items', [], (err, row) => {
599
- // Removed db.close() - let Node.js handle cleanup
600
- assert.strictEqual(row.count, 0);
601
- resolve();
602
- });
603
- });
604
- });
605
-
606
- Given('I create an epic {string} with mode {string}', function(title, mode) {
607
- const output = execSync(
608
- `node ${path.join(__dirname, '../../jettypod.js')} work create epic "${title}" "" --mode=${mode}`,
609
- { cwd: testDir, encoding: 'utf-8' }
610
- );
611
-
612
- const match = output.match(/Created \w+ #(\d+):/);
613
- if (match) {
614
- testContext.epicId = parseInt(match[1]);
615
- }
616
- });
617
-
618
- Given('I create a feature {string} with mode {string} and parent epic', function(title, mode) {
619
- const output = execSync(
620
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}" "" --mode=${mode} --parent=${testContext.epicId}`,
621
- { cwd: testDir, encoding: 'utf-8' }
622
- );
623
-
624
- const match = output.match(/Created \w+ #(\d+):/);
625
- if (match) {
626
- testContext.lastCreatedSpeedFeatureId = parseInt(match[1]); // For hierarchical scenarios
627
- }
628
- });
629
-
630
- When('I view the backlog', function() {
631
- testContext.lastOutput = execSync(
632
- `node ${path.join(__dirname, '../../jettypod.js')} backlog`,
633
- { cwd: testDir, encoding: 'utf-8' }
634
- );
635
- });
636
-
637
- Then('I see the epic with mode {string}', function(mode) {
638
- assert(testContext.lastOutput.includes('Test Epic'));
639
- });
640
-
641
- Then('I see the child feature with mode {string}', function(mode) {
642
- assert(testContext.lastOutput.includes('Child Feature'));
643
- assert(testContext.lastOutput.includes(`[${mode}]`));
644
- });
645
-
646
- // Bug steps
647
- When('I create a bug {string} with mode {string}', function(title, mode) {
648
- try {
649
- const output = execSync(
650
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --mode=${mode}`,
651
- { cwd: testDir, encoding: 'utf-8' }
652
- );
653
- testContext.lastOutput = output;
654
-
655
- const match = output.match(/Created \w+ #(\d+):/);
656
- if (match) {
657
- testContext.createdItemId = parseInt(match[1]);
658
- testContext.lastCreatedId = parseInt(match[1]); // For start work steps
659
- testContext.lastBugId = parseInt(match[1]); // For type-specific start work steps
660
- }
661
- } catch (err) {
662
- testContext.error = err.stderr || err.message;
663
- }
664
- });
665
-
666
- When('I create a bug {string} without mode', function(title) {
667
- try {
668
- const output = execSync(
669
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
670
- { cwd: testDir, encoding: 'utf-8' }
671
- );
672
- testContext.lastOutput = output;
673
-
674
- const match = output.match(/Created \w+ #(\d+):/);
675
- if (match) {
676
- testContext.createdItemId = parseInt(match[1]);
677
- }
678
- } catch (err) {
679
- testContext.error = err.stderr || err.message;
680
- }
681
- });
682
-
683
- When('I try to create a bug {string} without mode', function(title) {
684
- try {
685
- const output = execSync(
686
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
687
- { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
688
- );
689
- testContext.lastOutput = output;
690
- } catch (err) {
691
- testContext.error = err.stderr || err.message;
692
- }
693
- });
694
-
695
- When('I try to create a bug {string} with mode {string}', function(title, mode) {
696
- try {
697
- const output = execSync(
698
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --mode=${mode}`,
699
- { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
700
- );
701
- testContext.lastOutput = output;
702
- } catch (err) {
703
- testContext.error = err.stderr || err.message;
704
- }
705
- });
706
-
707
- // Chore steps
708
- When('I try to create a chore {string} with mode {string}', function(title, mode) {
709
- try {
710
- execSync(
711
- `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --mode=${mode}`,
712
- { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
713
- );
714
- testContext.error = null;
715
- } catch (err) {
716
- testContext.error = err.stderr || err.message;
717
- }
718
- });
719
-
720
- When('I create a chore {string} without mode', function(title) {
721
- try {
722
- const output = execSync(
723
- `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}"`,
724
- { cwd: testDir, encoding: 'utf-8' }
725
- );
726
- testContext.lastOutput = output;
727
-
728
- const match = output.match(/Created \w+ #(\d+):/);
729
- if (match) {
730
- testContext.createdItemId = parseInt(match[1]);
731
- }
732
- } catch (err) {
733
- testContext.error = err.stderr || err.message;
734
- }
735
- });
736
-
737
- When('I try to create a chore {string} without mode', function(title) {
738
- try {
739
- const output = execSync(
740
- `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}"`,
741
- { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
742
- );
743
- testContext.lastOutput = output;
744
- } catch (err) {
745
- testContext.error = err.stderr || err.message;
746
- }
747
- });
748
-
749
- // Additional assertions
750
- Then('the work item has NULL mode', function() {
751
- const db = getTestDb();
752
- return new Promise((resolve) => {
753
- db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
754
- // Removed db.close() - let Node.js handle cleanup
755
- assert.strictEqual(row.mode, null);
756
- resolve();
757
- });
758
- });
759
- });
760
-
761
- Given('I create a bug {string} with parent epic', function(title) {
762
- const output = execSync(
763
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --parent=${testContext.epicId}`,
764
- { cwd: testDir, encoding: 'utf-8' }
765
- );
766
-
767
- const match = output.match(/Created \w+ #(\d+):/);
768
- if (match) {
769
- testContext.lastCreatedStableBugId = parseInt(match[1]); // For hierarchical scenarios
770
- }
771
- });
772
-
773
- Given('I create a chore {string} without mode and parent epic', function(title) {
774
- const output = execSync(
775
- `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --parent=${testContext.epicId}`,
776
- { cwd: testDir, encoding: 'utf-8' }
777
- );
778
-
779
- const match = output.match(/Created \w+ #(\d+):/);
780
- if (match) {
781
- testContext.lastChoreId = parseInt(match[1]);
782
- }
783
- });
784
-
785
- Given('I create a chore {string} without mode and parent feature', function(title) {
786
- const output = execSync(
787
- `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --parent=${testContext.lastFeatureId}`,
788
- { cwd: testDir, encoding: 'utf-8' }
789
- );
790
-
791
- const match = output.match(/Created \w+ #(\d+):/);
792
- if (match) {
793
- testContext.lastChoreId = parseInt(match[1]);
794
- }
795
- });
796
-
797
- Then('I see the feature with mode {string}', function(mode) {
798
- assert(testContext.lastOutput.includes('Speed Feature'));
799
- assert(testContext.lastOutput.includes(`[${mode}]`));
800
- });
801
-
802
- Then('I see the bug with mode {string}', function(mode) {
803
- assert(testContext.lastOutput.includes('Stable Bug'));
804
- assert(testContext.lastOutput.includes(`[${mode}]`));
805
- });
806
-
807
- Then('I see the chore with mode {string}', function(mode) {
808
- assert(testContext.lastOutput.includes('Production Chore'));
809
- assert(testContext.lastOutput.includes(`[${mode}]`));
810
- });
811
-
812
- Then('I see the epic without mode indicator', function() {
813
- assert(testContext.lastOutput.includes('Test Epic'));
814
- // Epic should NOT have a mode indicator
815
- const epicLine = testContext.lastOutput.split('\n').find(line => line.includes('Test Epic'));
816
- assert(!epicLine.match(/\[(speed|discovery|stable|production)\]/), 'Epic should not have a mode indicator');
817
- });
818
-
819
- Then('I see the chore without mode indicator', function() {
820
- assert(testContext.lastOutput.includes('Test Chore'));
821
- // Chore should NOT have a mode indicator
822
- const choreLine = testContext.lastOutput.split('\n').find(line => line.includes('Test Chore'));
823
- assert(!choreLine.match(/\[(speed|discovery|stable|production)\]/), 'Chore should not have a mode indicator');
824
- });
825
-
826
- Then('both work items are created successfully', function() {
827
- assert(testContext.createdItemIds);
828
- assert(testContext.createdItemIds.length >= 2);
829
- });
830
-
831
- Then('they have different modes', function() {
832
- const db = getTestDb();
833
- const placeholders = testContext.createdItemIds.map(() => '?').join(',');
834
- return new Promise((resolve, reject) => {
835
- db.all(`SELECT mode FROM work_items WHERE id IN (${placeholders})`, ...testContext.createdItemIds, (err, rows) => {
836
- if (err) {
837
- return reject(err);
838
- }
839
- assert.strictEqual(rows.length, 2);
840
- assert.notStrictEqual(rows[0].mode, rows[1].mode);
841
- resolve();
842
- });
843
- });
844
- });
845
-
846
- // Steps for work-start-mode.feature
847
- Given('CLAUDE.md exists', function() {
848
- const claudePath = path.join(testDir, 'CLAUDE.md');
849
- const content = `<claude_context project="test">
850
- <current_work>
851
- Working on: [#1] Test Item (feature)
852
- Mode: speed
853
- Status: in_progress
854
- </current_work>
855
- <mode>speed</mode>
856
- </claude_context>`;
857
- fs.writeFileSync(claudePath, content);
858
- });
859
-
860
- Given('the feature status is {string}', function(status) {
861
- const db = getTestDb();
862
- return new Promise((resolve, reject) => {
863
- db.run('UPDATE work_items SET status = ? WHERE id = ?', [status, testContext.lastCreatedId], (err) => {
864
- // Removed db.close() - let Node.js handle cleanup
865
- if (err) reject(err);
866
- else resolve();
867
- });
868
- });
869
- });
870
-
871
- Given('CLAUDE.md has mode {string}', function(mode) {
872
- const claudePath = path.join(testDir, 'CLAUDE.md');
873
- const content = `<claude_context project="test">
874
- <current_work>
875
- Working on: [#1] Test Item (feature)
876
- Mode: ${mode}
877
- Status: in_progress
878
- </current_work>
879
- <mode>${mode}</mode>
880
- </claude_context>`;
881
- fs.writeFileSync(claudePath, content);
882
- });
883
-
884
- When('I start work on the feature', async function() {
885
- const { startWork } = require('../../features/work-commands');
886
- // Capture console.log output
887
- const originalLog = console.log;
888
- let capturedOutput = '';
889
- console.log = (...args) => {
890
- capturedOutput += args.join(' ') + '\n';
891
- originalLog(...args);
892
- };
893
- testContext.result = await startWork(testContext.lastFeatureId);
894
- console.log = originalLog;
895
- testContext.output = capturedOutput;
896
- });
897
-
898
- When('I start work on the bug', async function() {
899
- const { startWork } = require('../../features/work-commands');
900
- // Capture console.log output
901
- const originalLog = console.log;
902
- let capturedOutput = '';
903
- console.log = (...args) => {
904
- capturedOutput += args.join(' ') + '\n';
905
- originalLog(...args);
906
- };
907
- testContext.result = await startWork(testContext.lastBugId);
908
- console.log = originalLog;
909
- testContext.output = capturedOutput;
910
- });
911
-
912
- When('I start work on the chore', async function() {
913
- const { startWork } = require('../../features/work-commands');
914
- testContext.result = await startWork(testContext.lastChoreId);
915
- });
916
-
917
- When('I start work on the epic', async function() {
918
- const { startWork } = require('../../features/work-commands');
919
- testContext.result = await startWork(testContext.lastCreatedId);
920
- });
921
-
922
- When('I start work on the speed feature', async function() {
923
- const { startWork } = require('../../features/work-commands');
924
- testContext.result = await startWork(testContext.lastCreatedSpeedFeatureId);
925
- });
926
-
927
- When('I start work on the stable bug', async function() {
928
- const { startWork } = require('../../features/work-commands');
929
- testContext.result = await startWork(testContext.lastCreatedStableBugId);
930
- });
931
-
932
- When('I start work on the stable feature', async function() {
933
- const { startWork } = require('../../features/work-commands');
934
- // Find the stable feature ID from the last created items
935
- const { getDb } = require('../../lib/database');
936
- const db = getDb();
937
- return new Promise((resolve) => {
938
- db.get('SELECT id FROM work_items WHERE title = ? AND mode = ?', ['Stable Feature', 'stable'], (err, row) => {
939
- if (row) {
940
- startWork(row.id).then(() => resolve());
941
- } else {
942
- resolve();
943
- }
944
- });
945
- });
946
- });
947
-
948
- When('I stop work', async function() {
949
- const { stopWork } = require('../../features/work-commands');
950
- await stopWork();
951
- });
952
-
953
- Then('CLAUDE.md mode is set to {string}', function(expectedMode) {
954
- const claudePath = path.join(testDir, 'CLAUDE.md');
955
- const content = fs.readFileSync(claudePath, 'utf-8');
956
- const modeMatch = content.match(/^Mode: (.+)$/m);
957
- assert(modeMatch, 'CLAUDE.md should have a Mode line');
958
- assert.strictEqual(modeMatch[1], expectedMode);
959
- });
960
-
961
- Then('CLAUDE.md has no mode line', function() {
962
- const claudePath = path.join(testDir, 'CLAUDE.md');
963
- const content = fs.readFileSync(claudePath, 'utf-8');
964
- const modeMatch = content.match(/^Mode: (.+)$/m);
965
- assert(!modeMatch, 'CLAUDE.md should not have a Mode line for epics');
966
- });
967
-
968
- Then('the current work section exists', function() {
969
- const claudePath = path.join(testDir, 'CLAUDE.md');
970
- const content = fs.readFileSync(claudePath, 'utf-8');
971
- assert(content.includes('<current_work>'), 'CLAUDE.md should have current_work section');
972
- });
973
-
974
- Then('the work item still has mode {string}', function(expectedMode) {
975
- const db = getTestDb();
976
- return new Promise((resolve, reject) => {
977
- db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.lastCreatedId], (err, row) => {
978
- // Removed db.close() - let Node.js handle cleanup
979
- if (err) {
980
- reject(err);
981
- } else {
982
- assert.strictEqual(row.mode, expectedMode);
983
- resolve();
984
- }
985
- });
986
- });
987
- });
988
-
989
- // Steps for work-set-mode.feature
990
- When('I set mode for current item to {string}', function(mode) {
991
- try {
992
- // Get current work item ID from current-work.json
993
- const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
994
- execSync(
995
- `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${currentWork.id} ${mode}`,
996
- { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
997
- );
998
- testContext.error = null;
999
- } catch (err) {
1000
- testContext.error = err.message || err;
1001
- }
1002
- });
1003
-
1004
- When('I set mode for item {string} to {string}', function(title, mode) {
1005
- const db = getTestDb();
1006
- return new Promise((resolve) => {
1007
- db.get('SELECT id FROM work_items WHERE title = ?', [title], (err, row) => {
1008
- // Removed db.close() - let Node.js handle cleanup
1009
- if (err || !row) {
1010
- testContext.error = 'Work item not found';
1011
- resolve();
1012
- return;
1013
- }
1014
- try {
1015
- execSync(
1016
- `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${row.id} ${mode}`,
1017
- { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1018
- );
1019
- testContext.error = null;
1020
- } catch (err) {
1021
- testContext.error = err.message || err;
1022
- }
1023
- resolve();
1024
- });
1025
- });
1026
- });
1027
-
1028
- When('I set mode for the epic to {string}', function(mode) {
1029
- try {
1030
- execSync(
1031
- `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${testContext.epicId} ${mode}`,
1032
- { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1033
- );
1034
- testContext.error = null;
1035
- } catch (err) {
1036
- testContext.error = err.message || err;
1037
- }
1038
- });
1039
-
1040
- When('I try to set mode to {string}', function(mode) {
1041
- try {
1042
- // Get work item ID from testContext (set by create/start commands)
1043
- const workItemId = testContext.createdItemId || testContext.workItemId || testContext.firstWorkItemId;
1044
-
1045
- if (!workItemId) {
1046
- throw new Error('No work item ID found in test context');
1047
- }
1048
-
1049
- try {
1050
- testExecSync(
1051
- `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${workItemId} ${mode}`,
1052
- { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1053
- );
1054
- testContext.error = null;
1055
- } catch (cmdErr) {
1056
- // Check if error is due to invalid mode
1057
- const errOutput = cmdErr.stderr || cmdErr.message;
1058
- if (errOutput.includes('Invalid mode')) {
1059
- testContext.error = 'Invalid mode';
1060
- } else {
1061
- testContext.error = cmdErr.message || cmdErr;
1062
- }
1063
- }
1064
- } catch (err) {
1065
- testContext.error = err.message || err;
1066
- }
1067
- });
1068
-
1069
- Then('I get error {string}', function(expectedError) {
1070
- assert(testContext.error, 'Expected an error but got none');
1071
- assert(testContext.error.includes(expectedError), `Expected error to include "${expectedError}" but got: ${testContext.error}`);
1072
- });
1073
-
1074
- Then('item {string} has mode {string}', function(title, expectedMode) {
1075
- const db = getTestDb();
1076
- return new Promise((resolve) => {
1077
- db.get('SELECT mode FROM work_items WHERE title = ?', [title], (err, row) => {
1078
- // Removed db.close() - let Node.js handle cleanup
1079
- assert.strictEqual(row.mode, expectedMode);
1080
- resolve();
1081
- });
1082
- });
1083
- });
1084
-
1085
- Then('the epic has mode {string}', function(expectedMode) {
1086
- const db = getTestDb();
1087
- return new Promise((resolve) => {
1088
- db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.epicId], (err, row) => {
1089
- // Removed db.close() - let Node.js handle cleanup
1090
- assert.strictEqual(row.mode, expectedMode);
1091
- resolve();
1092
- });
1093
- });
1094
- });
1095
-
1096
- Then('CLAUDE.md still has no mode line', function() {
1097
- const claudePath = path.join(testDir, 'CLAUDE.md');
1098
- const content = fs.readFileSync(claudePath, 'utf-8');
1099
- const modeMatch = content.match(/^Mode: (.+)$/m);
1100
- assert(!modeMatch, 'CLAUDE.md should not have a Mode line for epics');
1101
- });
1102
-
1103
- Given('I start work on the feature {string}', async function(title) {
1104
- const { startWork } = require('../../features/work-commands');
1105
- const db = getTestDb();
1106
- return new Promise((resolve) => {
1107
- db.get('SELECT id FROM work_items WHERE title = ?', [title], async (err, row) => {
1108
- // Removed db.close() - let Node.js handle cleanup
1109
- if (err || !row) {
1110
- throw new Error('Work item not found');
1111
- }
1112
- testContext.result = await startWork(row.id);
1113
- resolve();
1114
- });
1115
- });
1116
- });
1117
-
1118
- // Steps for bug-workflow-display.feature
1119
- Then('the output contains {string}', function(text) {
1120
- assert(testContext.output, 'No output captured');
1121
- assert(testContext.output.includes(text), `Output does not contain: ${text}\n\nActual output:\n${testContext.output}`);
1122
- });
1123
-
1124
- Then('the output does not contain {string}', function(text) {
1125
- assert(testContext.output, 'No output captured');
1126
- assert(!testContext.output.includes(text), `Output should not contain: ${text}\n\nActual output:\n${testContext.output}`);
1127
- });
1128
-
1129
- // Simple bug/feature creation for backward compatibility
1130
- Given('I create a bug {string}', function(title) {
1131
- try {
1132
- const output = execSync(
1133
- `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
1134
- { cwd: testDir, encoding: 'utf-8' }
1135
- );
1136
- testContext.lastOutput = output;
1137
- testContext.output = output;
1138
-
1139
- const match = output.match(/Created \w+ #(\d+):/);
1140
- if (match) {
1141
- testContext.createdItemId = parseInt(match[1]);
1142
- testContext.lastBugId = parseInt(match[1]);
1143
- testContext.lastCreatedId = parseInt(match[1]);
1144
- }
1145
- } catch (err) {
1146
- testContext.error = err.stderr || err.message;
1147
- }
1148
- });
1149
-
1150
- Given('I create a feature {string}', function(title) {
1151
- try {
1152
- const output = execSync(
1153
- `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
1154
- { cwd: testDir, encoding: 'utf-8' }
1155
- );
1156
- testContext.lastOutput = output;
1157
- testContext.output = output;
1158
-
1159
- const match = output.match(/Created \w+ #(\d+):/);
1160
- if (match) {
1161
- testContext.createdItemId = parseInt(match[1]);
1162
- testContext.lastFeatureId = parseInt(match[1]);
1163
- testContext.lastCreatedId = parseInt(match[1]);
1164
- }
1165
- } catch (err) {
1166
- testContext.error = err.stderr || err.message;
1167
- }
1168
- });
1169
-
1170
- // Cleanup after all scenarios complete (AfterAll allows async operations)
1171
- AfterAll(async function() {
1172
- const { closeDb } = require('../../lib/database');
1173
- await closeDb();
1174
- });