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,69 @@
|
|
|
1
|
+
// Production readiness helpers
|
|
2
|
+
const { getDb } = require('./database');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get all features that are in stable mode and ready for production elevation
|
|
6
|
+
* @returns {Promise<Array>} Array of feature objects with id, title, description, etc.
|
|
7
|
+
*/
|
|
8
|
+
function getStableFeatures() {
|
|
9
|
+
const db = getDb();
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
db.all(`
|
|
13
|
+
SELECT *
|
|
14
|
+
FROM work_items
|
|
15
|
+
WHERE type = 'feature'
|
|
16
|
+
AND mode = 'stable'
|
|
17
|
+
AND phase = 'implementation'
|
|
18
|
+
ORDER BY id ASC
|
|
19
|
+
`, [], (err, rows) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
return reject(new Error(`Failed to fetch stable features: ${err.message}`));
|
|
22
|
+
}
|
|
23
|
+
resolve(rows || []);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create production chores for a given feature
|
|
30
|
+
* @param {Object} feature - Feature object with id and title
|
|
31
|
+
* @param {number} epicId - Epic ID to group chores under
|
|
32
|
+
* @returns {Promise<Array>} Array of created chore IDs
|
|
33
|
+
*/
|
|
34
|
+
async function createProductionChoresForFeature(feature, epicId) {
|
|
35
|
+
const { create } = require('../features/work-tracking');
|
|
36
|
+
|
|
37
|
+
const chores = [
|
|
38
|
+
{
|
|
39
|
+
title: `Add security hardening to ${feature.title}`,
|
|
40
|
+
description: `Add authentication/authorization checks, input sanitization, encryption, and attack protection (XSS, CSRF, injection) for ${feature.title}`
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: `Add scale testing to ${feature.title}`,
|
|
44
|
+
description: `Add performance optimization, load testing (100+ concurrent users), caching strategy, and monitoring for ${feature.title}`
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: `Add compliance requirements to ${feature.title}`,
|
|
48
|
+
description: `Add GDPR/HIPAA/SOC2 compliance, audit trails, data retention policies, and regulatory requirements for ${feature.title}`
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Create all chores (chores don't have modes - they inherit context from parent)
|
|
53
|
+
const choreIds = [];
|
|
54
|
+
for (const chore of chores) {
|
|
55
|
+
try {
|
|
56
|
+
const choreId = await create('chore', chore.title, chore.description, epicId, null, false);
|
|
57
|
+
choreIds.push(choreId);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new Error(`Failed to create production chore: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return choreIds;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
getStableFeatures,
|
|
68
|
+
createProductionChoresForFeature
|
|
69
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
describe('Project State', () => {
|
|
6
|
+
const testDir = path.join('/tmp', 'jettypod-project-state-test-' + Date.now());
|
|
7
|
+
const testConfigPath = path.join(testDir, 'config.json');
|
|
8
|
+
const originalPath = config.path;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
config.path = testConfigPath;
|
|
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
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
config.path = originalPath;
|
|
21
|
+
// SAFETY: Only delete if testDir is in /tmp
|
|
22
|
+
if (fs.existsSync(testDir) && testDir.startsWith('/tmp/')) {
|
|
23
|
+
fs.rmSync(testDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isValidProjectState', () => {
|
|
28
|
+
it('should accept "internal" as valid', () => {
|
|
29
|
+
expect(config.isValidProjectState('internal')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should accept "external" as valid', () => {
|
|
33
|
+
expect(config.isValidProjectState('external')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should reject invalid values', () => {
|
|
37
|
+
expect(config.isValidProjectState('production')).toBe(false);
|
|
38
|
+
expect(config.isValidProjectState('invalid')).toBe(false);
|
|
39
|
+
expect(config.isValidProjectState('')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('read', () => {
|
|
44
|
+
it('should default project_state to "internal" if not exists', () => {
|
|
45
|
+
const data = config.read();
|
|
46
|
+
expect(data.project_state).toBe('internal');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should default project_state to "internal" if invalid value in file', () => {
|
|
50
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({ name: 'test', project_state: 'invalid' }));
|
|
52
|
+
|
|
53
|
+
const data = config.read();
|
|
54
|
+
expect(data.project_state).toBe('internal');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should preserve valid project_state from file', () => {
|
|
58
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
59
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({ name: 'test', project_state: 'external' }));
|
|
60
|
+
|
|
61
|
+
const data = config.read();
|
|
62
|
+
expect(data.project_state).toBe('external');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('update', () => {
|
|
67
|
+
it('should update project_state to external', () => {
|
|
68
|
+
config.write({ name: 'test', project_state: 'internal' });
|
|
69
|
+
config.update({ project_state: 'external' });
|
|
70
|
+
|
|
71
|
+
const data = config.read();
|
|
72
|
+
expect(data.project_state).toBe('external');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject invalid project_state', () => {
|
|
76
|
+
config.write({ name: 'test', project_state: 'internal' });
|
|
77
|
+
|
|
78
|
+
expect(() => {
|
|
79
|
+
config.update({ project_state: 'invalid' });
|
|
80
|
+
}).toThrow('Invalid project_state: invalid');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should allow updating other fields without affecting project_state', () => {
|
|
84
|
+
config.write({ name: 'test', project_state: 'internal' });
|
|
85
|
+
config.update({ description: 'Test project' });
|
|
86
|
+
|
|
87
|
+
const data = config.read();
|
|
88
|
+
expect(data.project_state).toBe('internal');
|
|
89
|
+
expect(data.description).toBe('Test project');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates an isolated test directory for testing
|
|
8
|
+
* @returns {Object} Test context with testDir and originalCwd
|
|
9
|
+
* @throws {Error} If temporary directory cannot be created
|
|
10
|
+
*/
|
|
11
|
+
function createTestEnvironment() {
|
|
12
|
+
const originalCwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
let testDir;
|
|
15
|
+
try {
|
|
16
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jettypod-test-'));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
throw new Error(`Failed to create test directory: ${err.message}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
originalCwd,
|
|
23
|
+
testDir,
|
|
24
|
+
cleanup: () => {
|
|
25
|
+
try {
|
|
26
|
+
process.chdir(originalCwd);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Directory may have been deleted, ignore
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(testDir)) {
|
|
31
|
+
try {
|
|
32
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.warn(`Warning: Failed to cleanup test directory: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Copies directory recursively, excluding test files
|
|
43
|
+
* @param {string} src - Source directory
|
|
44
|
+
* @param {string} dest - Destination directory
|
|
45
|
+
* @throws {Error} If src doesn't exist, is not a directory, or copy fails
|
|
46
|
+
*/
|
|
47
|
+
function copyDirRecursive(src, dest) {
|
|
48
|
+
if (!src || typeof src !== 'string') {
|
|
49
|
+
throw new Error('Source path must be a non-empty string');
|
|
50
|
+
}
|
|
51
|
+
if (!dest || typeof dest !== 'string') {
|
|
52
|
+
throw new Error('Destination path must be a non-empty string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(src)) {
|
|
56
|
+
throw new Error(`Source directory does not exist: ${src}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const srcStat = fs.statSync(src);
|
|
60
|
+
if (!srcStat.isDirectory()) {
|
|
61
|
+
throw new Error(`Source is not a directory: ${src}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(dest)) {
|
|
66
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.readdirSync(src).forEach(item => {
|
|
70
|
+
const srcPath = path.join(src, item);
|
|
71
|
+
const destPath = path.join(dest, item);
|
|
72
|
+
|
|
73
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
74
|
+
copyDirRecursive(srcPath, destPath);
|
|
75
|
+
} else {
|
|
76
|
+
// Skip test files when copying
|
|
77
|
+
if (!item.endsWith('.test.js')) {
|
|
78
|
+
fs.copyFileSync(srcPath, destPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw new Error(`Failed to copy directory from ${src} to ${dest}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs a JettyPod command in test environment
|
|
89
|
+
* @param {string} cmd - Command to run (e.g., 'jettypod init')
|
|
90
|
+
* @param {Object} context - Test context with originalCwd
|
|
91
|
+
* @returns {string} Command output
|
|
92
|
+
* @throws {Error} If cmd is invalid or context is missing
|
|
93
|
+
*/
|
|
94
|
+
function runCommand(cmd, context) {
|
|
95
|
+
if (!cmd || typeof cmd !== 'string') {
|
|
96
|
+
throw new Error('Command must be a non-empty string');
|
|
97
|
+
}
|
|
98
|
+
if (!context || typeof context !== 'object') {
|
|
99
|
+
throw new Error('Context must be an object');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const productionJettypodPath = context.originalCwd ?
|
|
104
|
+
path.join(context.originalCwd, 'jettypod.js') :
|
|
105
|
+
path.join(process.cwd(), '..', '..', 'jettypod.js');
|
|
106
|
+
|
|
107
|
+
if (!fs.existsSync(productionJettypodPath)) {
|
|
108
|
+
throw new Error(`JettyPod script not found at: ${productionJettypodPath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fullCmd = cmd.replace(/^jettypod/, `node ${productionJettypodPath}`);
|
|
112
|
+
return execSync(fullCmd, {
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
stdio: 'pipe'
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return error.stdout || error.message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reads config.json from .jettypod directory
|
|
123
|
+
* @returns {Object|null} Config object or null if not found
|
|
124
|
+
* @throws {Error} If JSON is corrupted or file cannot be read
|
|
125
|
+
*/
|
|
126
|
+
function readConfig() {
|
|
127
|
+
const configPath = '.jettypod/config.json';
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(configPath)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
135
|
+
return JSON.parse(content);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof SyntaxError) {
|
|
138
|
+
throw new Error(`Config file contains invalid JSON: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Failed to read config file: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reads CLAUDE.md from current directory
|
|
146
|
+
* @returns {string} CLAUDE.md content or empty string
|
|
147
|
+
* @throws {Error} If file exists but cannot be read
|
|
148
|
+
*/
|
|
149
|
+
function readClaude() {
|
|
150
|
+
if (!fs.existsSync('CLAUDE.md')) {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
return fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new Error(`Failed to read CLAUDE.md: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Initializes tracking for created files/directories
|
|
163
|
+
* @param {Object} context - Test context to initialize
|
|
164
|
+
* @throws {Error} If context is invalid
|
|
165
|
+
*/
|
|
166
|
+
function initializeTracking(context) {
|
|
167
|
+
if (!context || typeof context !== 'object') {
|
|
168
|
+
throw new Error('Context must be an object');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
context.createdFiles = [];
|
|
172
|
+
context.createdDirs = [];
|
|
173
|
+
context.modifiedFiles = new Map();
|
|
174
|
+
context.createdTestDirs = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
createTestEnvironment,
|
|
179
|
+
copyDirRecursive,
|
|
180
|
+
runCommand,
|
|
181
|
+
readConfig,
|
|
182
|
+
readClaude,
|
|
183
|
+
initializeTracking
|
|
184
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const {
|
|
5
|
+
createTestEnvironment,
|
|
6
|
+
copyDirRecursive,
|
|
7
|
+
runCommand,
|
|
8
|
+
readConfig,
|
|
9
|
+
readClaude,
|
|
10
|
+
initializeTracking
|
|
11
|
+
} = require('./test-helpers');
|
|
12
|
+
|
|
13
|
+
describe('Test Helpers Module', () => {
|
|
14
|
+
describe('createTestEnvironment()', () => {
|
|
15
|
+
test('should create isolated test directory', () => {
|
|
16
|
+
const env = createTestEnvironment();
|
|
17
|
+
|
|
18
|
+
expect(env.testDir).toBeTruthy();
|
|
19
|
+
expect(fs.existsSync(env.testDir)).toBe(true);
|
|
20
|
+
expect(env.testDir).toContain('jettypod-test-');
|
|
21
|
+
expect(env.originalCwd).toBeTruthy();
|
|
22
|
+
|
|
23
|
+
env.cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should provide cleanup function', () => {
|
|
27
|
+
const env = createTestEnvironment();
|
|
28
|
+
const testDir = env.testDir;
|
|
29
|
+
|
|
30
|
+
expect(fs.existsSync(testDir)).toBe(true);
|
|
31
|
+
env.cleanup();
|
|
32
|
+
expect(fs.existsSync(testDir)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should handle cleanup when directory already deleted', () => {
|
|
36
|
+
const env = createTestEnvironment();
|
|
37
|
+
fs.rmSync(env.testDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
expect(() => env.cleanup()).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should restore original working directory on cleanup', () => {
|
|
43
|
+
const originalCwd = process.cwd();
|
|
44
|
+
const env = createTestEnvironment();
|
|
45
|
+
|
|
46
|
+
process.chdir(env.testDir);
|
|
47
|
+
env.cleanup();
|
|
48
|
+
|
|
49
|
+
expect(process.cwd()).toBe(originalCwd);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('copyDirRecursive()', () => {
|
|
54
|
+
let testEnv;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
testEnv = createTestEnvironment();
|
|
58
|
+
process.chdir(testEnv.testDir);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
testEnv.cleanup();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should copy directory recursively', () => {
|
|
66
|
+
const srcDir = path.join(testEnv.testDir, 'src');
|
|
67
|
+
const destDir = path.join(testEnv.testDir, 'dest');
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(srcDir);
|
|
70
|
+
fs.writeFileSync(path.join(srcDir, 'file1.js'), 'content1');
|
|
71
|
+
fs.writeFileSync(path.join(srcDir, 'file2.js'), 'content2');
|
|
72
|
+
|
|
73
|
+
copyDirRecursive(srcDir, destDir);
|
|
74
|
+
|
|
75
|
+
expect(fs.existsSync(path.join(destDir, 'file1.js'))).toBe(true);
|
|
76
|
+
expect(fs.existsSync(path.join(destDir, 'file2.js'))).toBe(true);
|
|
77
|
+
expect(fs.readFileSync(path.join(destDir, 'file1.js'), 'utf-8')).toBe('content1');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should skip .test.js files when copying', () => {
|
|
81
|
+
const srcDir = path.join(testEnv.testDir, 'src');
|
|
82
|
+
const destDir = path.join(testEnv.testDir, 'dest');
|
|
83
|
+
|
|
84
|
+
fs.mkdirSync(srcDir);
|
|
85
|
+
fs.writeFileSync(path.join(srcDir, 'file.js'), 'content');
|
|
86
|
+
fs.writeFileSync(path.join(srcDir, 'file.test.js'), 'test content');
|
|
87
|
+
|
|
88
|
+
copyDirRecursive(srcDir, destDir);
|
|
89
|
+
|
|
90
|
+
expect(fs.existsSync(path.join(destDir, 'file.js'))).toBe(true);
|
|
91
|
+
expect(fs.existsSync(path.join(destDir, 'file.test.js'))).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should copy nested directories', () => {
|
|
95
|
+
const srcDir = path.join(testEnv.testDir, 'src');
|
|
96
|
+
const destDir = path.join(testEnv.testDir, 'dest');
|
|
97
|
+
const nestedDir = path.join(srcDir, 'nested');
|
|
98
|
+
|
|
99
|
+
fs.mkdirSync(srcDir);
|
|
100
|
+
fs.mkdirSync(nestedDir);
|
|
101
|
+
fs.writeFileSync(path.join(nestedDir, 'file.js'), 'nested content');
|
|
102
|
+
|
|
103
|
+
copyDirRecursive(srcDir, destDir);
|
|
104
|
+
|
|
105
|
+
expect(fs.existsSync(path.join(destDir, 'nested', 'file.js'))).toBe(true);
|
|
106
|
+
expect(fs.readFileSync(path.join(destDir, 'nested', 'file.js'), 'utf-8')).toBe('nested content');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should create destination directory if it does not exist', () => {
|
|
110
|
+
const srcDir = path.join(testEnv.testDir, 'src');
|
|
111
|
+
const destDir = path.join(testEnv.testDir, 'dest');
|
|
112
|
+
|
|
113
|
+
fs.mkdirSync(srcDir);
|
|
114
|
+
fs.writeFileSync(path.join(srcDir, 'file.js'), 'content');
|
|
115
|
+
|
|
116
|
+
expect(fs.existsSync(destDir)).toBe(false);
|
|
117
|
+
copyDirRecursive(srcDir, destDir);
|
|
118
|
+
expect(fs.existsSync(destDir)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('should throw error for non-existent source directory', () => {
|
|
122
|
+
const srcDir = path.join(testEnv.testDir, 'nonexistent');
|
|
123
|
+
const destDir = path.join(testEnv.testDir, 'dest');
|
|
124
|
+
|
|
125
|
+
expect(() => copyDirRecursive(srcDir, destDir)).toThrow('Source directory does not exist');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should throw error for invalid source path', () => {
|
|
129
|
+
expect(() => copyDirRecursive(null, 'dest')).toThrow('Source path must be a non-empty string');
|
|
130
|
+
expect(() => copyDirRecursive('', 'dest')).toThrow('Source path must be a non-empty string');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should throw error for invalid destination path', () => {
|
|
134
|
+
const srcDir = path.join(testEnv.testDir, 'src');
|
|
135
|
+
fs.mkdirSync(srcDir);
|
|
136
|
+
|
|
137
|
+
expect(() => copyDirRecursive(srcDir, null)).toThrow('Destination path must be a non-empty string');
|
|
138
|
+
expect(() => copyDirRecursive(srcDir, '')).toThrow('Destination path must be a non-empty string');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('should throw error if source is not a directory', () => {
|
|
142
|
+
const srcFile = path.join(testEnv.testDir, 'file.js');
|
|
143
|
+
fs.writeFileSync(srcFile, 'content');
|
|
144
|
+
|
|
145
|
+
expect(() => copyDirRecursive(srcFile, 'dest')).toThrow('Source is not a directory');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('runCommand()', () => {
|
|
150
|
+
let testEnv;
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
testEnv = createTestEnvironment();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
testEnv.cleanup();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('should throw error for invalid command', () => {
|
|
161
|
+
expect(() => runCommand(null, testEnv)).toThrow('Command must be a non-empty string');
|
|
162
|
+
expect(() => runCommand('', testEnv)).toThrow('Command must be a non-empty string');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should throw error for invalid context', () => {
|
|
166
|
+
expect(() => runCommand('jettypod --version', null)).toThrow('Context must be an object');
|
|
167
|
+
expect(() => runCommand('jettypod --version', 'string')).toThrow('Context must be an object');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should return error message if jettypod.js not found', () => {
|
|
171
|
+
const invalidContext = { originalCwd: '/nonexistent/path' };
|
|
172
|
+
const result = runCommand('jettypod --version', invalidContext);
|
|
173
|
+
expect(result).toContain('JettyPod script not found');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('readConfig()', () => {
|
|
178
|
+
let testEnv;
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
testEnv = createTestEnvironment();
|
|
182
|
+
process.chdir(testEnv.testDir);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterEach(() => {
|
|
186
|
+
testEnv.cleanup();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should return null if config file does not exist', () => {
|
|
190
|
+
expect(readConfig()).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('should read config.json successfully', () => {
|
|
194
|
+
const jettypodDir = path.join(testEnv.testDir, '.jettypod');
|
|
195
|
+
fs.mkdirSync(jettypodDir);
|
|
196
|
+
fs.writeFileSync(
|
|
197
|
+
path.join(jettypodDir, 'config.json'),
|
|
198
|
+
JSON.stringify({ mode: 'speed', stage: 'starting' })
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const config = readConfig();
|
|
202
|
+
expect(config).toEqual({ mode: 'speed', stage: 'starting' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('should throw error for corrupted JSON', () => {
|
|
206
|
+
const jettypodDir = path.join(testEnv.testDir, '.jettypod');
|
|
207
|
+
fs.mkdirSync(jettypodDir);
|
|
208
|
+
fs.writeFileSync(path.join(jettypodDir, 'config.json'), 'not valid json');
|
|
209
|
+
|
|
210
|
+
expect(() => readConfig()).toThrow('Config file contains invalid JSON');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('readClaude()', () => {
|
|
215
|
+
let testEnv;
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
testEnv = createTestEnvironment();
|
|
219
|
+
process.chdir(testEnv.testDir);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
testEnv.cleanup();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('should return empty string if CLAUDE.md does not exist', () => {
|
|
227
|
+
expect(readClaude()).toBe('');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('should read CLAUDE.md successfully', () => {
|
|
231
|
+
fs.writeFileSync('CLAUDE.md', '# Test CLAUDE.md');
|
|
232
|
+
|
|
233
|
+
const content = readClaude();
|
|
234
|
+
expect(content).toBe('# Test CLAUDE.md');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('initializeTracking()', () => {
|
|
239
|
+
test('should initialize tracking properties on context', () => {
|
|
240
|
+
const context = {};
|
|
241
|
+
initializeTracking(context);
|
|
242
|
+
|
|
243
|
+
expect(context.createdFiles).toEqual([]);
|
|
244
|
+
expect(context.createdDirs).toEqual([]);
|
|
245
|
+
expect(context.modifiedFiles).toBeInstanceOf(Map);
|
|
246
|
+
expect(context.createdTestDirs).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('should throw error for invalid context', () => {
|
|
250
|
+
expect(() => initializeTracking(null)).toThrow('Context must be an object');
|
|
251
|
+
expect(() => initializeTracking('string')).toThrow('Context must be an object');
|
|
252
|
+
expect(() => initializeTracking(undefined)).toThrow('Context must be an object');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jettypod",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Simplified development mode manager",
|
|
5
|
+
"main": "jettypod.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jettypod": "./jettypod.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "npm run test:unit && npm run test:bdd && npm run test:cleanup",
|
|
11
|
+
"test:bdd": "NODE_ENV=test cucumber-js",
|
|
12
|
+
"test:unit": "NODE_ENV=test jest",
|
|
13
|
+
"test:unit:watch": "NODE_ENV=test jest --watch",
|
|
14
|
+
"test:cleanup": "node -e \"const fs = require('fs'); const path = require('path'); const testDb = path.join(process.cwd(), '.jettypod', 'test-work.db'); if (fs.existsSync(testDb)) { fs.unlinkSync(testDb); console.log('✅ Cleaned up test database'); }\""
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@cucumber/cucumber": "^10.0.1",
|
|
18
|
+
"chai": "^6.0.1",
|
|
19
|
+
"jest": "^30.1.3"
|
|
20
|
+
},
|
|
21
|
+
"jest": {
|
|
22
|
+
"testEnvironment": "node",
|
|
23
|
+
"testMatch": [
|
|
24
|
+
"**/__tests__/**/*.js",
|
|
25
|
+
"**/*.test.js"
|
|
26
|
+
],
|
|
27
|
+
"testPathIgnorePatterns": [
|
|
28
|
+
"/node_modules/"
|
|
29
|
+
],
|
|
30
|
+
"collectCoverage": false,
|
|
31
|
+
"coverageDirectory": "coverage"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"sqlite3": "^5.1.7"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
test
|