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
package/lib/constants.js
ADDED
|
@@ -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
|
+
});
|