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,216 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createTestEnvironment } = require('../../lib/test-helpers');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* End-to-end tests for skills update functionality
|
|
8
|
+
* Tests the BDD scenarios from features/always-update-skills.feature
|
|
9
|
+
*/
|
|
10
|
+
describe('Skills Update on JettyPod Init', () => {
|
|
11
|
+
let testEnv;
|
|
12
|
+
let jettypodPath;
|
|
13
|
+
let skillsSourceDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
testEnv = createTestEnvironment();
|
|
17
|
+
process.chdir(testEnv.testDir);
|
|
18
|
+
|
|
19
|
+
// Path to jettypod script (relative to test directory)
|
|
20
|
+
jettypodPath = path.join(__dirname, '..', '..', 'jettypod.js');
|
|
21
|
+
|
|
22
|
+
// SAFETY: Create mock skills source directory in /tmp/ test environment, NOT in actual project
|
|
23
|
+
skillsSourceDir = path.join(testEnv.testDir, 'mock-jettypod-skills');
|
|
24
|
+
if (!fs.existsSync(skillsSourceDir)) {
|
|
25
|
+
fs.mkdirSync(skillsSourceDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create sample skills for testing
|
|
29
|
+
const speedModeSkillDir = path.join(skillsSourceDir, 'speed-mode');
|
|
30
|
+
fs.mkdirSync(speedModeSkillDir, { recursive: true });
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(speedModeSkillDir, 'SKILL.md'),
|
|
33
|
+
'# Speed Mode Skill\nTest content for speed mode skill'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const stableModeSkillDir = path.join(skillsSourceDir, 'stable-mode');
|
|
37
|
+
fs.mkdirSync(stableModeSkillDir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(stableModeSkillDir, 'SKILL.md'),
|
|
40
|
+
'# Stable Mode Skill\nTest content for stable mode skill'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Configure jettypod to use mock skills source
|
|
44
|
+
process.env.JETTYPOD_SKILLS_SOURCE_DIR = skillsSourceDir;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// Clean up environment variable
|
|
49
|
+
delete process.env.JETTYPOD_SKILLS_SOURCE_DIR;
|
|
50
|
+
testEnv.cleanup();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('Scenario: Skills are updated when initializing existing project', () => {
|
|
54
|
+
test('should backup existing skills before updating', () => {
|
|
55
|
+
// Given: I have a project with old .claude/skills/ directory
|
|
56
|
+
const claudeDir = path.join(testEnv.testDir, '.claude');
|
|
57
|
+
const oldSkillsDir = path.join(claudeDir, 'skills');
|
|
58
|
+
fs.mkdirSync(oldSkillsDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const oldSkillDir = path.join(oldSkillsDir, 'old-skill');
|
|
61
|
+
fs.mkdirSync(oldSkillDir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(oldSkillDir, 'SKILL.md'),
|
|
64
|
+
'# Old Skill\nThis is an old skill that should be backed up'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// When: I run jettypod in the project directory
|
|
68
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
69
|
+
|
|
70
|
+
// Then: old skills are backed up before replacement
|
|
71
|
+
const backupDirs = fs.readdirSync(claudeDir).filter(name => name.startsWith('skills.backup-'));
|
|
72
|
+
expect(backupDirs.length).toBe(1);
|
|
73
|
+
|
|
74
|
+
const backupDir = path.join(claudeDir, backupDirs[0]);
|
|
75
|
+
expect(fs.existsSync(backupDir)).toBe(true);
|
|
76
|
+
|
|
77
|
+
// Verify old skill is in backup
|
|
78
|
+
const backedUpSkillFile = path.join(backupDir, 'old-skill', 'SKILL.md');
|
|
79
|
+
expect(fs.existsSync(backedUpSkillFile)).toBe(true);
|
|
80
|
+
const backedUpContent = fs.readFileSync(backedUpSkillFile, 'utf8');
|
|
81
|
+
expect(backedUpContent).toContain('This is an old skill that should be backed up');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should replace skills with latest from jettypod', () => {
|
|
85
|
+
// Given: I have a project with old .claude/skills/ directory
|
|
86
|
+
const claudeDir = path.join(testEnv.testDir, '.claude');
|
|
87
|
+
const oldSkillsDir = path.join(claudeDir, 'skills');
|
|
88
|
+
fs.mkdirSync(oldSkillsDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
const oldSkillDir = path.join(oldSkillsDir, 'old-skill');
|
|
91
|
+
fs.mkdirSync(oldSkillDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(oldSkillDir, 'SKILL.md'), '# Old Skill');
|
|
93
|
+
|
|
94
|
+
// When: I run jettypod in the project directory
|
|
95
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
96
|
+
|
|
97
|
+
// Then: .claude/skills/ is replaced with latest skills from jettypod
|
|
98
|
+
const newSkillsDir = path.join(testEnv.testDir, '.claude', 'skills');
|
|
99
|
+
expect(fs.existsSync(newSkillsDir)).toBe(true);
|
|
100
|
+
|
|
101
|
+
// Old skill should be gone
|
|
102
|
+
expect(fs.existsSync(path.join(newSkillsDir, 'old-skill'))).toBe(false);
|
|
103
|
+
|
|
104
|
+
// New skills should be present
|
|
105
|
+
expect(fs.existsSync(path.join(newSkillsDir, 'speed-mode', 'SKILL.md'))).toBe(true);
|
|
106
|
+
expect(fs.existsSync(path.join(newSkillsDir, 'stable-mode', 'SKILL.md'))).toBe(true);
|
|
107
|
+
|
|
108
|
+
// Verify content is from jettypod source
|
|
109
|
+
const speedModeContent = fs.readFileSync(
|
|
110
|
+
path.join(newSkillsDir, 'speed-mode', 'SKILL.md'),
|
|
111
|
+
'utf8'
|
|
112
|
+
);
|
|
113
|
+
expect(speedModeContent).toContain('Test content for speed mode skill');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should handle concurrent inits with unique backup names', () => {
|
|
117
|
+
// Given: I have a project with .claude/skills/ directory
|
|
118
|
+
const claudeDir = path.join(testEnv.testDir, '.claude');
|
|
119
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
120
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
121
|
+
fs.writeFileSync(path.join(skillsDir, 'test.md'), 'test');
|
|
122
|
+
|
|
123
|
+
// Manually create a backup with current timestamp format
|
|
124
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
125
|
+
const existingBackup = path.join(claudeDir, `skills.backup-${timestamp}`);
|
|
126
|
+
fs.mkdirSync(existingBackup, { recursive: true });
|
|
127
|
+
fs.writeFileSync(path.join(existingBackup, 'existing.md'), 'existing');
|
|
128
|
+
|
|
129
|
+
// Restore skills dir for the actual init
|
|
130
|
+
if (fs.existsSync(skillsDir)) {
|
|
131
|
+
fs.rmSync(skillsDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(skillsDir, 'test.md'), 'test');
|
|
135
|
+
|
|
136
|
+
// When: I run jettypod (which creates another backup with same timestamp)
|
|
137
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
138
|
+
|
|
139
|
+
// Then: backup directory should have counter appended
|
|
140
|
+
const backupDirs = fs.readdirSync(claudeDir)
|
|
141
|
+
.filter(name => name.startsWith('skills.backup-'))
|
|
142
|
+
.sort();
|
|
143
|
+
|
|
144
|
+
// Should have at least 2 backups (the manual one and the new one)
|
|
145
|
+
expect(backupDirs.length).toBeGreaterThanOrEqual(2);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Scenario: Skills are copied when initializing new project', () => {
|
|
150
|
+
test('should create .claude/skills/ with latest skills', () => {
|
|
151
|
+
// Given: I have a new project with no .claude/ directory
|
|
152
|
+
// (testEnv starts clean)
|
|
153
|
+
|
|
154
|
+
// When: I run jettypod to initialize
|
|
155
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
156
|
+
|
|
157
|
+
// Then: .claude/skills/ is created with latest skills from jettypod
|
|
158
|
+
const skillsDir = path.join(testEnv.testDir, '.claude', 'skills');
|
|
159
|
+
expect(fs.existsSync(skillsDir)).toBe(true);
|
|
160
|
+
|
|
161
|
+
// And: all skills are ready to use
|
|
162
|
+
expect(fs.existsSync(path.join(skillsDir, 'speed-mode', 'SKILL.md'))).toBe(true);
|
|
163
|
+
expect(fs.existsSync(path.join(skillsDir, 'stable-mode', 'SKILL.md'))).toBe(true);
|
|
164
|
+
|
|
165
|
+
const speedModeContent = fs.readFileSync(
|
|
166
|
+
path.join(skillsDir, 'speed-mode', 'SKILL.md'),
|
|
167
|
+
'utf8'
|
|
168
|
+
);
|
|
169
|
+
expect(speedModeContent).toContain('Test content for speed mode skill');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should not create backup when no existing skills', () => {
|
|
173
|
+
// Given: I have a new project with no .claude/ directory
|
|
174
|
+
// When: I run jettypod to initialize
|
|
175
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
176
|
+
|
|
177
|
+
// Then: no backup should be created
|
|
178
|
+
const claudeDir = path.join(testEnv.testDir, '.claude');
|
|
179
|
+
const backupDirs = fs.readdirSync(claudeDir).filter(name => name.startsWith('skills.backup-'));
|
|
180
|
+
expect(backupDirs.length).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Error handling validation', () => {
|
|
185
|
+
test('should handle source directory missing gracefully', () => {
|
|
186
|
+
// Remove skills source directory
|
|
187
|
+
if (fs.existsSync(skillsSourceDir)) {
|
|
188
|
+
fs.rmSync(skillsSourceDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Should not throw, just warn
|
|
192
|
+
expect(() => {
|
|
193
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
194
|
+
}).not.toThrow();
|
|
195
|
+
|
|
196
|
+
// .claude should still be created
|
|
197
|
+
expect(fs.existsSync(path.join(testEnv.testDir, '.claude'))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should handle empty source directory gracefully', () => {
|
|
201
|
+
// Empty the skills source directory
|
|
202
|
+
if (fs.existsSync(skillsSourceDir)) {
|
|
203
|
+
fs.rmSync(skillsSourceDir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
fs.mkdirSync(skillsSourceDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
// Should not throw, just warn
|
|
208
|
+
expect(() => {
|
|
209
|
+
execSync(`node ${jettypodPath}`, { stdio: 'pipe', env: { ...process.env } });
|
|
210
|
+
}).not.toThrow();
|
|
211
|
+
|
|
212
|
+
// .claude should still be created
|
|
213
|
+
expect(fs.existsSync(path.join(testEnv.testDir, '.claude'))).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
|
|
7
|
+
const testDir = path.join('/tmp', 'jettypod-production-chores-test-' + Date.now());
|
|
8
|
+
const jettypodDir = path.join(testDir, '.jettypod');
|
|
9
|
+
const configPath = path.join(jettypodDir, 'config.json');
|
|
10
|
+
const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
|
|
11
|
+
const dbPath = path.join(jettypodDir, dbFileName);
|
|
12
|
+
|
|
13
|
+
let testContext = {};
|
|
14
|
+
let originalDir = process.cwd();
|
|
15
|
+
|
|
16
|
+
// Setup test environment
|
|
17
|
+
async function setupTestEnv() {
|
|
18
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
19
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
20
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
23
|
+
fs.mkdirSync(jettypodDir, { recursive: true });
|
|
24
|
+
process.chdir(testDir);
|
|
25
|
+
|
|
26
|
+
// Initialize git
|
|
27
|
+
execSync('git init', { stdio: 'pipe' });
|
|
28
|
+
execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
|
|
29
|
+
execSync('git config user.name "Test"', { stdio: 'pipe' });
|
|
30
|
+
|
|
31
|
+
// Initialize database
|
|
32
|
+
const { getDb } = require('../../lib/database');
|
|
33
|
+
const { runMigrations } = require('../../lib/migrations');
|
|
34
|
+
const db = getDb();
|
|
35
|
+
await runMigrations(db);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cleanup test environment
|
|
39
|
+
async function cleanupTestEnv() {
|
|
40
|
+
const { closeDb } = require('../../lib/database');
|
|
41
|
+
const { resetDb } = require('../../lib/database');
|
|
42
|
+
await closeDb();
|
|
43
|
+
resetDb();
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(testDir)) {
|
|
46
|
+
process.chdir(originalDir);
|
|
47
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
testContext = {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Given steps
|
|
53
|
+
Given('I have a project with {int} features in stable mode', async function (featureCount) {
|
|
54
|
+
await setupTestEnv();
|
|
55
|
+
|
|
56
|
+
// Create config
|
|
57
|
+
const config = {
|
|
58
|
+
name: 'test-project',
|
|
59
|
+
project_state: 'internal',
|
|
60
|
+
project_discovery: {
|
|
61
|
+
status: 'completed'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
65
|
+
|
|
66
|
+
// Create features in database
|
|
67
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
68
|
+
const db = new sqlite3.Database(dbPath);
|
|
69
|
+
|
|
70
|
+
for (let i = 1; i <= featureCount; i++) {
|
|
71
|
+
await new Promise((resolve) => {
|
|
72
|
+
db.run(
|
|
73
|
+
`INSERT INTO work_items (id, type, title, status, mode, phase) VALUES (?, 'feature', ?, 'completed', 'stable', 'implementation')`,
|
|
74
|
+
[i, `Feature ${i}`],
|
|
75
|
+
resolve
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
testContext.featureCount = featureCount;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
Given('the project state is {string}', function (state) {
|
|
84
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
85
|
+
assert.strictEqual(config.project_state, state);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// When steps
|
|
89
|
+
When('I transition the project to external', function () {
|
|
90
|
+
try {
|
|
91
|
+
testContext.output = execSync(
|
|
92
|
+
`node ${path.join(originalDir, 'jettypod.js')} project external`,
|
|
93
|
+
{ cwd: testDir, encoding: 'utf-8' }
|
|
94
|
+
);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
testContext.error = err.message;
|
|
97
|
+
testContext.output = err.stdout || '';
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Then steps
|
|
102
|
+
Then('the project state is set to {string}', function (expectedState) {
|
|
103
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
104
|
+
assert.strictEqual(config.project_state, expectedState);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Then('a {string} epic is created', function (epicTitle) {
|
|
108
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
109
|
+
const db = new sqlite3.Database(dbPath);
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
db.get(
|
|
113
|
+
`SELECT * FROM work_items WHERE type = 'epic' AND title = ?`,
|
|
114
|
+
[epicTitle],
|
|
115
|
+
(err, row) => {
|
|
116
|
+
assert(row, `Epic "${epicTitle}" should exist`);
|
|
117
|
+
testContext.epicId = row.id;
|
|
118
|
+
resolve();
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
Then('{int} production chores are created for each stable feature', function (choresPerFeature) {
|
|
125
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
126
|
+
const db = new sqlite3.Database(dbPath);
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
db.all(
|
|
130
|
+
`SELECT * FROM work_items WHERE type = 'chore' AND parent_id = ?`,
|
|
131
|
+
[testContext.epicId],
|
|
132
|
+
(err, rows) => {
|
|
133
|
+
const expectedTotal = testContext.featureCount * choresPerFeature;
|
|
134
|
+
assert.strictEqual(rows.length, expectedTotal, `Should have ${expectedTotal} production chores`);
|
|
135
|
+
testContext.productionChores = rows;
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
Then('the chores are grouped under the epic', function () {
|
|
143
|
+
const choresUnderEpic = testContext.productionChores.filter(
|
|
144
|
+
chore => chore.parent_id === testContext.epicId
|
|
145
|
+
);
|
|
146
|
+
assert.strictEqual(
|
|
147
|
+
choresUnderEpic.length,
|
|
148
|
+
testContext.productionChores.length,
|
|
149
|
+
'All production chores should be under the epic'
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
Then('I see a summary of created chores', function () {
|
|
154
|
+
assert(testContext.output.includes('production chores'), 'Output should mention production chores');
|
|
155
|
+
assert(testContext.output.includes('Feature Production Readiness'), 'Output should mention the epic');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Cleanup after all scenarios complete (AfterAll allows async operations)
|
|
159
|
+
AfterAll(async function() {
|
|
160
|
+
const { closeDb } = require('../../lib/database');
|
|
161
|
+
await closeDb();
|
|
162
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { Given, When, Then } = require('@cucumber/cucumber');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
Given('the logo module exists', function () {
|
|
7
|
+
const logoPath = path.join(__dirname, '../../lib/logo.js');
|
|
8
|
+
assert(fs.existsSync(logoPath), `Logo module should exist at ${logoPath}`);
|
|
9
|
+
this.logoPath = logoPath;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
When('I import it', function () {
|
|
13
|
+
// Import the logo module
|
|
14
|
+
this.logoModule = require(this.logoPath);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
Then('it exports a {string} function', function (functionName) {
|
|
18
|
+
assert(this.logoModule, 'Logo module should be imported');
|
|
19
|
+
assert(typeof this.logoModule[functionName] === 'function', `Should export a ${functionName} function`);
|
|
20
|
+
this.exportedFunction = this.logoModule[functionName];
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
When('I call the showLogo function', function () {
|
|
24
|
+
// Capture console.log output
|
|
25
|
+
const originalLog = console.log;
|
|
26
|
+
const logOutput = [];
|
|
27
|
+
|
|
28
|
+
console.log = (...args) => {
|
|
29
|
+
logOutput.push(args.join(' '));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Call the showLogo function
|
|
34
|
+
const { showLogo } = require('../../lib/logo.js');
|
|
35
|
+
showLogo();
|
|
36
|
+
} finally {
|
|
37
|
+
console.log = originalLog;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.logoOutput = logOutput;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
Then('it outputs {int} lines of logo art', function (expectedLines) {
|
|
44
|
+
assert(this.logoOutput, 'Logo output should exist');
|
|
45
|
+
// Count total lines across all console.log calls (split by newlines)
|
|
46
|
+
const totalLines = this.logoOutput.reduce((count, output) => {
|
|
47
|
+
return count + output.split('\n').filter(line => line.trim().length > 0).length;
|
|
48
|
+
}, 0);
|
|
49
|
+
assert(totalLines >= expectedLines,
|
|
50
|
+
`Expected at least ${expectedLines} lines but got ${totalLines}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
Then('it includes the version subtitle', function () {
|
|
54
|
+
assert(this.logoOutput, 'Logo output should exist');
|
|
55
|
+
const hasVersionLine = this.logoOutput.some(line =>
|
|
56
|
+
line.includes('version') || line.includes('v') || line.includes('.')
|
|
57
|
+
);
|
|
58
|
+
assert(hasVersionLine, 'Logo should include version subtitle');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
When('I run jettypod init', function () {
|
|
62
|
+
const { execSync } = require('child_process');
|
|
63
|
+
const jettypodPath = path.join(__dirname, '../../jettypod.js');
|
|
64
|
+
const skillsSourceDir = path.join(__dirname, '../../.claude/skills');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const output = execSync(`node ${jettypodPath} init`, {
|
|
68
|
+
cwd: this.testDir || process.cwd(),
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
env: { ...process.env, JETTYPOD_SKILLS_SOURCE_DIR: skillsSourceDir }
|
|
71
|
+
});
|
|
72
|
+
this.initOutput = output;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.initOutput = err.stdout || '';
|
|
75
|
+
this.initError = err;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
Then('the output contains the unicode gradient logo', function () {
|
|
80
|
+
assert(this.initOutput || this.commandOutput, 'Should have output from jettypod init');
|
|
81
|
+
const output = this.initOutput || this.commandOutput;
|
|
82
|
+
|
|
83
|
+
// Check for DEV or POD in the output
|
|
84
|
+
const hasLogo = output.includes('DEV') || output.includes('POD') ||
|
|
85
|
+
output.includes('█') || output.includes('▓') ||
|
|
86
|
+
output.includes('▒') || output.includes('░');
|
|
87
|
+
|
|
88
|
+
assert(hasLogo, 'Output should contain unicode gradient logo characters');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
Then('I see the unicode logo', function () {
|
|
92
|
+
assert(this.initOutput || this.commandOutput, 'Should have output from jettypod init');
|
|
93
|
+
const output = this.initOutput || this.commandOutput;
|
|
94
|
+
|
|
95
|
+
const hasLogo = output.includes('DEV') || output.includes('POD') ||
|
|
96
|
+
output.includes('█') || output.includes('▓');
|
|
97
|
+
|
|
98
|
+
assert(hasLogo, 'Should see unicode logo in output');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
Then('the logo includes {string} and {string} text', function (text1, text2) {
|
|
102
|
+
const output = this.initOutput || this.commandOutput;
|
|
103
|
+
assert(output, 'Should have output');
|
|
104
|
+
// The logo uses box-drawing characters, not literal text
|
|
105
|
+
// Just check that the output has the box characters that form the logo
|
|
106
|
+
const hasBoxChars = output.includes('█') || output.includes('╔') || output.includes('║');
|
|
107
|
+
assert(hasBoxChars, `Logo should include box-drawing characters (forms ${text1} and ${text2})`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
Then('the logo uses ANSI color codes', function () {
|
|
111
|
+
const output = this.initOutput || this.commandOutput;
|
|
112
|
+
assert(output, 'Should have output');
|
|
113
|
+
|
|
114
|
+
// Check for ANSI escape codes
|
|
115
|
+
const hasAnsiCodes = output.includes('\x1b[') || output.includes('\u001b[');
|
|
116
|
+
assert(hasAnsiCodes, 'Logo should use ANSI color codes');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
Then('the .jettypod directory is created', function () {
|
|
120
|
+
const jettypodDir = path.join(this.testDir || process.cwd(), '.jettypod');
|
|
121
|
+
assert(fs.existsSync(jettypodDir), '.jettypod directory should be created');
|
|
122
|
+
|
|
123
|
+
// Verify it contains expected files
|
|
124
|
+
const configPath = path.join(jettypodDir, 'config.json');
|
|
125
|
+
assert(fs.existsSync(configPath), 'config.json should exist in .jettypod directory');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
Then('git hooks are installed', function () {
|
|
129
|
+
const hooksDir = path.join(this.testDir || process.cwd(), '.git', 'hooks');
|
|
130
|
+
|
|
131
|
+
// Check if .git directory exists (it might not if init was run without git)
|
|
132
|
+
const gitDir = path.join(this.testDir || process.cwd(), '.git');
|
|
133
|
+
if (!fs.existsSync(gitDir)) {
|
|
134
|
+
// Skip this check if not a git repo
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for post-commit hook
|
|
139
|
+
const postCommitHook = path.join(hooksDir, 'post-commit');
|
|
140
|
+
assert(fs.existsSync(postCommitHook), 'post-commit hook should be installed');
|
|
141
|
+
|
|
142
|
+
// Check for post-merge hook
|
|
143
|
+
const postMergeHook = path.join(hooksDir, 'post-merge');
|
|
144
|
+
assert(fs.existsSync(postMergeHook), 'post-merge hook should be installed');
|
|
145
|
+
});
|