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.
Files changed (122) hide show
  1. package/.claude/PROTECT_SKILLS.md +28 -0
  2. package/.claude/settings.json +24 -0
  3. package/.claude/settings.local.json +16 -0
  4. package/.claude/skills/epic-discover/SKILL.md +262 -0
  5. package/.claude/skills/feature-discover/SKILL.md +393 -0
  6. package/.claude/skills/speed-mode/SKILL.md +364 -0
  7. package/.claude/skills/stable-mode/SKILL.md +591 -0
  8. package/.github/workflows/test-safety.yml +85 -0
  9. package/README.md +25 -0
  10. package/SPEED-STABLE-AUDIT.md +853 -0
  11. package/SYSTEM-BEHAVIOR.md +1241 -0
  12. package/TEST_SAFETY_AUDIT.md +314 -0
  13. package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
  14. package/cucumber.js +8 -0
  15. package/docs/COMMAND_REFERENCE.md +903 -0
  16. package/docs/DECISIONS.md +68 -0
  17. package/docs/README.md +48 -0
  18. package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
  19. package/docs/TEST-REWRITE-PLAN.md +261 -0
  20. package/docs/ai-test-writing-requirements.md +219 -0
  21. package/docs/claude-code-skills.md +607 -0
  22. package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
  23. package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
  24. package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
  25. package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
  26. package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
  27. package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
  28. package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
  29. package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
  30. package/docs/features/jettypod-standards-explained.md +543 -0
  31. package/docs/features/standards-inventory.md +257 -0
  32. package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
  33. package/docs/jettypod-system-overview.md +409 -0
  34. package/features/auto-generate-production-chores.feature +14 -0
  35. package/features/claude-md-protection/steps.js +487 -0
  36. package/features/decisions/index.js +490 -0
  37. package/features/decisions/index.test.js +208 -0
  38. package/features/git-hooks/git-hooks.feature +30 -0
  39. package/features/git-hooks/index.js +93 -0
  40. package/features/git-hooks/index.test.js +137 -0
  41. package/features/git-hooks/post-commit +56 -0
  42. package/features/git-hooks/post-merge +47 -0
  43. package/features/git-hooks/pre-commit +28 -0
  44. package/features/git-hooks/simple-steps.js +53 -0
  45. package/features/git-hooks/simple-test.feature +10 -0
  46. package/features/git-hooks/steps.js +196 -0
  47. package/features/jettypod-update-command.feature +46 -0
  48. package/features/mode-prompts/index.js +95 -0
  49. package/features/mode-prompts/simple-steps.js +44 -0
  50. package/features/mode-prompts/simple-test.feature +9 -0
  51. package/features/mode-prompts/validation.test.js +120 -0
  52. package/features/refactor-mode/steps.js +217 -0
  53. package/features/refactor-mode.feature +49 -0
  54. package/features/skills-update/index.test.js +216 -0
  55. package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
  56. package/features/step_definitions/terminal-logo.steps.js +145 -0
  57. package/features/step_definitions/update-command.steps.js +183 -0
  58. package/features/terminal-logo/index.js +39 -0
  59. package/features/terminal-logo/terminal-logo.feature +30 -0
  60. package/features/update-command/index.js +181 -0
  61. package/features/update-command/index.test.js +225 -0
  62. package/features/work-commands/bug-workflow-display.feature +22 -0
  63. package/features/work-commands/index.js +311 -0
  64. package/features/work-commands/simple-steps.js +69 -0
  65. package/features/work-commands/stable-tests.feature +57 -0
  66. package/features/work-commands/steps.js +1120 -0
  67. package/features/work-commands/validation.test.js +88 -0
  68. package/features/work-commands/work-commands.feature +13 -0
  69. package/features/work-tracking/discovery-validation.test.js +228 -0
  70. package/features/work-tracking/index.js +1511 -0
  71. package/features/work-tracking/mode-required.feature +112 -0
  72. package/features/work-tracking/phase-tracking.test.js +482 -0
  73. package/features/work-tracking/prototype-tracking.test.js +485 -0
  74. package/features/work-tracking/tree-view.test.js +310 -0
  75. package/features/work-tracking/work-set-mode.feature +71 -0
  76. package/features/work-tracking/work-start-mode.feature +88 -0
  77. package/full-test.txt +0 -0
  78. package/install.sh +89 -0
  79. package/jettypod.js +1640 -0
  80. package/lib/bug-workflow.js +94 -0
  81. package/lib/bug-workflow.test.js +177 -0
  82. package/lib/claudemd.js +130 -0
  83. package/lib/claudemd.test.js +195 -0
  84. package/lib/comprehensive-standards-full.json +1778 -0
  85. package/lib/config.js +181 -0
  86. package/lib/config.test.js +511 -0
  87. package/lib/constants.js +107 -0
  88. package/lib/constants.test.js +164 -0
  89. package/lib/current-work.js +130 -0
  90. package/lib/current-work.test.js +146 -0
  91. package/lib/database-project-config.test.js +107 -0
  92. package/lib/database.js +256 -0
  93. package/lib/database.test.js +106 -0
  94. package/lib/decisions-generator.js +102 -0
  95. package/lib/decisions-generator.test.js +457 -0
  96. package/lib/decisions-helpers.js +119 -0
  97. package/lib/decisions-helpers.test.js +310 -0
  98. package/lib/discovery-checkpoint.js +83 -0
  99. package/lib/docs-generator.js +280 -0
  100. package/lib/external-checklist.js +177 -0
  101. package/lib/git.js +142 -0
  102. package/lib/git.test.js +145 -0
  103. package/lib/logo.js +3 -0
  104. package/lib/migrations/001-epic-to-parent.js +24 -0
  105. package/lib/migrations/002-default-work-item-modes.js +37 -0
  106. package/lib/migrations/002-default-work-item-modes.test.js +351 -0
  107. package/lib/migrations/003-epic-discovery-fields.js +52 -0
  108. package/lib/migrations/004-discovery-decisions-table.js +32 -0
  109. package/lib/migrations/005-migrate-decision-data.js +62 -0
  110. package/lib/migrations/006-feature-phase-field.js +61 -0
  111. package/lib/migrations/007-prototype-tracking.js +38 -0
  112. package/lib/migrations/008-scenario-file-field.js +24 -0
  113. package/lib/migrations/index.js +74 -0
  114. package/lib/production-helpers.js +69 -0
  115. package/lib/project-state.test.js +92 -0
  116. package/lib/test-helpers.js +184 -0
  117. package/lib/test-helpers.test.js +255 -0
  118. package/package.json +36 -0
  119. package/prototypes/test/index.html +1 -0
  120. package/setup-dist-repo.sh +68 -0
  121. package/test-safety-check.sh +80 -0
  122. package/work-item-tracking-plan.md +199 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Work item types
3
+ * @constant {Object}
4
+ */
5
+ const WORK_TYPES = Object.freeze({
6
+ EPIC: 'epic',
7
+ FEATURE: 'feature',
8
+ BUG: 'bug',
9
+ CHORE: 'chore'
10
+ });
11
+
12
+ /**
13
+ * Work item statuses
14
+ * @constant {Object}
15
+ */
16
+ const WORK_STATUSES = Object.freeze({
17
+ BACKLOG: 'backlog',
18
+ TODO: 'todo',
19
+ IN_PROGRESS: 'in_progress',
20
+ BLOCKED: 'blocked',
21
+ DONE: 'done',
22
+ CANCELLED: 'cancelled'
23
+ });
24
+
25
+ /**
26
+ * Type emojis for display
27
+ * @constant {Object}
28
+ */
29
+ const TYPE_EMOJIS = Object.freeze({
30
+ [WORK_TYPES.EPIC]: '🎯',
31
+ [WORK_TYPES.FEATURE]: '✨',
32
+ [WORK_TYPES.BUG]: '🐛',
33
+ [WORK_TYPES.CHORE]: '🔧'
34
+ });
35
+
36
+ /**
37
+ * Status emojis for display
38
+ * @constant {Object}
39
+ */
40
+ const STATUS_EMOJIS = Object.freeze({
41
+ [WORK_STATUSES.BACKLOG]: '⏳',
42
+ [WORK_STATUSES.TODO]: '📋',
43
+ [WORK_STATUSES.IN_PROGRESS]: '🔄',
44
+ [WORK_STATUSES.DONE]: '✅',
45
+ [WORK_STATUSES.CANCELLED]: '❌'
46
+ });
47
+
48
+ /**
49
+ * Valid status values as array
50
+ * @constant {Array<string>}
51
+ */
52
+ const VALID_STATUSES = Object.freeze(Object.values(WORK_STATUSES));
53
+
54
+ /**
55
+ * Valid type values as array
56
+ * @constant {Array<string>}
57
+ */
58
+ const VALID_TYPES = Object.freeze(Object.values(WORK_TYPES));
59
+
60
+ /**
61
+ * Check if a type string is valid
62
+ * @param {string} type - Type to validate
63
+ * @returns {boolean} True if type is valid
64
+ */
65
+ function isValidType(type) {
66
+ return VALID_TYPES.includes(type);
67
+ }
68
+
69
+ /**
70
+ * Check if a status string is valid
71
+ * @param {string} status - Status to validate
72
+ * @returns {boolean} True if status is valid
73
+ */
74
+ function isValidStatus(status) {
75
+ return VALID_STATUSES.includes(status);
76
+ }
77
+
78
+ /**
79
+ * Get emoji for a work item type
80
+ * @param {string} type - Work item type
81
+ * @returns {string} Emoji for the type, or default emoji if not found
82
+ */
83
+ function getTypeEmoji(type) {
84
+ return TYPE_EMOJIS[type] || '📋';
85
+ }
86
+
87
+ /**
88
+ * Get emoji for a work item status
89
+ * @param {string} status - Work item status
90
+ * @returns {string} Emoji for the status, or default emoji if not found
91
+ */
92
+ function getStatusEmoji(status) {
93
+ return STATUS_EMOJIS[status] || '❓';
94
+ }
95
+
96
+ module.exports = {
97
+ WORK_TYPES,
98
+ WORK_STATUSES,
99
+ TYPE_EMOJIS,
100
+ STATUS_EMOJIS,
101
+ VALID_STATUSES,
102
+ VALID_TYPES,
103
+ isValidType,
104
+ isValidStatus,
105
+ getTypeEmoji,
106
+ getStatusEmoji
107
+ };
@@ -0,0 +1,164 @@
1
+ const {
2
+ WORK_TYPES,
3
+ WORK_STATUSES,
4
+ TYPE_EMOJIS,
5
+ STATUS_EMOJIS,
6
+ VALID_STATUSES,
7
+ VALID_TYPES,
8
+ isValidType,
9
+ isValidStatus,
10
+ getTypeEmoji,
11
+ getStatusEmoji
12
+ } = require('./constants');
13
+
14
+ describe('Constants Module', () => {
15
+ describe('WORK_TYPES', () => {
16
+ test('should have all required types', () => {
17
+ expect(WORK_TYPES.EPIC).toBe('epic');
18
+ expect(WORK_TYPES.FEATURE).toBe('feature');
19
+ expect(WORK_TYPES.BUG).toBe('bug');
20
+ expect(WORK_TYPES.CHORE).toBe('chore');
21
+ });
22
+
23
+ test('should be frozen (immutable)', () => {
24
+ expect(Object.isFrozen(WORK_TYPES)).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe('WORK_STATUSES', () => {
29
+ test('should have all required statuses', () => {
30
+ expect(WORK_STATUSES.BACKLOG).toBe('backlog');
31
+ expect(WORK_STATUSES.TODO).toBe('todo');
32
+ expect(WORK_STATUSES.IN_PROGRESS).toBe('in_progress');
33
+ expect(WORK_STATUSES.BLOCKED).toBe('blocked');
34
+ expect(WORK_STATUSES.DONE).toBe('done');
35
+ expect(WORK_STATUSES.CANCELLED).toBe('cancelled');
36
+ });
37
+
38
+ test('should be frozen (immutable)', () => {
39
+ expect(Object.isFrozen(WORK_STATUSES)).toBe(true);
40
+ });
41
+ });
42
+
43
+ describe('TYPE_EMOJIS', () => {
44
+ test('should have emojis for all types', () => {
45
+ expect(TYPE_EMOJIS[WORK_TYPES.EPIC]).toBe('🎯');
46
+ expect(TYPE_EMOJIS[WORK_TYPES.FEATURE]).toBe('✨');
47
+ expect(TYPE_EMOJIS[WORK_TYPES.BUG]).toBe('🐛');
48
+ expect(TYPE_EMOJIS[WORK_TYPES.CHORE]).toBe('🔧');
49
+ });
50
+
51
+ test('should be frozen (immutable)', () => {
52
+ expect(Object.isFrozen(TYPE_EMOJIS)).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('STATUS_EMOJIS', () => {
57
+ test('should have emojis for all statuses', () => {
58
+ expect(STATUS_EMOJIS[WORK_STATUSES.BACKLOG]).toBe('⏳');
59
+ expect(STATUS_EMOJIS[WORK_STATUSES.TODO]).toBe('📋');
60
+ expect(STATUS_EMOJIS[WORK_STATUSES.IN_PROGRESS]).toBe('🔄');
61
+ expect(STATUS_EMOJIS[WORK_STATUSES.DONE]).toBe('✅');
62
+ expect(STATUS_EMOJIS[WORK_STATUSES.CANCELLED]).toBe('❌');
63
+ });
64
+
65
+ test('should be frozen (immutable)', () => {
66
+ expect(Object.isFrozen(STATUS_EMOJIS)).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe('VALID_TYPES', () => {
71
+ test('should contain all type values', () => {
72
+ expect(VALID_TYPES).toHaveLength(4);
73
+ expect(VALID_TYPES).toContain('epic');
74
+ expect(VALID_TYPES).toContain('feature');
75
+ expect(VALID_TYPES).toContain('bug');
76
+ expect(VALID_TYPES).toContain('chore');
77
+ });
78
+
79
+ test('should be frozen (immutable)', () => {
80
+ expect(Object.isFrozen(VALID_TYPES)).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe('VALID_STATUSES', () => {
85
+ test('should contain all status values', () => {
86
+ expect(VALID_STATUSES).toHaveLength(6);
87
+ expect(VALID_STATUSES).toContain('backlog');
88
+ expect(VALID_STATUSES).toContain('todo');
89
+ expect(VALID_STATUSES).toContain('in_progress');
90
+ expect(VALID_STATUSES).toContain('blocked');
91
+ expect(VALID_STATUSES).toContain('done');
92
+ expect(VALID_STATUSES).toContain('cancelled');
93
+ });
94
+
95
+ test('should be frozen (immutable)', () => {
96
+ expect(Object.isFrozen(VALID_STATUSES)).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe('isValidType()', () => {
101
+ test('should return true for valid types', () => {
102
+ expect(isValidType('epic')).toBe(true);
103
+ expect(isValidType('feature')).toBe(true);
104
+ expect(isValidType('bug')).toBe(true);
105
+ expect(isValidType('chore')).toBe(true);
106
+ });
107
+
108
+ test('should return false for invalid types', () => {
109
+ expect(isValidType('invalid')).toBe(false);
110
+ expect(isValidType('')).toBe(false);
111
+ expect(isValidType(null)).toBe(false);
112
+ expect(isValidType(undefined)).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('isValidStatus()', () => {
117
+ test('should return true for valid statuses', () => {
118
+ expect(isValidStatus('backlog')).toBe(true);
119
+ expect(isValidStatus('todo')).toBe(true);
120
+ expect(isValidStatus('in_progress')).toBe(true);
121
+ expect(isValidStatus('blocked')).toBe(true);
122
+ expect(isValidStatus('done')).toBe(true);
123
+ expect(isValidStatus('cancelled')).toBe(true);
124
+ });
125
+
126
+ test('should return false for invalid statuses', () => {
127
+ expect(isValidStatus('invalid')).toBe(false);
128
+ expect(isValidStatus('')).toBe(false);
129
+ expect(isValidStatus(null)).toBe(false);
130
+ expect(isValidStatus(undefined)).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('getTypeEmoji()', () => {
135
+ test('should return correct emoji for valid types', () => {
136
+ expect(getTypeEmoji('epic')).toBe('🎯');
137
+ expect(getTypeEmoji('feature')).toBe('✨');
138
+ expect(getTypeEmoji('bug')).toBe('🐛');
139
+ expect(getTypeEmoji('chore')).toBe('🔧');
140
+ });
141
+
142
+ test('should return default emoji for invalid types', () => {
143
+ expect(getTypeEmoji('invalid')).toBe('📋');
144
+ expect(getTypeEmoji(null)).toBe('📋');
145
+ expect(getTypeEmoji(undefined)).toBe('📋');
146
+ });
147
+ });
148
+
149
+ describe('getStatusEmoji()', () => {
150
+ test('should return correct emoji for valid statuses', () => {
151
+ expect(getStatusEmoji('backlog')).toBe('⏳');
152
+ expect(getStatusEmoji('todo')).toBe('📋');
153
+ expect(getStatusEmoji('in_progress')).toBe('🔄');
154
+ expect(getStatusEmoji('done')).toBe('✅');
155
+ expect(getStatusEmoji('cancelled')).toBe('❌');
156
+ });
157
+
158
+ test('should return default emoji for invalid statuses', () => {
159
+ expect(getStatusEmoji('invalid')).toBe('❓');
160
+ expect(getStatusEmoji(null)).toBe('❓');
161
+ expect(getStatusEmoji(undefined)).toBe('❓');
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,130 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Get path to current-work.json file
6
+ * @returns {string} Absolute path to current-work.json
7
+ */
8
+ function getCurrentWorkPath() {
9
+ return path.join(process.cwd(), '.jettypod', 'current-work.json');
10
+ }
11
+
12
+ /**
13
+ * Get current work item from file
14
+ * @returns {Object|null} Current work item or null if not set
15
+ * @throws {Error} If file exists but cannot be read due to permissions
16
+ */
17
+ function getCurrentWork() {
18
+ const currentWorkPath = getCurrentWorkPath();
19
+
20
+ if (!fs.existsSync(currentWorkPath)) {
21
+ return null;
22
+ }
23
+
24
+ try {
25
+ const content = fs.readFileSync(currentWorkPath, 'utf-8');
26
+ const workItem = JSON.parse(content);
27
+
28
+ // Validate required fields
29
+ if (!workItem.id || !workItem.title || !workItem.type) {
30
+ console.warn('Warning: Current work file is missing required fields, ignoring');
31
+ return null;
32
+ }
33
+
34
+ return workItem;
35
+ } catch (err) {
36
+ if (err.code === 'EACCES') {
37
+ throw new Error(`No read permission for current work file: ${currentWorkPath}`);
38
+ }
39
+
40
+ // Corrupted JSON - warn but don't throw
41
+ console.warn(`Warning: Corrupted current work file: ${err.message}`);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Validate work item structure
48
+ * @param {Object} workItem - Work item to validate
49
+ * @throws {Error} If work item is invalid
50
+ */
51
+ function validateWorkItem(workItem) {
52
+ if (!workItem || typeof workItem !== 'object') {
53
+ throw new Error('Work item must be an object');
54
+ }
55
+
56
+ if (!workItem.id || typeof workItem.id !== 'number') {
57
+ throw new Error('Work item must have a numeric id');
58
+ }
59
+
60
+ if (!workItem.title || typeof workItem.title !== 'string') {
61
+ throw new Error('Work item must have a string title');
62
+ }
63
+
64
+ if (!workItem.type || typeof workItem.type !== 'string') {
65
+ throw new Error('Work item must have a string type');
66
+ }
67
+
68
+ if (!workItem.status || typeof workItem.status !== 'string') {
69
+ throw new Error('Work item must have a string status');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Set current work item to file
75
+ * @param {Object} workItem - Work item to set as current
76
+ * @throws {Error} If workItem is invalid or file cannot be written
77
+ */
78
+ function setCurrentWork(workItem) {
79
+ validateWorkItem(workItem);
80
+
81
+ const currentWorkPath = getCurrentWorkPath();
82
+ const jettypodDir = path.dirname(currentWorkPath);
83
+
84
+ try {
85
+ // Ensure .jettypod directory exists
86
+ if (!fs.existsSync(jettypodDir)) {
87
+ fs.mkdirSync(jettypodDir, { recursive: true });
88
+ }
89
+
90
+ // Check write permission
91
+ if (fs.existsSync(jettypodDir)) {
92
+ fs.accessSync(jettypodDir, fs.constants.W_OK);
93
+ }
94
+
95
+ fs.writeFileSync(currentWorkPath, JSON.stringify(workItem, null, 2));
96
+ } catch (err) {
97
+ if (err.code === 'EACCES') {
98
+ throw new Error(`No write permission for directory: ${jettypodDir}`);
99
+ }
100
+ throw new Error(`Failed to write current work file: ${err.message}`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Clear current work item file
106
+ * @throws {Error} If file cannot be deleted due to permissions
107
+ */
108
+ function clearCurrentWork() {
109
+ const currentWorkPath = getCurrentWorkPath();
110
+
111
+ if (!fs.existsSync(currentWorkPath)) {
112
+ return;
113
+ }
114
+
115
+ try {
116
+ fs.unlinkSync(currentWorkPath);
117
+ } catch (err) {
118
+ if (err.code === 'EACCES') {
119
+ throw new Error(`No permission to delete current work file: ${currentWorkPath}`);
120
+ }
121
+ throw new Error(`Failed to clear current work file: ${err.message}`);
122
+ }
123
+ }
124
+
125
+ module.exports = {
126
+ getCurrentWork,
127
+ setCurrentWork,
128
+ clearCurrentWork,
129
+ getCurrentWorkPath
130
+ };
@@ -0,0 +1,146 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { createTestEnvironment } = require('./test-helpers');
4
+ const {
5
+ getCurrentWork,
6
+ setCurrentWork,
7
+ clearCurrentWork,
8
+ getCurrentWorkPath
9
+ } = require('./current-work');
10
+
11
+ describe('Current Work Module', () => {
12
+ let testEnv;
13
+
14
+ beforeEach(() => {
15
+ testEnv = createTestEnvironment();
16
+ process.chdir(testEnv.testDir);
17
+ });
18
+
19
+ afterEach(() => {
20
+ testEnv.cleanup();
21
+ });
22
+
23
+ const validWorkItem = {
24
+ id: 1,
25
+ title: 'Test Feature',
26
+ type: 'feature',
27
+ status: 'in_progress',
28
+ parent_id: null,
29
+ parent_title: null,
30
+ epic_id: null,
31
+ epic_title: null
32
+ };
33
+
34
+ describe('getCurrentWorkPath()', () => {
35
+ test('should return path to current-work.json', () => {
36
+ const workPath = getCurrentWorkPath();
37
+ expect(workPath).toContain('.jettypod');
38
+ expect(workPath).toContain('current-work.json');
39
+ });
40
+ });
41
+
42
+ describe('getCurrentWork()', () => {
43
+ test('should return null if file does not exist', () => {
44
+ expect(getCurrentWork()).toBeNull();
45
+ });
46
+
47
+ test('should return work item if file exists', () => {
48
+ setCurrentWork(validWorkItem);
49
+ const result = getCurrentWork();
50
+ expect(result).toEqual(validWorkItem);
51
+ });
52
+
53
+ test('should return null for corrupted JSON', () => {
54
+ const jettypodDir = path.join(testEnv.testDir, '.jettypod');
55
+ fs.mkdirSync(jettypodDir, { recursive: true });
56
+ fs.writeFileSync(getCurrentWorkPath(), 'not valid json');
57
+
58
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
59
+ const result = getCurrentWork();
60
+
61
+ expect(result).toBeNull();
62
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Corrupted current work file'));
63
+ consoleSpy.mockRestore();
64
+ });
65
+
66
+ test('should return null for work item missing required fields', () => {
67
+ const jettypodDir = path.join(testEnv.testDir, '.jettypod');
68
+ fs.mkdirSync(jettypodDir, { recursive: true });
69
+ fs.writeFileSync(getCurrentWorkPath(), JSON.stringify({ id: 1 })); // Missing title, type, status
70
+
71
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
72
+ const result = getCurrentWork();
73
+
74
+ expect(result).toBeNull();
75
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('missing required fields'));
76
+ consoleSpy.mockRestore();
77
+ });
78
+ });
79
+
80
+ describe('setCurrentWork()', () => {
81
+ test('should create .jettypod directory if it does not exist', () => {
82
+ setCurrentWork(validWorkItem);
83
+ expect(fs.existsSync(path.join(testEnv.testDir, '.jettypod'))).toBe(true);
84
+ });
85
+
86
+ test('should write work item to file', () => {
87
+ setCurrentWork(validWorkItem);
88
+
89
+ const content = fs.readFileSync(getCurrentWorkPath(), 'utf-8');
90
+ expect(JSON.parse(content)).toEqual(validWorkItem);
91
+ });
92
+
93
+ test('should throw error for invalid work item - not an object', () => {
94
+ expect(() => setCurrentWork(null)).toThrow('Work item must be an object');
95
+ expect(() => setCurrentWork('string')).toThrow('Work item must be an object');
96
+ });
97
+
98
+ test('should throw error for missing id', () => {
99
+ const invalid = { ...validWorkItem, id: null };
100
+ expect(() => setCurrentWork(invalid)).toThrow('Work item must have a numeric id');
101
+ });
102
+
103
+ test('should throw error for non-numeric id', () => {
104
+ const invalid = { ...validWorkItem, id: 'not-a-number' };
105
+ expect(() => setCurrentWork(invalid)).toThrow('Work item must have a numeric id');
106
+ });
107
+
108
+ test('should throw error for missing title', () => {
109
+ const invalid = { ...validWorkItem, title: null };
110
+ expect(() => setCurrentWork(invalid)).toThrow('Work item must have a string title');
111
+ });
112
+
113
+ test('should throw error for missing type', () => {
114
+ const invalid = { ...validWorkItem, type: null };
115
+ expect(() => setCurrentWork(invalid)).toThrow('Work item must have a string type');
116
+ });
117
+
118
+ test('should throw error for missing status', () => {
119
+ const invalid = { ...validWorkItem, status: null };
120
+ expect(() => setCurrentWork(invalid)).toThrow('Work item must have a string status');
121
+ });
122
+ });
123
+
124
+ describe('clearCurrentWork()', () => {
125
+ test('should do nothing if file does not exist', () => {
126
+ expect(() => clearCurrentWork()).not.toThrow();
127
+ });
128
+
129
+ test('should delete current work file', () => {
130
+ setCurrentWork(validWorkItem);
131
+ expect(fs.existsSync(getCurrentWorkPath())).toBe(true);
132
+
133
+ clearCurrentWork();
134
+ expect(fs.existsSync(getCurrentWorkPath())).toBe(false);
135
+ });
136
+
137
+ test('should allow setting work again after clearing', () => {
138
+ setCurrentWork(validWorkItem);
139
+ clearCurrentWork();
140
+ setCurrentWork(validWorkItem);
141
+
142
+ const result = getCurrentWork();
143
+ expect(result).toEqual(validWorkItem);
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,107 @@
1
+ const { getDb, closeDb } = require('./database');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ describe('Project Config Table', () => {
6
+ let db;
7
+ const originalCwd = process.cwd();
8
+ const testDir = path.join('/tmp', 'jettypod-project-config-test-' + Date.now());
9
+
10
+ beforeAll(() => {
11
+ // Use test database
12
+ // SAFETY: Only delete if testDir is in /tmp
13
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
14
+ fs.rmSync(testDir, { recursive: true });
15
+ }
16
+ fs.mkdirSync(testDir, { recursive: true });
17
+ process.chdir(testDir);
18
+ db = getDb();
19
+ });
20
+
21
+ afterEach((done) => {
22
+ // Clean up data between tests
23
+ db.run("DELETE FROM project_config", () => {
24
+ done();
25
+ });
26
+ });
27
+
28
+ afterAll(() => {
29
+ closeDb();
30
+ process.chdir(originalCwd);
31
+ // SAFETY: Only delete if testDir is in /tmp
32
+ if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
33
+ fs.rmSync(testDir, { recursive: true });
34
+ }
35
+ });
36
+
37
+ describe('Schema', () => {
38
+ it('should create project_config table', (done) => {
39
+ db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='project_config'", (err, row) => {
40
+ expect(err).toBeNull();
41
+ expect(row).toBeDefined();
42
+ expect(row.name).toBe('project_config');
43
+ done();
44
+ });
45
+ });
46
+
47
+ it('should have correct columns', (done) => {
48
+ db.all("PRAGMA table_info(project_config)", (err, columns) => {
49
+ expect(err).toBeNull();
50
+ const columnNames = columns.map(c => c.name);
51
+ expect(columnNames).toContain('id');
52
+ expect(columnNames).toContain('project_state');
53
+ expect(columnNames).toContain('updated_at');
54
+ done();
55
+ });
56
+ });
57
+
58
+ it('should default project_state to "internal"', (done) => {
59
+ db.run("INSERT INTO project_config (id) VALUES (1)", (err) => {
60
+ expect(err).toBeNull();
61
+ db.get("SELECT project_state FROM project_config WHERE id = 1", (err, row) => {
62
+ expect(err).toBeNull();
63
+ expect(row.project_state).toBe('internal');
64
+ done();
65
+ });
66
+ });
67
+ });
68
+
69
+ it('should enforce singleton constraint', (done) => {
70
+ db.run("INSERT INTO project_config (id, project_state) VALUES (1, 'internal')", (err) => {
71
+ expect(err).toBeNull();
72
+ db.run("INSERT INTO project_config (id, project_state) VALUES (2, 'external')", (err) => {
73
+ expect(err).toBeDefined(); // Should fail due to CHECK constraint
74
+ done();
75
+ });
76
+ });
77
+ });
78
+ });
79
+
80
+ describe('Operations', () => {
81
+ it('should insert and read project state', (done) => {
82
+ db.run("INSERT INTO project_config (id, project_state) VALUES (1, 'external')", (err) => {
83
+ expect(err).toBeNull();
84
+ db.get("SELECT * FROM project_config WHERE id = 1", (err, row) => {
85
+ expect(err).toBeNull();
86
+ expect(row.project_state).toBe('external');
87
+ expect(row.id).toBe(1);
88
+ done();
89
+ });
90
+ });
91
+ });
92
+
93
+ it('should update project state', (done) => {
94
+ db.run("INSERT INTO project_config (id, project_state) VALUES (1, 'internal')", (err) => {
95
+ expect(err).toBeNull();
96
+ db.run("UPDATE project_config SET project_state = 'external' WHERE id = 1", (err) => {
97
+ expect(err).toBeNull();
98
+ db.get("SELECT project_state FROM project_config WHERE id = 1", (err, row) => {
99
+ expect(err).toBeNull();
100
+ expect(row.project_state).toBe('external');
101
+ done();
102
+ });
103
+ });
104
+ });
105
+ });
106
+ });
107
+ });