jettypod 4.4.19 → 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.
- package/jettypod.js +12 -12
- package/lib/decisions/index.js +490 -0
- package/lib/git-hooks/git-hooks.feature +30 -0
- package/lib/git-hooks/index.js +94 -0
- package/lib/git-hooks/post-commit +59 -0
- package/lib/git-hooks/post-merge +71 -0
- package/lib/git-hooks/pre-commit +28 -0
- package/lib/git-hooks/simple-steps.js +53 -0
- package/lib/git-hooks/simple-test.feature +10 -0
- package/lib/git-hooks/steps.js +196 -0
- package/lib/mode-prompts/index.js +95 -0
- package/lib/mode-prompts/simple-steps.js +44 -0
- package/lib/mode-prompts/simple-test.feature +9 -0
- package/lib/terminal-logo/index.js +39 -0
- package/lib/terminal-logo/terminal-logo.feature +30 -0
- package/lib/update-command/index.js +181 -0
- package/lib/work-commands/bug-workflow-display.feature +22 -0
- package/lib/work-commands/index.js +1603 -0
- package/lib/work-commands/simple-steps.js +69 -0
- package/lib/work-commands/stable-tests.feature +57 -0
- package/lib/work-commands/steps.js +1233 -0
- package/lib/work-commands/work-commands.feature +13 -0
- package/lib/work-commands/worktree-management.feature +63 -0
- package/lib/work-tracking/index.js +2396 -0
- package/lib/work-tracking/mode-required.feature +111 -0
- package/lib/work-tracking/work-set-mode.feature +70 -0
- package/lib/work-tracking/work-start-mode.feature +83 -0
- package/lib/worktree-manager.js +19 -13
- package/package.json +1 -1
|
@@ -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
|
+
});
|