jettypod 4.4.21 → 4.4.22

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.
@@ -0,0 +1,1233 @@
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_state>internal - Internal (team only, staging/preview - no external users)</project_state>
171
+ <mode>${mode}</mode>
172
+ </claude_context>`;
173
+ fs.writeFileSync(claudePath, content);
174
+ });
175
+
176
+ Given('I am on branch {string}', function (branch) {
177
+ try {
178
+ execSync(`git checkout -b ${branch}`, { stdio: 'pipe' });
179
+ } catch (e) {
180
+ execSync(`git checkout ${branch}`, { stdio: 'pipe' });
181
+ }
182
+ });
183
+
184
+ When('I run {string}', async function (command) {
185
+ const parts = command.split(' ');
186
+ if (parts[0] === 'jettypod' && parts[1] === 'work' && parts[2] === 'start') {
187
+ const id = parseInt(parts[3]);
188
+ const result = await workCommands.startWork(id);
189
+ testContext.output = `Working on: [#${result.workItem.id}] ${result.workItem.title} (${result.workItem.type})`;
190
+ if (result.workItem.parent_title) {
191
+ testContext.output = `Working on: [#${result.workItem.id}] ${result.workItem.title} (${result.workItem.type} of #${result.workItem.parent_id} ${result.workItem.parent_title})`;
192
+ }
193
+ } else if (parts[0] === 'jettypod' && (parts[1] === 'init' || parts.length === 1)) {
194
+ // Capture output for jettypod init
195
+ const originalLog = console.log;
196
+ let capturedOutput = '';
197
+ console.log = (...args) => {
198
+ capturedOutput += args.join(' ') + '\n';
199
+ originalLog(...args);
200
+ };
201
+
202
+ // Ensure testDir exists or use current directory
203
+ const workDir = (testDir && fs.existsSync(testDir)) ? testDir : process.cwd();
204
+
205
+ try {
206
+ const output = execSync(
207
+ `node ${path.join(__dirname, '../../jettypod.js')} ${parts.slice(1).join(' ')}`,
208
+ { cwd: workDir, encoding: 'utf-8' }
209
+ );
210
+ capturedOutput += output;
211
+ } catch (err) {
212
+ capturedOutput += err.stdout || '';
213
+ } finally {
214
+ console.log = originalLog;
215
+ }
216
+
217
+ testContext.commandOutput = capturedOutput;
218
+ testContext.initOutput = capturedOutput;
219
+ // Also set on 'this' for other step files (e.g., terminal-logo tests)
220
+ this.commandOutput = capturedOutput;
221
+ this.initOutput = capturedOutput;
222
+ } else if (parts[0] === 'jettypod') {
223
+ // Handle other jettypod commands (backlog, project external, etc.)
224
+ const workDir = (testDir && fs.existsSync(testDir)) ? testDir : process.cwd();
225
+
226
+ try {
227
+ const output = execSync(
228
+ `node ${path.join(__dirname, '../../jettypod.js')} ${parts.slice(1).join(' ')}`,
229
+ { cwd: workDir, encoding: 'utf-8' }
230
+ );
231
+ testContext.commandOutput = output;
232
+ this.commandOutput = output;
233
+ this.output = output; // For external-transition tests
234
+ this.error = null; // No error occurred
235
+ } catch (err) {
236
+ testContext.commandOutput = err.stdout || '';
237
+ this.commandOutput = err.stdout || '';
238
+ this.output = err.stdout || err.stderr || err.message; // For external-transition tests
239
+ this.error = err; // Store error for external-transition tests
240
+ }
241
+ }
242
+ });
243
+
244
+ When('I run {string} and enter status {string}', async function (command, status) {
245
+ await workCommands.stopWork(status);
246
+ });
247
+
248
+ Then('the current work file should contain work item {string}', function (id) {
249
+ assert(fs.existsSync(getCurrentWorkPath()), 'Current work file does not exist');
250
+ const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
251
+ assert.strictEqual(currentWork.id, parseInt(id));
252
+ });
253
+
254
+ Then('the work item {string} status should be {string}', function (id, status) {
255
+ const db = getTestDb();
256
+ return new Promise((resolve) => {
257
+ db.get(`SELECT status FROM work_items WHERE id = ?`, [parseInt(id)], (err, row) => {
258
+ // Removed db.close() - let Node.js handle cleanup
259
+ assert.strictEqual(row.status, status);
260
+ resolve();
261
+ });
262
+ });
263
+ });
264
+
265
+ Then('a feature branch {string} should be created', function (branchName) {
266
+ const branches = execSync('git branch', { encoding: 'utf-8' });
267
+ assert(branches.includes(branchName), `Branch ${branchName} not found`);
268
+ });
269
+
270
+ Then('CLAUDE.md current_work should show {string}', function (text) {
271
+ const content = fs.readFileSync(claudePath, 'utf-8');
272
+ assert(content.includes(text), `CLAUDE.md does not contain: ${text}`);
273
+ });
274
+
275
+ Then('the current work file should be empty', function () {
276
+ assert(!fs.existsSync(getCurrentWorkPath()), 'Current work file still exists');
277
+ });
278
+
279
+ Then('the output should contain {string}', function (text) {
280
+ assert(testContext.output && testContext.output.includes(text), `Output does not contain: ${text}`);
281
+ });
282
+
283
+ Then('CLAUDE.md mode should still be {string}', function (mode) {
284
+ const content = fs.readFileSync(claudePath, 'utf-8');
285
+ const modeMatch = content.match(/<mode>(.*?)<\/mode>/);
286
+ assert(modeMatch && modeMatch[1] === mode, `Mode is not ${mode}`);
287
+ });
288
+
289
+ Then('I should be on branch {string}', function (branchName) {
290
+ const currentBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
291
+ assert.strictEqual(currentBranch, branchName);
292
+ });
293
+
294
+ // Stable tests steps
295
+
296
+ Given('jettypod is initialized', async function () {
297
+ await setupTestEnv();
298
+ });
299
+
300
+ Given('jettypod is not initialized', function () {
301
+ // SAFETY: Only delete if testDir is in /tmp
302
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
303
+ fs.rmSync(testDir, { recursive: true, force: true });
304
+ }
305
+ fs.mkdirSync(testDir, { recursive: true });
306
+ process.chdir(testDir);
307
+ // Don't create .jettypod directory
308
+ });
309
+
310
+ Given('I have current work', async function () {
311
+ await setupTestEnv();
312
+ return new Promise((resolve) => {
313
+ const db = getTestDb();
314
+ db.run(`INSERT INTO work_items (id, type, title, status) VALUES (1, 'feature', 'Test Work', 'in_progress')`, () => {
315
+ // Removed db.close() - let Node.js handle cleanup
316
+ const currentWork = {
317
+ id: 1,
318
+ title: 'Test Work',
319
+ type: 'feature',
320
+ status: 'in_progress'
321
+ };
322
+ fs.writeFileSync(getCurrentWorkPath(), JSON.stringify(currentWork, null, 2));
323
+ resolve();
324
+ });
325
+ });
326
+ });
327
+
328
+ Given('no work is active', function () {
329
+ if (fs.existsSync(getCurrentWorkPath())) {
330
+ fs.unlinkSync(getCurrentWorkPath());
331
+ }
332
+ });
333
+
334
+ Given('current work file is corrupted', async function () {
335
+ await setupTestEnv();
336
+ fs.writeFileSync(getCurrentWorkPath(), 'invalid json {{{');
337
+ });
338
+
339
+ Given('jettypod is initialized without git', function (done) {
340
+ // Reset database singleton to avoid issues with previous tests
341
+ resetDb();
342
+
343
+ // SAFETY: Only delete if testDir is in /tmp
344
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
345
+ fs.rmSync(testDir, { recursive: true, force: true });
346
+ }
347
+ fs.mkdirSync(testDir, { recursive: true });
348
+ process.chdir(testDir); // Change directory BEFORE creating .jettypod
349
+ fs.mkdirSync(getJettypodDir(), { recursive: true });
350
+
351
+ // Initialize database but no git - use proper migrations
352
+ const db = getTestDb();
353
+ runMigrations(db).then(() => done()).catch(done);
354
+ });
355
+
356
+ Given('I have a work item', async function () {
357
+ // Check if we need full setup (including git) or just database
358
+ const dbPath = path.join(getJettypodDir(), process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db');
359
+ const needsSetup = !fs.existsSync(dbPath);
360
+
361
+ if (needsSetup) {
362
+ // Check if we're in a git-free test (testDir exists but no .git)
363
+ const isWithoutGit = fs.existsSync(testDir) && !fs.existsSync(path.join(testDir, '.git'));
364
+ if (!isWithoutGit) {
365
+ await setupTestEnv();
366
+ }
367
+ } else {
368
+ // Database exists, but clean up git state from previous scenarios
369
+ if (fs.existsSync(testDir) && fs.existsSync(path.join(testDir, '.git'))) {
370
+ try {
371
+ // Remove all feature branches
372
+ const branches = execSync('git branch', { cwd: testDir, encoding: 'utf8', stdio: 'pipe' });
373
+ branches.split('\n').forEach(branch => {
374
+ branch = branch.trim().replace('*', '').trim();
375
+ if (branch && branch.startsWith('feature/')) {
376
+ try {
377
+ execSync(`git branch -D "${branch}"`, { cwd: testDir, stdio: 'pipe' });
378
+ } catch (e) {
379
+ // Branch might be checked out or already deleted, ignore
380
+ }
381
+ }
382
+ });
383
+
384
+ // Prune worktrees
385
+ try {
386
+ execSync('git worktree prune', { cwd: testDir, stdio: 'pipe' });
387
+ } catch (e) {
388
+ // Ignore prune errors
389
+ }
390
+
391
+ // Remove worktree directories
392
+ const worktreeDir = path.join(testDir, '.jettypod-work');
393
+ if (fs.existsSync(worktreeDir)) {
394
+ fs.rmSync(worktreeDir, { recursive: true, force: true });
395
+ }
396
+ } catch (e) {
397
+ // If cleanup fails, ignore and continue
398
+ }
399
+ }
400
+ }
401
+
402
+ return new Promise((resolve) => {
403
+ const db = getTestDb();
404
+ db.run(`INSERT INTO work_items (id, type, title, status) VALUES (1, 'feature', 'Test Item', 'todo')`, () => {
405
+ testContext.workItemId = 1;
406
+ this.workItemId = 1; // Also set on this for git-hooks steps compatibility
407
+ resolve();
408
+ });
409
+ });
410
+ });
411
+
412
+ Given('I start work on it', function () {
413
+ // Check if workItemId is in testContext or this (from other step files)
414
+ const workItemId = testContext.workItemId || this.workItemId;
415
+
416
+ // Use CLI to avoid database singleton issues across test contexts
417
+ try {
418
+ testExecSync(`node ${path.join(__dirname, '../../jettypod.js')} work start ${workItemId}`, { cwd: testDir, stdio: 'pipe' });
419
+ testContext.firstWorkItemId = workItemId;
420
+ } catch (err) {
421
+ // If CLI fails, try direct module call
422
+ return workCommands.startWork(workItemId).then(result => {
423
+ testContext.firstWorkItemId = result.workItem.id;
424
+ });
425
+ }
426
+ });
427
+
428
+ When('I try to start work with ID {string}', async function (id) {
429
+ try {
430
+ await workCommands.startWork(id);
431
+ testContext.error = null;
432
+ } catch (err) {
433
+ testContext.error = err.message;
434
+ }
435
+ });
436
+
437
+ When('I try to stop work with status {string}', async function (status) {
438
+ try {
439
+ await workCommands.stopWork(status);
440
+ testContext.error = null;
441
+ } catch (err) {
442
+ testContext.error = err.message;
443
+ }
444
+ });
445
+
446
+ When('I try to stop work', async function () {
447
+ try {
448
+ await workCommands.stopWork();
449
+ testContext.error = null;
450
+ } catch (err) {
451
+ testContext.error = err.message;
452
+ }
453
+ });
454
+
455
+ When('I get current work', async function () {
456
+ try {
457
+ testContext.currentWork = await workCommands.getCurrentWork();
458
+ } catch (err) {
459
+ testContext.currentWork = null;
460
+ }
461
+ });
462
+
463
+ // Note: "I start work on the item" is defined in git-hooks/steps.js
464
+
465
+ When('I start work on a different item', async function () {
466
+ // Create a second work item
467
+ return new Promise((resolve) => {
468
+ const db = getTestDb();
469
+ db.run(`INSERT INTO work_items (id, type, title, status) VALUES (2, 'feature', 'Second Item', 'todo')`, async () => {
470
+ // Removed db.close() - let Node.js handle cleanup
471
+ testContext.secondWorkItemId = 2;
472
+ await workCommands.startWork(testContext.secondWorkItemId);
473
+ resolve();
474
+ });
475
+ });
476
+ });
477
+
478
+ Then('I get an error {string}', function (expectedError) {
479
+ assert(testContext.error, 'No error was thrown');
480
+ assert(testContext.error.includes(expectedError), `Expected error "${expectedError}" but got "${testContext.error}"`);
481
+ });
482
+
483
+ Then('operation succeeds with no changes', function () {
484
+ assert.strictEqual(testContext.error, null, 'Operation should not error');
485
+ });
486
+
487
+ Then('it returns null', function () {
488
+ assert.strictEqual(testContext.currentWork, null);
489
+ });
490
+
491
+ Then('it succeeds without creating branch', function () {
492
+ assert(!fs.existsSync(path.join(testDir, '.git')), 'Git directory should not exist');
493
+ assert(fs.existsSync(getCurrentWorkPath()), 'Current work file should exist');
494
+ });
495
+
496
+ Then('the first item stops being current', function () {
497
+ const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
498
+ assert.notStrictEqual(currentWork.id, testContext.firstWorkItemId);
499
+ });
500
+
501
+ Then('the second item becomes current', function () {
502
+ const currentWork = JSON.parse(fs.readFileSync(getCurrentWorkPath(), 'utf-8'));
503
+ assert.strictEqual(currentWork.id, testContext.secondWorkItemId);
504
+ });
505
+
506
+ Then('the status remains {string}', function (expectedStatus) {
507
+ const db = getTestDb();
508
+ return new Promise((resolve) => {
509
+ db.get(`SELECT status FROM work_items WHERE id = ?`, [testContext.workItemId], (err, row) => {
510
+ // Removed db.close() - let Node.js handle cleanup
511
+ assert.strictEqual(row.status, expectedStatus);
512
+ resolve();
513
+ });
514
+ });
515
+ });
516
+
517
+ // Mode-required steps - Epic
518
+ When('I create an epic {string} without mode', function(title) {
519
+ try {
520
+ const output = execSync(
521
+ `node ${path.join(__dirname, '../../jettypod.js')} work create epic "${title}"`,
522
+ { cwd: testDir, encoding: 'utf-8' }
523
+ );
524
+ testContext.lastOutput = output;
525
+
526
+ const match = output.match(/Created \w+ #(\d+):/);
527
+ if (match) {
528
+ testContext.createdItemId = parseInt(match[1]);
529
+ testContext.epicId = parseInt(match[1]); // Also set epicId for parent references
530
+ testContext.lastCreatedId = parseInt(match[1]); // For start work steps
531
+ }
532
+ } catch (err) {
533
+ testContext.error = err.stderr || err.message;
534
+ }
535
+ });
536
+
537
+ // Removed duplicate: When('I create an epic {string} with mode {string}')
538
+ // Using the Given version at line 538 instead
539
+
540
+ // Mode-required steps - Feature
541
+ When('I 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' }
546
+ );
547
+ testContext.lastOutput = output;
548
+
549
+ const match = output.match(/Created \w+ #(\d+):/);
550
+ if (match) {
551
+ testContext.createdItemId = parseInt(match[1]);
552
+ testContext.lastCreatedId = parseInt(match[1]); // For start work steps
553
+ testContext.lastFeatureId = parseInt(match[1]); // For type-specific start work steps
554
+ if (!testContext.createdItemIds) testContext.createdItemIds = [];
555
+ testContext.createdItemIds.push(testContext.createdItemId);
556
+ }
557
+ } catch (err) {
558
+ testContext.error = err.stderr || err.message;
559
+ }
560
+ });
561
+
562
+ When('I create a feature {string} without mode', function(title) {
563
+ try {
564
+ const output = execSync(
565
+ `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
566
+ { cwd: testDir, encoding: 'utf-8' }
567
+ );
568
+ testContext.lastOutput = output;
569
+
570
+ const match = output.match(/Created \w+ #(\d+):/);
571
+ if (match) {
572
+ testContext.createdItemId = parseInt(match[1]);
573
+ }
574
+ } catch (err) {
575
+ testContext.error = err.stderr || err.message;
576
+ }
577
+ });
578
+
579
+ When('I try to create a feature {string} without mode', function(title) {
580
+ try {
581
+ const output = execSync(
582
+ `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
583
+ { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
584
+ );
585
+ testContext.lastOutput = output;
586
+ } catch (err) {
587
+ testContext.error = err.stderr || err.message;
588
+ }
589
+ });
590
+
591
+ When('I try to create a feature {string} with mode {string}', function(title, mode) {
592
+ try {
593
+ const output = execSync(
594
+ `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}" "" --mode=${mode}`,
595
+ { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
596
+ );
597
+ testContext.lastOutput = output;
598
+ } catch (err) {
599
+ testContext.error = err.stderr || err.message;
600
+ }
601
+ });
602
+
603
+ Then('the work item is created successfully', function() {
604
+ assert(testContext.lastOutput.includes('Created'));
605
+ assert(typeof testContext.createdItemId === 'number');
606
+ });
607
+
608
+ Then('the work item has mode {string}', function(mode) {
609
+ const db = getTestDb();
610
+ return new Promise((resolve) => {
611
+ db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
612
+ // Removed db.close() - let Node.js handle cleanup
613
+ assert.strictEqual(row.mode, mode);
614
+ resolve();
615
+ });
616
+ });
617
+ });
618
+
619
+ Then('the work item has no mode', function() {
620
+ const db = getTestDb();
621
+ return new Promise((resolve) => {
622
+ db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
623
+ // Removed db.close() - let Node.js handle cleanup
624
+ assert.strictEqual(row.mode, null);
625
+ resolve();
626
+ });
627
+ });
628
+ });
629
+
630
+ Then('no work item is created', function() {
631
+ const db = getTestDb();
632
+ return new Promise((resolve) => {
633
+ db.get('SELECT COUNT(*) as count FROM work_items', [], (err, row) => {
634
+ // Removed db.close() - let Node.js handle cleanup
635
+ assert.strictEqual(row.count, 0);
636
+ resolve();
637
+ });
638
+ });
639
+ });
640
+
641
+ Given('I create an epic {string} with mode {string}', function(title, mode) {
642
+ const output = execSync(
643
+ `node ${path.join(__dirname, '../../jettypod.js')} work create epic "${title}" "" --mode=${mode}`,
644
+ { cwd: testDir, encoding: 'utf-8' }
645
+ );
646
+
647
+ const match = output.match(/Created \w+ #(\d+):/);
648
+ if (match) {
649
+ testContext.epicId = parseInt(match[1]);
650
+ }
651
+ });
652
+
653
+ Given('I create a feature {string} with mode {string} and parent epic', function(title, mode) {
654
+ const output = execSync(
655
+ `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}" "" --mode=${mode} --parent=${testContext.epicId}`,
656
+ { cwd: testDir, encoding: 'utf-8' }
657
+ );
658
+
659
+ const match = output.match(/Created \w+ #(\d+):/);
660
+ if (match) {
661
+ const id = parseInt(match[1]);
662
+ // Store based on mode to support multiple features with different modes
663
+ if (mode === 'speed') {
664
+ testContext.lastCreatedSpeedFeatureId = id;
665
+ } else if (mode === 'stable') {
666
+ testContext.lastCreatedStableFeatureId = id;
667
+ }
668
+ // Also store as generic lastCreatedId for backward compatibility
669
+ testContext.lastCreatedId = id;
670
+ }
671
+ });
672
+
673
+ When('I view the backlog', function() {
674
+ testContext.lastOutput = execSync(
675
+ `node ${path.join(__dirname, '../../jettypod.js')} backlog`,
676
+ { cwd: testDir, encoding: 'utf-8' }
677
+ );
678
+ });
679
+
680
+ Then('I see the epic with mode {string}', function(mode) {
681
+ assert(testContext.lastOutput.includes('Test Epic'));
682
+ });
683
+
684
+ Then('I see the child feature with mode {string}', function(mode) {
685
+ assert(testContext.lastOutput.includes('Child Feature'));
686
+ assert(testContext.lastOutput.includes(`[${mode}]`));
687
+ });
688
+
689
+ // Bug steps
690
+ When('I create a bug {string} with mode {string}', function(title, mode) {
691
+ try {
692
+ const output = execSync(
693
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --mode=${mode}`,
694
+ { cwd: testDir, encoding: 'utf-8' }
695
+ );
696
+ testContext.lastOutput = output;
697
+
698
+ const match = output.match(/Created \w+ #(\d+):/);
699
+ if (match) {
700
+ testContext.createdItemId = parseInt(match[1]);
701
+ testContext.lastCreatedId = parseInt(match[1]); // For start work steps
702
+ testContext.lastBugId = parseInt(match[1]); // For type-specific start work steps
703
+ }
704
+ } catch (err) {
705
+ testContext.error = err.stderr || err.message;
706
+ }
707
+ });
708
+
709
+ When('I create a bug {string} without mode', function(title) {
710
+ try {
711
+ const output = execSync(
712
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
713
+ { cwd: testDir, encoding: 'utf-8' }
714
+ );
715
+ testContext.lastOutput = output;
716
+
717
+ const match = output.match(/Created \w+ #(\d+):/);
718
+ if (match) {
719
+ testContext.createdItemId = parseInt(match[1]);
720
+ }
721
+ } catch (err) {
722
+ testContext.error = err.stderr || err.message;
723
+ }
724
+ });
725
+
726
+ When('I try to create a bug {string} without mode', function(title) {
727
+ try {
728
+ const output = execSync(
729
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
730
+ { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
731
+ );
732
+ testContext.lastOutput = output;
733
+ } catch (err) {
734
+ testContext.error = err.stderr || err.message;
735
+ }
736
+ });
737
+
738
+ When('I try to create a bug {string} with mode {string}', function(title, mode) {
739
+ try {
740
+ const output = execSync(
741
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --mode=${mode}`,
742
+ { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
743
+ );
744
+ testContext.lastOutput = output;
745
+ } catch (err) {
746
+ testContext.error = err.stderr || err.message;
747
+ }
748
+ });
749
+
750
+ // Chore steps
751
+ When('I try to create a chore {string} with mode {string}', function(title, mode) {
752
+ try {
753
+ execSync(
754
+ `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --mode=${mode}`,
755
+ { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
756
+ );
757
+ testContext.error = null;
758
+ } catch (err) {
759
+ testContext.error = err.stderr || err.message;
760
+ }
761
+ });
762
+
763
+ When('I create a chore {string} without mode', function(title) {
764
+ try {
765
+ const output = execSync(
766
+ `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}"`,
767
+ { cwd: testDir, encoding: 'utf-8' }
768
+ );
769
+ testContext.lastOutput = output;
770
+
771
+ const match = output.match(/Created \w+ #(\d+):/);
772
+ if (match) {
773
+ testContext.createdItemId = parseInt(match[1]);
774
+ }
775
+ } catch (err) {
776
+ testContext.error = err.stderr || err.message;
777
+ }
778
+ });
779
+
780
+ When('I try to create a chore {string} without mode', function(title) {
781
+ try {
782
+ const output = execSync(
783
+ `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}"`,
784
+ { cwd: testDir, encoding: 'utf-8', stderr: 'pipe' }
785
+ );
786
+ testContext.lastOutput = output;
787
+ } catch (err) {
788
+ testContext.error = err.stderr || err.message;
789
+ }
790
+ });
791
+
792
+ // Additional assertions
793
+ Then('the work item has NULL mode', function() {
794
+ const db = getTestDb();
795
+ return new Promise((resolve) => {
796
+ db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.createdItemId], (err, row) => {
797
+ // Removed db.close() - let Node.js handle cleanup
798
+ assert.strictEqual(row.mode, null);
799
+ resolve();
800
+ });
801
+ });
802
+ });
803
+
804
+ Given('I create a bug {string} with parent epic', function(title) {
805
+ const output = execSync(
806
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}" "" --parent=${testContext.epicId}`,
807
+ { cwd: testDir, encoding: 'utf-8' }
808
+ );
809
+
810
+ const match = output.match(/Created \w+ #(\d+):/);
811
+ if (match) {
812
+ testContext.lastCreatedStableBugId = parseInt(match[1]); // For hierarchical scenarios
813
+ }
814
+ });
815
+
816
+ Given('I create a chore {string} without mode and parent epic', function(title) {
817
+ const output = execSync(
818
+ `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --parent=${testContext.epicId}`,
819
+ { cwd: testDir, encoding: 'utf-8' }
820
+ );
821
+
822
+ const match = output.match(/Created \w+ #(\d+):/);
823
+ if (match) {
824
+ testContext.lastChoreId = parseInt(match[1]);
825
+ }
826
+ });
827
+
828
+ Given('I create a chore {string} without mode and parent feature', function(title) {
829
+ const output = execSync(
830
+ `node ${path.join(__dirname, '../../jettypod.js')} work create chore "${title}" "" --parent=${testContext.lastFeatureId}`,
831
+ { cwd: testDir, encoding: 'utf-8' }
832
+ );
833
+
834
+ const match = output.match(/Created \w+ #(\d+):/);
835
+ if (match) {
836
+ testContext.lastChoreId = parseInt(match[1]);
837
+ }
838
+ });
839
+
840
+ Then('I see the feature with mode {string}', function(mode) {
841
+ assert(testContext.lastOutput.includes('Speed Feature'));
842
+ assert(testContext.lastOutput.includes(`[${mode}]`));
843
+ });
844
+
845
+ Then('I see the bug with mode {string}', function(mode) {
846
+ assert(testContext.lastOutput.includes('Stable Bug'));
847
+ assert(testContext.lastOutput.includes(`[${mode}]`));
848
+ });
849
+
850
+ Then('I see the chore with mode {string}', function(mode) {
851
+ assert(testContext.lastOutput.includes('Production Chore'));
852
+ assert(testContext.lastOutput.includes(`[${mode}]`));
853
+ });
854
+
855
+ Then('I see the epic without mode indicator', function() {
856
+ assert(testContext.lastOutput.includes('Test Epic'));
857
+ // Epic should NOT have a mode indicator
858
+ const epicLine = testContext.lastOutput.split('\n').find(line => line.includes('Test Epic'));
859
+ assert(!epicLine.match(/\[(speed|discovery|stable|production)\]/), 'Epic should not have a mode indicator');
860
+ });
861
+
862
+ Then('I see the chore without mode indicator', function() {
863
+ assert(testContext.lastOutput.includes('Test Chore'));
864
+ // Chore should NOT have a mode indicator
865
+ const choreLine = testContext.lastOutput.split('\n').find(line => line.includes('Test Chore'));
866
+ assert(!choreLine.match(/\[(speed|discovery|stable|production)\]/), 'Chore should not have a mode indicator');
867
+ });
868
+
869
+ Then('I see the bug without mode indicator', function() {
870
+ assert(testContext.lastOutput.includes('Test Bug'));
871
+ // Bug should NOT have a mode indicator
872
+ const bugLine = testContext.lastOutput.split('\n').find(line => line.includes('Test Bug'));
873
+ assert(!bugLine.match(/\[(speed|discovery|stable|production)\]/), 'Bug should not have a mode indicator');
874
+ });
875
+
876
+ Then('both work items are created successfully', function() {
877
+ assert(testContext.createdItemIds);
878
+ assert(testContext.createdItemIds.length >= 2);
879
+ });
880
+
881
+ Then('they have different modes', function() {
882
+ const db = getTestDb();
883
+ const placeholders = testContext.createdItemIds.map(() => '?').join(',');
884
+ return new Promise((resolve, reject) => {
885
+ db.all(`SELECT mode FROM work_items WHERE id IN (${placeholders})`, ...testContext.createdItemIds, (err, rows) => {
886
+ if (err) {
887
+ return reject(err);
888
+ }
889
+ assert.strictEqual(rows.length, 2);
890
+ assert.notStrictEqual(rows[0].mode, rows[1].mode);
891
+ resolve();
892
+ });
893
+ });
894
+ });
895
+
896
+ // Steps for work-start-mode.feature
897
+ Given('CLAUDE.md exists', function() {
898
+ const claudePath = path.join(testDir, 'CLAUDE.md');
899
+ const content = `<claude_context project="test">
900
+ <current_work>
901
+ Working on: [#1] Test Item (feature)
902
+ Mode: speed
903
+ Status: in_progress
904
+ </current_work>
905
+ <mode>speed</mode>
906
+ </claude_context>`;
907
+ fs.writeFileSync(claudePath, content);
908
+ });
909
+
910
+ Given('the feature status is {string}', function(status) {
911
+ const db = getTestDb();
912
+ return new Promise((resolve, reject) => {
913
+ db.run('UPDATE work_items SET status = ? WHERE id = ?', [status, testContext.lastCreatedId], (err) => {
914
+ // Removed db.close() - let Node.js handle cleanup
915
+ if (err) reject(err);
916
+ else resolve();
917
+ });
918
+ });
919
+ });
920
+
921
+ Given('CLAUDE.md has mode {string}', function(mode) {
922
+ const claudePath = path.join(testDir, 'CLAUDE.md');
923
+ const content = `<claude_context project="test">
924
+ <current_work>
925
+ Working on: [#1] Test Item (feature)
926
+ Mode: ${mode}
927
+ Status: in_progress
928
+ </current_work>
929
+ <mode>${mode}</mode>
930
+ </claude_context>`;
931
+ fs.writeFileSync(claudePath, content);
932
+ });
933
+
934
+ When('I start work on the feature', async function() {
935
+ const { startWork } = require('../../features/work-commands');
936
+ // Capture console.log output
937
+ const originalLog = console.log;
938
+ let capturedOutput = '';
939
+ console.log = (...args) => {
940
+ capturedOutput += args.join(' ') + '\n';
941
+ originalLog(...args);
942
+ };
943
+ testContext.result = await startWork(testContext.lastFeatureId);
944
+ console.log = originalLog;
945
+ testContext.output = capturedOutput;
946
+ });
947
+
948
+ When('I start work on the bug', async function() {
949
+ const { startWork } = require('../../features/work-commands');
950
+ // Capture console.log output
951
+ const originalLog = console.log;
952
+ let capturedOutput = '';
953
+ console.log = (...args) => {
954
+ capturedOutput += args.join(' ') + '\n';
955
+ originalLog(...args);
956
+ };
957
+ testContext.result = await startWork(testContext.lastBugId);
958
+ console.log = originalLog;
959
+ testContext.output = capturedOutput;
960
+ });
961
+
962
+ When('I start work on the chore', async function() {
963
+ const { startWork } = require('../../features/work-commands');
964
+ testContext.result = await startWork(testContext.lastChoreId);
965
+ });
966
+
967
+ When('I start work on the epic', async function() {
968
+ const { startWork } = require('../../features/work-commands');
969
+ testContext.result = await startWork(testContext.lastCreatedId);
970
+ });
971
+
972
+ When('I start work on the speed feature', async function() {
973
+ const { startWork } = require('../../features/work-commands');
974
+ testContext.result = await startWork(testContext.lastCreatedSpeedFeatureId);
975
+ });
976
+
977
+ When('I start work on the stable bug', async function() {
978
+ const { startWork } = require('../../features/work-commands');
979
+ testContext.result = await startWork(testContext.lastCreatedStableBugId);
980
+ });
981
+
982
+ When('I start work on the stable feature', async function() {
983
+ const { startWork } = require('../../features/work-commands');
984
+ // Find the stable feature ID from the last created items
985
+ const { getDb } = require('../../lib/database');
986
+ const db = getDb();
987
+ return new Promise((resolve) => {
988
+ db.get('SELECT id FROM work_items WHERE title = ? AND mode = ?', ['Stable Feature', 'stable'], (err, row) => {
989
+ if (row) {
990
+ startWork(row.id).then(() => resolve());
991
+ } else {
992
+ resolve();
993
+ }
994
+ });
995
+ });
996
+ });
997
+
998
+ When('I stop work', async function() {
999
+ const { stopWork } = require('../../features/work-commands');
1000
+ await stopWork();
1001
+ });
1002
+
1003
+ Then('CLAUDE.md mode is set to {string}', function(expectedMode) {
1004
+ // Now reads from session file instead of CLAUDE.md
1005
+ const sessionPath = path.join(process.cwd(), '.claude', 'session.md');
1006
+ if (!fs.existsSync(sessionPath)) {
1007
+ assert.fail('Session file should exist at .claude/session.md');
1008
+ }
1009
+ const content = fs.readFileSync(sessionPath, 'utf-8');
1010
+ const modeMatch = content.match(/^Mode: (.+)$/m);
1011
+ assert(modeMatch, 'Session file should have a Mode line');
1012
+ assert.strictEqual(modeMatch[1], expectedMode);
1013
+ });
1014
+
1015
+ Then('CLAUDE.md has no mode line', function() {
1016
+ // For epics, no session file should be created
1017
+ const sessionPath = path.join(process.cwd(), '.claude', 'session.md');
1018
+ assert(!fs.existsSync(sessionPath), 'Session file should not exist for epics (no mode)');
1019
+ });
1020
+
1021
+ Then('the current work section exists', function() {
1022
+ // Check that current work is set in database (works for all items including epics)
1023
+ const { getCurrentWork } = require('../../lib/current-work');
1024
+ const currentWork = getCurrentWork();
1025
+ assert(currentWork, 'Current work should be set in database');
1026
+ });
1027
+
1028
+ Then('the work item still has mode {string}', function(expectedMode) {
1029
+ const db = getTestDb();
1030
+ return new Promise((resolve, reject) => {
1031
+ db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.lastCreatedId], (err, row) => {
1032
+ // Removed db.close() - let Node.js handle cleanup
1033
+ if (err) {
1034
+ reject(err);
1035
+ } else {
1036
+ assert.strictEqual(row.mode, expectedMode);
1037
+ resolve();
1038
+ }
1039
+ });
1040
+ });
1041
+ });
1042
+
1043
+ // Steps for work-set-mode.feature
1044
+ When('I set mode for current item to {string}', async function(mode) {
1045
+ try {
1046
+ // Get current work item ID from database
1047
+ const { getCurrentWork } = require('../../lib/current-work');
1048
+ const currentWork = await getCurrentWork();
1049
+ if (!currentWork) {
1050
+ testContext.error = 'No current work item';
1051
+ return;
1052
+ }
1053
+ execSync(
1054
+ `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${currentWork.id} ${mode}`,
1055
+ { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1056
+ );
1057
+ testContext.error = null;
1058
+ } catch (err) {
1059
+ testContext.error = err.message || err;
1060
+ }
1061
+ });
1062
+
1063
+ When('I set mode for item {string} to {string}', function(title, mode) {
1064
+ const db = getTestDb();
1065
+ return new Promise((resolve) => {
1066
+ db.get('SELECT id FROM work_items WHERE title = ?', [title], (err, row) => {
1067
+ // Removed db.close() - let Node.js handle cleanup
1068
+ if (err || !row) {
1069
+ testContext.error = 'Work item not found';
1070
+ resolve();
1071
+ return;
1072
+ }
1073
+ try {
1074
+ execSync(
1075
+ `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${row.id} ${mode}`,
1076
+ { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1077
+ );
1078
+ testContext.error = null;
1079
+ } catch (err) {
1080
+ testContext.error = err.message || err;
1081
+ }
1082
+ resolve();
1083
+ });
1084
+ });
1085
+ });
1086
+
1087
+ When('I set mode for the epic to {string}', function(mode) {
1088
+ try {
1089
+ execSync(
1090
+ `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${testContext.epicId} ${mode}`,
1091
+ { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1092
+ );
1093
+ testContext.error = null;
1094
+ } catch (err) {
1095
+ testContext.error = err.message || err;
1096
+ }
1097
+ });
1098
+
1099
+ When('I try to set mode to {string}', function(mode) {
1100
+ try {
1101
+ // Get work item ID from testContext (set by create/start commands)
1102
+ const workItemId = testContext.createdItemId || testContext.workItemId || testContext.firstWorkItemId;
1103
+
1104
+ if (!workItemId) {
1105
+ throw new Error('No work item ID found in test context');
1106
+ }
1107
+
1108
+ try {
1109
+ testExecSync(
1110
+ `node ${path.join(__dirname, '../../jettypod.js')} work set-mode ${workItemId} ${mode}`,
1111
+ { cwd: testDir, encoding: 'utf-8', stdio: 'pipe' }
1112
+ );
1113
+ testContext.error = null;
1114
+ } catch (cmdErr) {
1115
+ // Check if error is due to invalid mode
1116
+ const errOutput = cmdErr.stderr || cmdErr.message;
1117
+ if (errOutput.includes('Invalid mode')) {
1118
+ testContext.error = 'Invalid mode';
1119
+ } else {
1120
+ testContext.error = cmdErr.message || cmdErr;
1121
+ }
1122
+ }
1123
+ } catch (err) {
1124
+ testContext.error = err.message || err;
1125
+ }
1126
+ });
1127
+
1128
+ Then('I get error {string}', function(expectedError) {
1129
+ assert(testContext.error, 'Expected an error but got none');
1130
+ assert(testContext.error.includes(expectedError), `Expected error to include "${expectedError}" but got: ${testContext.error}`);
1131
+ });
1132
+
1133
+ Then('item {string} has mode {string}', function(title, expectedMode) {
1134
+ const db = getTestDb();
1135
+ return new Promise((resolve) => {
1136
+ db.get('SELECT mode FROM work_items WHERE title = ?', [title], (err, row) => {
1137
+ // Removed db.close() - let Node.js handle cleanup
1138
+ assert.strictEqual(row.mode, expectedMode);
1139
+ resolve();
1140
+ });
1141
+ });
1142
+ });
1143
+
1144
+ Then('the epic has mode {string}', function(expectedMode) {
1145
+ const db = getTestDb();
1146
+ return new Promise((resolve) => {
1147
+ db.get('SELECT mode FROM work_items WHERE id = ?', [testContext.epicId], (err, row) => {
1148
+ // Removed db.close() - let Node.js handle cleanup
1149
+ assert.strictEqual(row.mode, expectedMode);
1150
+ resolve();
1151
+ });
1152
+ });
1153
+ });
1154
+
1155
+ Then('CLAUDE.md still has no mode line', function() {
1156
+ const claudePath = path.join(testDir, 'CLAUDE.md');
1157
+ const content = fs.readFileSync(claudePath, 'utf-8');
1158
+ const modeMatch = content.match(/^Mode: (.+)$/m);
1159
+ assert(!modeMatch, 'CLAUDE.md should not have a Mode line for epics');
1160
+ });
1161
+
1162
+ Given('I start work on the feature {string}', async function(title) {
1163
+ const { startWork } = require('../../features/work-commands');
1164
+ const db = getTestDb();
1165
+ return new Promise((resolve) => {
1166
+ db.get('SELECT id FROM work_items WHERE title = ?', [title], async (err, row) => {
1167
+ // Removed db.close() - let Node.js handle cleanup
1168
+ if (err || !row) {
1169
+ throw new Error('Work item not found');
1170
+ }
1171
+ testContext.result = await startWork(row.id);
1172
+ resolve();
1173
+ });
1174
+ });
1175
+ });
1176
+
1177
+ // Steps for bug-workflow-display.feature
1178
+ Then('the output contains {string}', function(text) {
1179
+ assert(testContext.output, 'No output captured');
1180
+ assert(testContext.output.includes(text), `Output does not contain: ${text}\n\nActual output:\n${testContext.output}`);
1181
+ });
1182
+
1183
+ Then('the output does not contain {string}', function(text) {
1184
+ assert(testContext.output, 'No output captured');
1185
+ assert(!testContext.output.includes(text), `Output should not contain: ${text}\n\nActual output:\n${testContext.output}`);
1186
+ });
1187
+
1188
+ // Simple bug/feature creation for backward compatibility
1189
+ Given('I create a bug {string}', function(title) {
1190
+ try {
1191
+ const output = execSync(
1192
+ `node ${path.join(__dirname, '../../jettypod.js')} work create bug "${title}"`,
1193
+ { cwd: testDir, encoding: 'utf-8' }
1194
+ );
1195
+ testContext.lastOutput = output;
1196
+ testContext.output = output;
1197
+
1198
+ const match = output.match(/Created \w+ #(\d+):/);
1199
+ if (match) {
1200
+ testContext.createdItemId = parseInt(match[1]);
1201
+ testContext.lastBugId = parseInt(match[1]);
1202
+ testContext.lastCreatedId = parseInt(match[1]);
1203
+ }
1204
+ } catch (err) {
1205
+ testContext.error = err.stderr || err.message;
1206
+ }
1207
+ });
1208
+
1209
+ Given('I create a feature {string}', function(title) {
1210
+ try {
1211
+ const output = execSync(
1212
+ `node ${path.join(__dirname, '../../jettypod.js')} work create feature "${title}"`,
1213
+ { cwd: testDir, encoding: 'utf-8' }
1214
+ );
1215
+ testContext.lastOutput = output;
1216
+ testContext.output = output;
1217
+
1218
+ const match = output.match(/Created \w+ #(\d+):/);
1219
+ if (match) {
1220
+ testContext.createdItemId = parseInt(match[1]);
1221
+ testContext.lastFeatureId = parseInt(match[1]);
1222
+ testContext.lastCreatedId = parseInt(match[1]);
1223
+ }
1224
+ } catch (err) {
1225
+ testContext.error = err.stderr || err.message;
1226
+ }
1227
+ });
1228
+
1229
+ // Cleanup after all scenarios complete (AfterAll allows async operations)
1230
+ AfterAll(async function() {
1231
+ const { closeDb } = require('../../lib/database');
1232
+ await closeDb();
1233
+ });