jettypod 3.0.1

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