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