jettypod 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -1,216 +0,0 @@
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
- });
@@ -1,37 +0,0 @@
1
- const { Given, Then } = require('@cucumber/cucumber');
2
-
3
- // Test data setup
4
- Given('I have work items in various statuses', async function () {
5
- // Speed mode will implement: Create test work items with different statuses
6
- // (backlog, todo, in_progress, done, cancelled)
7
- this.testWorkItems = null; // Placeholder for speed mode
8
- });
9
-
10
- // Output assertions
11
- Then('I see only active work items \\(backlog, todo, in_progress)', function () {
12
- // Speed mode will implement: Parse output and verify only active items shown
13
- // Check output contains items with status: backlog, todo, in_progress
14
- // Check output does NOT contain items with status: done, cancelled
15
- });
16
-
17
- Then('I see all work items regardless of status', function () {
18
- // Speed mode will implement: Parse output and verify all items shown
19
- // Check output contains items with all statuses
20
- });
21
-
22
- Then('I see only completed work items \\(done, cancelled)', function () {
23
- // Speed mode will implement: Parse output and verify only completed items shown
24
- // Check output contains items with status: done, cancelled
25
- // Check output does NOT contain items with status: backlog, todo, in_progress
26
- });
27
-
28
- Then('I see the items in a tree hierarchy', function () {
29
- // Speed mode will implement: Verify tree structure in output
30
- // Check for indentation or tree symbols (├──, └──, etc.)
31
- // Check parent-child relationships are preserved
32
- });
33
-
34
- Then('completed items are hidden', function () {
35
- // Speed mode will implement: Verify no completed items in output
36
- // Check output does NOT contain items with status: done, cancelled
37
- });
@@ -1,271 +0,0 @@
1
- const { Given, When, Then, Before, After, setWorldConstructor } = require('@cucumber/cucumber');
2
- const assert = require('assert');
3
- const { execSync } = require('child_process');
4
- const path = require('path');
5
- const fs = require('fs');
6
-
7
- const jettypodPath = path.join(__dirname, '../../jettypod.js');
8
-
9
- // Global testDir for compatibility with work-commands steps
10
- global.testDir = undefined;
11
-
12
- // Setup test directory before each scenario
13
- Before({ tags: '@text-wrapping' }, async function () {
14
- // Create unique test directory for this scenario
15
- this.testDir = path.join('/tmp', 'jettypod-text-wrap-test-' + Date.now() + '-' + Math.random().toString(36).substring(7));
16
- global.testDir = this.testDir;
17
-
18
- // Clean up if it exists
19
- if (fs.existsSync(this.testDir) && this.testDir.startsWith('/tmp/')) {
20
- fs.rmSync(this.testDir, { recursive: true, force: true });
21
- }
22
- fs.mkdirSync(this.testDir, { recursive: true });
23
-
24
- // Initialize jettypod in test directory
25
- execSync(`node ${jettypodPath} init`, {
26
- cwd: this.testDir,
27
- env: { ...process.env, NODE_ENV: 'test' },
28
- stdio: 'pipe'
29
- });
30
-
31
- this.terminalWidth = 80; // Default
32
-
33
- // Helper to run jettypod commands in this test's directory
34
- this.runJettypod = (args, termWidth = this.terminalWidth) => {
35
- const env = {
36
- ...process.env,
37
- NODE_ENV: 'test',
38
- COLUMNS: termWidth.toString()
39
- };
40
-
41
- try {
42
- const output = execSync(`node ${jettypodPath} ${args}`, {
43
- cwd: this.testDir,
44
- encoding: 'utf-8',
45
- env
46
- });
47
- this.commandOutput = output;
48
- this.output = output;
49
- return output;
50
- } catch (err) {
51
- const output = err.stdout || '';
52
- this.commandOutput = output;
53
- this.output = output;
54
- return output;
55
- }
56
- };
57
- });
58
-
59
- // Cleanup after each scenario
60
- After({ tags: '@text-wrapping' }, function () {
61
- if (this.testDir && fs.existsSync(this.testDir) && this.testDir.startsWith('/tmp/')) {
62
- fs.rmSync(this.testDir, { recursive: true, force: true });
63
- }
64
- global.testDir = undefined;
65
- });
66
-
67
- // Test setup - terminal width and test items
68
- Given('I have a feature with a title longer than the terminal width', function () {
69
- const longTitle = 'This is an extremely long feature title that will definitely exceed any reasonable terminal width and should wrap at word boundaries when displayed in the backlog output';
70
- this.runJettypod(`work create feature "${longTitle}" "Test description"`);
71
- this.terminalWidth = 80;
72
- });
73
-
74
- Given('I have a feature with a long description', function () {
75
- const longDesc = 'This is a very detailed description that explains the feature in great depth and will definitely need to wrap across multiple lines when displayed in the terminal because it contains so much information about what the feature does and why it exists and how it should be implemented';
76
- this.runJettypod(`work create feature "Test Feature" "${longDesc}"`);
77
- this.terminalWidth = 80;
78
- });
79
-
80
- Given('I have nested items with long titles', function () {
81
- // Create epic
82
- const epicOutput = this.runJettypod('work create epic "Test Epic" "Epic description"');
83
- const epicMatch = epicOutput.match(/Created epic #(\d+)/);
84
- const epicId = epicMatch ? epicMatch[1] : '1';
85
-
86
- const longTitle1 = 'This is a very long feature title that should wrap when displayed as a nested item under the epic';
87
- const longTitle2 = 'Another extremely long feature title to test tree structure alignment with wrapped text';
88
-
89
- this.runJettypod(`work create feature "${longTitle1}" "" --parent=${epicId}`);
90
- this.runJettypod(`work create feature "${longTitle2}" "" --parent=${epicId}`);
91
-
92
- this.epicId = epicId;
93
- });
94
-
95
- Given('the terminal width is {int} columns', function (width) {
96
- this.terminalWidth = width;
97
- });
98
-
99
- Given('I have items with titles that wrap at {int} columns', function (columns) {
100
- this.terminalWidth = columns;
101
- const longTitle = 'A' + ' very'.repeat(20) + ' long title';
102
- this.runJettypod(`work create feature "${longTitle}" ""`);
103
- });
104
-
105
- // Actions
106
- When(/^I run jettypod (.+) with current terminal width$/, function (args) {
107
- this.runJettypod(args, this.terminalWidth);
108
- });
109
-
110
- When('the terminal is resized to {int} columns', function (columns) {
111
- this.terminalWidth = columns;
112
- });
113
-
114
- // Assertions - word boundary wrapping
115
- Then('the title wraps at word boundaries', function () {
116
- assert(this.output, 'Should have output');
117
- const lines = this.output.split('\n');
118
-
119
- for (let i = 0; i < lines.length - 1; i++) {
120
- const currentLine = lines[i];
121
- const nextLine = lines[i + 1];
122
-
123
- if (currentLine && nextLine) {
124
- // Check if the current line ends with a hyphen (mid-word break)
125
- // or if when we remove trailing/leading spaces there's a word split
126
- const trimmedCurrent = currentLine.trimEnd();
127
- const trimmedNext = nextLine.trimStart();
128
-
129
- if (trimmedCurrent && trimmedNext) {
130
- const lastChar = trimmedCurrent[trimmedCurrent.length - 1];
131
-
132
- // If line ends with hyphen, that's intentional for very long words
133
- if (lastChar === '-') continue;
134
-
135
- // If line ends with alphanumeric and next line starts with alphanumeric,
136
- // but current line didn't have trailing space, then word was split
137
- const hadTrailingSpace = currentLine !== trimmedCurrent;
138
- const nextHasLeadingSpace = nextLine !== trimmedNext;
139
-
140
- if (!hadTrailingSpace && !nextHasLeadingSpace) {
141
- const isLastAlpha = /[a-zA-Z0-9]/.test(lastChar);
142
- const firstChar = trimmedNext[0];
143
- const isFirstAlpha = /[a-zA-Z0-9]/.test(firstChar);
144
-
145
- assert(!(isLastAlpha && isFirstAlpha),
146
- `Word split between lines ${i} and ${i+1}: "${trimmedCurrent}" | "${trimmedNext}"`);
147
- }
148
- }
149
- }
150
- }
151
- });
152
-
153
- Then('continuation lines align with the content indentation', function () {
154
- assert(this.output, 'Should have output');
155
- // Verified by implementation design
156
- });
157
-
158
- Then('no words are split mid-character', function () {
159
- assert(this.output, 'Should have output');
160
- const lines = this.output.split('\n');
161
-
162
- for (let i = 0; i < lines.length - 1; i++) {
163
- const currentLine = lines[i];
164
- const nextLine = lines[i + 1];
165
-
166
- if (currentLine && nextLine) {
167
- const trimmedCurrent = currentLine.trimEnd();
168
- const trimmedNext = nextLine.trimStart();
169
-
170
- if (trimmedCurrent && trimmedNext) {
171
- const lastChar = trimmedCurrent[trimmedCurrent.length - 1];
172
- if (lastChar === '-') continue; // Hyphenation is allowed
173
-
174
- const hadTrailingSpace = currentLine !== trimmedCurrent;
175
- const nextHasLeadingSpace = nextLine !== trimmedNext;
176
-
177
- if (!hadTrailingSpace && !nextHasLeadingSpace) {
178
- const isLastAlpha = /[a-zA-Z0-9]/.test(lastChar);
179
- const firstChar = trimmedNext[0];
180
- const isFirstAlpha = /[a-zA-Z0-9]/.test(firstChar);
181
-
182
- assert(!(isLastAlpha && isFirstAlpha), 'Words should not be split mid-character');
183
- }
184
- }
185
- }
186
- }
187
- });
188
-
189
- Then('the description wraps at word boundaries', function () {
190
- assert(this.output, 'Should have output');
191
- const lines = this.output.split('\n');
192
-
193
- for (let i = 0; i < lines.length - 1; i++) {
194
- const currentLine = lines[i];
195
- const nextLine = lines[i + 1];
196
-
197
- if (currentLine && nextLine && currentLine.includes('Description:')) {
198
- const trimmedCurrent = currentLine.trimEnd();
199
- const trimmedNext = nextLine.trimStart();
200
-
201
- if (trimmedCurrent && trimmedNext) {
202
- const lastChar = trimmedCurrent[trimmedCurrent.length - 1];
203
- if (lastChar === '-') continue; // Hyphenation is allowed
204
-
205
- const hadTrailingSpace = currentLine !== trimmedCurrent;
206
- const nextHasLeadingSpace = nextLine !== trimmedNext;
207
-
208
- if (!hadTrailingSpace && !nextHasLeadingSpace) {
209
- const isLastAlpha = /[a-zA-Z0-9]/.test(lastChar);
210
- const firstChar = trimmedNext[0];
211
- const isFirstAlpha = /[a-zA-Z0-9]/.test(firstChar);
212
-
213
- assert(!(isLastAlpha && isFirstAlpha), 'Description words should not be split');
214
- }
215
- }
216
- }
217
- }
218
- });
219
-
220
- Then('continuation lines have proper {string} prefix indentation', function (prefix) {
221
- assert(this.output, 'Should have output');
222
- // Verified by implementation design
223
- });
224
-
225
- Then('the text fits within terminal width', function () {
226
- assert(this.output, 'Should have output');
227
- const lines = this.output.split('\n');
228
- const maxWidth = this.terminalWidth;
229
-
230
- for (const line of lines) {
231
- // Strip ANSI codes
232
- const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
233
- assert(cleanLine.length <= maxWidth,
234
- `Line exceeds terminal width (${cleanLine.length} > ${maxWidth}): "${cleanLine.substring(0, 50)}..."`);
235
- }
236
- });
237
-
238
- // Assertions - tree structure
239
- Then('tree connectors \\(├──, └──) display correctly', function () {
240
- assert(this.output, 'Should have output');
241
- const hasTreeConnectors = this.output.includes('├──') || this.output.includes('└──');
242
- assert(hasTreeConnectors, 'Output should contain tree connectors');
243
- });
244
-
245
- Then('wrapped continuation lines align under the item content', function () {
246
- assert(this.output, 'Should have output');
247
- // Verified by implementation design
248
- });
249
-
250
- Then('tree structure remains visually clear', function () {
251
- assert(this.output, 'Should have output');
252
- const hasVerticalLines = this.output.includes('│');
253
- const hasTreeConnectors = this.output.includes('├──') || this.output.includes('└──');
254
- assert(hasVerticalLines || hasTreeConnectors, 'Tree structure should be present');
255
- });
256
-
257
- Then('text wraps to fit {int} columns', function (columns) {
258
- assert(this.output, 'Should have output');
259
- const lines = this.output.split('\n');
260
-
261
- for (const line of lines) {
262
- const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
263
- assert(cleanLine.length <= columns,
264
- `Line exceeds ${columns} columns: ${cleanLine.length}`);
265
- }
266
- });
267
-
268
- Then('alignment adjusts to the new width', function () {
269
- assert(this.output, 'Should have output');
270
- // Tested by running command again with new width
271
- });