feat: Enhanced project initialization with Git worktree detection (#743)
* Fix Cursor deeplink installation with copy-paste instructions (#723) * detect git worktree * add changeset * add aliases and git flags * add changeset * rename and update test * add store tasks in git functionality * update changeset * fix newline * remove unused import * update command wording * update command option text
This commit is contained in:
581
tests/integration/manage-gitignore.test.js
Normal file
581
tests/integration/manage-gitignore.test.js
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* Integration tests for manage-gitignore.js module
|
||||
* Tests actual file system operations in a temporary directory
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import manageGitignoreFile from '../../src/utils/manage-gitignore.js';
|
||||
|
||||
describe('manage-gitignore.js Integration Tests', () => {
|
||||
let tempDir;
|
||||
let testGitignorePath;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary directory for each test
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
|
||||
testGitignorePath = path.join(tempDir, '.gitignore');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directory after each test
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('New File Creation', () => {
|
||||
const templateContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
test('should create new .gitignore file with commented task lines (storeTasksInGit = true)', () => {
|
||||
const logs = [];
|
||||
const mockLog = (level, message) => logs.push({ level, message });
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, true, mockLog);
|
||||
|
||||
// Verify file was created
|
||||
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||
|
||||
// Verify content
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
expect(content).toContain('# Logs');
|
||||
expect(content).toContain('logs');
|
||||
expect(content).toContain('# Dependencies');
|
||||
expect(content).toContain('node_modules/');
|
||||
expect(content).toContain('# Task files');
|
||||
expect(content).toContain('tasks.json');
|
||||
expect(content).toContain('tasks/');
|
||||
|
||||
// Verify task lines are commented (storeTasksInGit = true)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||
);
|
||||
|
||||
// Verify log message
|
||||
expect(logs).toContainEqual({
|
||||
level: 'success',
|
||||
message: expect.stringContaining('Created')
|
||||
});
|
||||
});
|
||||
|
||||
test('should create new .gitignore file with uncommented task lines (storeTasksInGit = false)', () => {
|
||||
const logs = [];
|
||||
const mockLog = (level, message) => logs.push({ level, message });
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||
|
||||
// Verify file was created
|
||||
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||
|
||||
// Verify content
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
expect(content).toContain('# Task files');
|
||||
|
||||
// Verify task lines are uncommented (storeTasksInGit = false)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
|
||||
// Verify log message
|
||||
expect(logs).toContainEqual({
|
||||
level: 'success',
|
||||
message: expect.stringContaining('Created')
|
||||
});
|
||||
});
|
||||
|
||||
test('should work without log function', () => {
|
||||
expect(() => {
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Merging', () => {
|
||||
const templateContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
test('should merge template with existing file content', () => {
|
||||
// Create existing .gitignore file
|
||||
const existingContent = `# Existing content
|
||||
old-files.txt
|
||||
*.backup
|
||||
|
||||
# Old task files (to be replaced)
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/
|
||||
|
||||
# More existing content
|
||||
cache/`;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
const logs = [];
|
||||
const mockLog = (level, message) => logs.push({ level, message });
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||
|
||||
// Verify file still exists
|
||||
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain existing non-task content
|
||||
expect(content).toContain('# Existing content');
|
||||
expect(content).toContain('old-files.txt');
|
||||
expect(content).toContain('*.backup');
|
||||
expect(content).toContain('# More existing content');
|
||||
expect(content).toContain('cache/');
|
||||
|
||||
// Should add new template content
|
||||
expect(content).toContain('# Logs');
|
||||
expect(content).toContain('logs');
|
||||
expect(content).toContain('# Dependencies');
|
||||
expect(content).toContain('node_modules/');
|
||||
expect(content).toContain('# Environment variables');
|
||||
expect(content).toContain('.env');
|
||||
|
||||
// Should replace task section with new preference (storeTasksInGit = false means uncommented)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
|
||||
// Verify log message
|
||||
expect(logs).toContainEqual({
|
||||
level: 'success',
|
||||
message: expect.stringContaining('Updated')
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle switching task preferences from commented to uncommented', () => {
|
||||
// Create existing file with commented task lines
|
||||
const existingContent = `# Existing
|
||||
existing.txt
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
// Update with storeTasksInGit = true (commented)
|
||||
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain existing content
|
||||
expect(content).toContain('# Existing');
|
||||
expect(content).toContain('existing.txt');
|
||||
|
||||
// Should have commented task lines (storeTasksInGit = true)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle switching task preferences from uncommented to commented', () => {
|
||||
// Create existing file with uncommented task lines
|
||||
const existingContent = `# Existing
|
||||
existing.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
// Update with storeTasksInGit = false (uncommented)
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain existing content
|
||||
expect(content).toContain('# Existing');
|
||||
expect(content).toContain('existing.txt');
|
||||
|
||||
// Should have uncommented task lines (storeTasksInGit = false)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should not duplicate existing template content', () => {
|
||||
// Create existing file that already has some template content
|
||||
const existingContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Custom content
|
||||
custom.txt
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should not duplicate logs section
|
||||
const logsMatches = content.match(/# Logs/g);
|
||||
expect(logsMatches).toHaveLength(1);
|
||||
|
||||
// Should not duplicate dependencies section
|
||||
const depsMatches = content.match(/# Dependencies/g);
|
||||
expect(depsMatches).toHaveLength(1);
|
||||
|
||||
// Should retain custom content
|
||||
expect(content).toContain('# Custom content');
|
||||
expect(content).toContain('custom.txt');
|
||||
|
||||
// Should add new template content that wasn't present
|
||||
expect(content).toContain('# Environment variables');
|
||||
expect(content).toContain('.env');
|
||||
});
|
||||
|
||||
test('should handle empty existing file', () => {
|
||||
// Create empty file
|
||||
fs.writeFileSync(testGitignorePath, '');
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
|
||||
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
expect(content).toContain('# Logs');
|
||||
expect(content).toContain('# Task files');
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle file with only whitespace', () => {
|
||||
// Create file with only whitespace
|
||||
fs.writeFileSync(testGitignorePath, ' \n\n \n');
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
expect(content).toContain('# Logs');
|
||||
expect(content).toContain('# Task files');
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Task Section Handling', () => {
|
||||
test('should remove task section with mixed comments and spacing', () => {
|
||||
const existingContent = `# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Task files
|
||||
|
||||
# tasks.json
|
||||
tasks/
|
||||
|
||||
|
||||
# More content
|
||||
more.txt`;
|
||||
|
||||
const templateContent = `# New content
|
||||
new.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain non-task content
|
||||
expect(content).toContain('# Dependencies');
|
||||
expect(content).toContain('node_modules/');
|
||||
expect(content).toContain('# More content');
|
||||
expect(content).toContain('more.txt');
|
||||
|
||||
// Should add new content
|
||||
expect(content).toContain('# New content');
|
||||
expect(content).toContain('new.txt');
|
||||
|
||||
// Should have clean task section (storeTasksInGit = false means uncommented)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple task file variations', () => {
|
||||
const existingContent = `# Existing
|
||||
existing.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
# tasks.json
|
||||
# tasks/
|
||||
tasks/
|
||||
#tasks.json
|
||||
|
||||
# More content
|
||||
more.txt`;
|
||||
|
||||
const templateContent = `# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain non-task content
|
||||
expect(content).toContain('# Existing');
|
||||
expect(content).toContain('existing.txt');
|
||||
expect(content).toContain('# More content');
|
||||
expect(content).toContain('more.txt');
|
||||
|
||||
// Should have clean task section with preference applied (storeTasksInGit = true means commented)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||
);
|
||||
|
||||
// Should not have multiple task sections
|
||||
const taskFileMatches = content.match(/# Task files/g);
|
||||
expect(taskFileMatches).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle permission errors gracefully', () => {
|
||||
// Create a directory where we would create the file, then remove write permissions
|
||||
const readOnlyDir = path.join(tempDir, 'readonly');
|
||||
fs.mkdirSync(readOnlyDir);
|
||||
fs.chmodSync(readOnlyDir, 0o444); // Read-only
|
||||
|
||||
const readOnlyGitignorePath = path.join(readOnlyDir, '.gitignore');
|
||||
const templateContent = `# Test
|
||||
test.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
const logs = [];
|
||||
const mockLog = (level, message) => logs.push({ level, message });
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile(
|
||||
readOnlyGitignorePath,
|
||||
templateContent,
|
||||
false,
|
||||
mockLog
|
||||
);
|
||||
}).toThrow();
|
||||
|
||||
// Verify error was logged
|
||||
expect(logs).toContainEqual({
|
||||
level: 'error',
|
||||
message: expect.stringContaining('Failed to create')
|
||||
});
|
||||
|
||||
// Restore permissions for cleanup
|
||||
fs.chmodSync(readOnlyDir, 0o755);
|
||||
});
|
||||
|
||||
test('should handle read errors on existing files', () => {
|
||||
// Create a file then remove read permissions
|
||||
fs.writeFileSync(testGitignorePath, 'existing content');
|
||||
fs.chmodSync(testGitignorePath, 0o000); // No permissions
|
||||
|
||||
const templateContent = `# Test
|
||||
test.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
const logs = [];
|
||||
const mockLog = (level, message) => logs.push({ level, message });
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||
}).toThrow();
|
||||
|
||||
// Verify error was logged
|
||||
expect(logs).toContainEqual({
|
||||
level: 'error',
|
||||
message: expect.stringContaining('Failed to merge content')
|
||||
});
|
||||
|
||||
// Restore permissions for cleanup
|
||||
fs.chmodSync(testGitignorePath, 0o644);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Scenarios', () => {
|
||||
test('should handle typical Node.js project .gitignore', () => {
|
||||
const existingNodeGitignore = `# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next`;
|
||||
|
||||
const taskMasterTemplate = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingNodeGitignore);
|
||||
|
||||
manageGitignoreFile(testGitignorePath, taskMasterTemplate, false);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain existing Node.js specific entries
|
||||
expect(content).toContain('npm-debug.log*');
|
||||
expect(content).toContain('yarn-debug.log*');
|
||||
expect(content).toContain('*.pid');
|
||||
expect(content).toContain('jspm_packages/');
|
||||
expect(content).toContain('.npm');
|
||||
expect(content).toContain('*.tgz');
|
||||
expect(content).toContain('.yarn-integrity');
|
||||
expect(content).toContain('.next');
|
||||
|
||||
// Should add new content from template that wasn't present
|
||||
expect(content).toContain('dist/');
|
||||
expect(content).toContain('build/');
|
||||
|
||||
// Should add task files section with correct preference (storeTasksInGit = false means uncommented)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
|
||||
// Should not duplicate common entries
|
||||
const nodeModulesMatches = content.match(/node_modules\//g);
|
||||
expect(nodeModulesMatches).toHaveLength(1);
|
||||
|
||||
const logsMatches = content.match(/# Logs/g);
|
||||
expect(logsMatches).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should handle project with existing task files in git', () => {
|
||||
const existingContent = `# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Current task setup - keeping in git
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/
|
||||
|
||||
# Build output
|
||||
dist/`;
|
||||
|
||||
const templateContent = `# New template
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
fs.writeFileSync(testGitignorePath, existingContent);
|
||||
|
||||
// Change preference to exclude tasks from git (storeTasksInGit = false means uncommented/ignored)
|
||||
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||
|
||||
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||
|
||||
// Should retain existing content
|
||||
expect(content).toContain('# Dependencies');
|
||||
expect(content).toContain('node_modules/');
|
||||
expect(content).toContain('# Logs');
|
||||
expect(content).toContain('*.log');
|
||||
expect(content).toContain('# Build output');
|
||||
expect(content).toContain('dist/');
|
||||
|
||||
// Should update task preference to uncommented (storeTasksInGit = false)
|
||||
expect(content).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
538
tests/unit/initialize-project.test.js
Normal file
538
tests/unit/initialize-project.test.js
Normal file
@@ -0,0 +1,538 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Reduce noise in test output
|
||||
process.env.TASKMASTER_LOG_LEVEL = 'error';
|
||||
|
||||
// === Mock everything early ===
|
||||
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
||||
jest.mock('fs', () => ({
|
||||
...jest.requireActual('fs'),
|
||||
mkdirSync: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
readFileSync: jest.fn(),
|
||||
appendFileSync: jest.fn(),
|
||||
existsSync: jest.fn(),
|
||||
mkdtempSync: jest.requireActual('fs').mkdtempSync,
|
||||
rmSync: jest.requireActual('fs').rmSync
|
||||
}));
|
||||
|
||||
// Mock console methods to suppress output
|
||||
const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
|
||||
consoleMethods.forEach((method) => {
|
||||
global.console[method] = jest.fn();
|
||||
});
|
||||
|
||||
// Mock ES modules using unstable_mockModule
|
||||
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
||||
isSilentMode: jest.fn(() => true),
|
||||
enableSilentMode: jest.fn(),
|
||||
log: jest.fn(),
|
||||
findProjectRoot: jest.fn(() => process.cwd())
|
||||
}));
|
||||
|
||||
// Mock git-utils module
|
||||
jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
|
||||
insideGitWorkTree: jest.fn(() => false)
|
||||
}));
|
||||
|
||||
// Mock rule transformer
|
||||
jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
|
||||
convertAllRulesToProfileRules: jest.fn(),
|
||||
getRulesProfile: jest.fn(() => ({
|
||||
conversionConfig: {},
|
||||
globalReplacements: []
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock any other modules that might output or do real operations
|
||||
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||
createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
|
||||
saveConfig: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock display libraries
|
||||
jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
|
||||
jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
|
||||
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
|
||||
jest.mock('chalk', () => ({
|
||||
blue: jest.fn((text) => text),
|
||||
green: jest.fn((text) => text),
|
||||
red: jest.fn((text) => text),
|
||||
yellow: jest.fn((text) => text),
|
||||
cyan: jest.fn((text) => text),
|
||||
white: jest.fn((text) => text),
|
||||
dim: jest.fn((text) => text),
|
||||
bold: jest.fn((text) => text),
|
||||
underline: jest.fn((text) => text)
|
||||
}));
|
||||
|
||||
const { execSync } = jest.requireMock('child_process');
|
||||
const mockFs = jest.requireMock('fs');
|
||||
|
||||
// Import the mocked modules
|
||||
const mockUtils = await import('../../scripts/modules/utils.js');
|
||||
const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
|
||||
const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
|
||||
|
||||
// Import after mocks
|
||||
const { initializeProject } = await import('../../scripts/init.js');
|
||||
|
||||
describe('initializeProject – Git / Alias flag logic', () => {
|
||||
let tmpDir;
|
||||
const origCwd = process.cwd();
|
||||
|
||||
// Standard non-interactive options for all tests
|
||||
const baseOptions = {
|
||||
yes: true,
|
||||
skipInstall: true,
|
||||
name: 'test-project',
|
||||
description: 'Test project description',
|
||||
version: '1.0.0',
|
||||
author: 'Test Author'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up basic fs mocks
|
||||
mockFs.mkdirSync.mockImplementation(() => {});
|
||||
mockFs.writeFileSync.mockImplementation(() => {});
|
||||
mockFs.readFileSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||
return 'mock template content';
|
||||
}
|
||||
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||
return '# existing config';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
mockFs.appendFileSync.mockImplementation(() => {});
|
||||
mockFs.existsSync.mockImplementation((filePath) => {
|
||||
// Template source files exist
|
||||
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||
return true;
|
||||
}
|
||||
// Shell config files exist by default
|
||||
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Reset utils mocks
|
||||
mockUtils.isSilentMode.mockReturnValue(true);
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
|
||||
// Default execSync mock
|
||||
execSync.mockImplementation(() => '');
|
||||
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(origCwd);
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Git Flag Behavior', () => {
|
||||
it('completes successfully with git:false in dry run', async () => {
|
||||
const result = await initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: true
|
||||
});
|
||||
|
||||
expect(result.dryRun).toBe(true);
|
||||
});
|
||||
|
||||
it('completes successfully with git:true when not inside repo', async () => {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('completes successfully when already inside repo', async () => {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(true);
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('uses default git behavior without errors', async () => {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('handles git command failures gracefully', async () => {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
execSync.mockImplementation((cmd) => {
|
||||
if (cmd.includes('git init')) {
|
||||
throw new Error('git not found');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alias Flag Behavior', () => {
|
||||
it('completes successfully when aliases:true and environment is set up', async () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalHome = process.env.HOME;
|
||||
|
||||
process.env.SHELL = '/bin/zsh';
|
||||
process.env.HOME = '/mock/home';
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: true,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
process.env.SHELL = originalShell;
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('completes successfully when aliases:false', async () => {
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('handles missing shell gracefully', async () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalHome = process.env.HOME;
|
||||
|
||||
delete process.env.SHELL; // Remove shell env var
|
||||
process.env.HOME = '/mock/home';
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: true,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
process.env.SHELL = originalShell;
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('handles missing shell config file gracefully', async () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalHome = process.env.HOME;
|
||||
|
||||
process.env.SHELL = '/bin/zsh';
|
||||
process.env.HOME = '/mock/home';
|
||||
|
||||
// Shell config doesn't exist
|
||||
mockFs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||
return false;
|
||||
}
|
||||
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: true,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
process.env.SHELL = originalShell;
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flag Combinations', () => {
|
||||
it.each`
|
||||
git | aliases | description
|
||||
${true} | ${true} | ${'git & aliases enabled'}
|
||||
${true} | ${false} | ${'git enabled, aliases disabled'}
|
||||
${false} | ${true} | ${'git disabled, aliases enabled'}
|
||||
${false} | ${false} | ${'git & aliases disabled'}
|
||||
`('handles $description without errors', async ({ git, aliases }) => {
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalHome = process.env.HOME;
|
||||
|
||||
if (aliases) {
|
||||
process.env.SHELL = '/bin/zsh';
|
||||
process.env.HOME = '/mock/home';
|
||||
}
|
||||
|
||||
if (git) {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
}
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git,
|
||||
aliases,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
process.env.SHELL = originalShell;
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dry Run Mode', () => {
|
||||
it('returns dry run result and performs no operations', async () => {
|
||||
const result = await initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: true,
|
||||
dryRun: true
|
||||
});
|
||||
|
||||
expect(result.dryRun).toBe(true);
|
||||
});
|
||||
|
||||
it.each`
|
||||
git | aliases | description
|
||||
${true} | ${false} | ${'git-specific behavior'}
|
||||
${false} | ${false} | ${'no-git behavior'}
|
||||
${false} | ${true} | ${'alias behavior'}
|
||||
`('shows $description in dry run', async ({ git, aliases }) => {
|
||||
const result = await initializeProject({
|
||||
...baseOptions,
|
||||
git,
|
||||
aliases,
|
||||
dryRun: true
|
||||
});
|
||||
|
||||
expect(result.dryRun).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('handles npm install failures gracefully', async () => {
|
||||
execSync.mockImplementation((cmd) => {
|
||||
if (cmd.includes('npm install')) {
|
||||
throw new Error('npm failed');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
skipInstall: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('handles git failures gracefully', async () => {
|
||||
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||
execSync.mockImplementation((cmd) => {
|
||||
if (cmd.includes('git init')) {
|
||||
throw new Error('git failed');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('handles file system errors gracefully', async () => {
|
||||
mockFs.mkdirSync.mockImplementation(() => {
|
||||
throw new Error('Permission denied');
|
||||
});
|
||||
|
||||
// Should handle file system errors gracefully
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-Interactive Mode', () => {
|
||||
it('bypasses prompts with yes:true', async () => {
|
||||
const result = await initializeProject({
|
||||
...baseOptions,
|
||||
git: true,
|
||||
aliases: true,
|
||||
dryRun: true
|
||||
});
|
||||
|
||||
expect(result).toEqual({ dryRun: true });
|
||||
});
|
||||
|
||||
it('completes without hanging', async () => {
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('handles all flag combinations without hanging', async () => {
|
||||
const flagCombinations = [
|
||||
{ git: true, aliases: true },
|
||||
{ git: true, aliases: false },
|
||||
{ git: false, aliases: true },
|
||||
{ git: false, aliases: false },
|
||||
{} // No flags (uses defaults)
|
||||
];
|
||||
|
||||
for (const flags of flagCombinations) {
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
...flags,
|
||||
dryRun: true // Use dry run for speed
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts complete project details', async () => {
|
||||
await expect(
|
||||
initializeProject({
|
||||
name: 'test-project',
|
||||
description: 'test description',
|
||||
version: '2.0.0',
|
||||
author: 'Test User',
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: true
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('works with skipInstall option', async () => {
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
skipInstall: true,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Function Integration', () => {
|
||||
it('calls utility functions without errors', async () => {
|
||||
await initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
});
|
||||
|
||||
// Verify that utility functions were called
|
||||
expect(mockUtils.isSilentMode).toHaveBeenCalled();
|
||||
expect(
|
||||
mockRuleTransformer.convertAllRulesToProfileRules
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles template operations gracefully', async () => {
|
||||
// Make file operations throw errors
|
||||
mockFs.writeFileSync.mockImplementation(() => {
|
||||
throw new Error('Write failed');
|
||||
});
|
||||
|
||||
// Should complete despite file operation failures
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false,
|
||||
aliases: false,
|
||||
dryRun: false
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('validates boolean flag conversion', async () => {
|
||||
// Test the boolean flag handling specifically
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: true, // Should convert to initGit: true
|
||||
aliases: false, // Should convert to addAliases: false
|
||||
dryRun: true
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
await expect(
|
||||
initializeProject({
|
||||
...baseOptions,
|
||||
git: false, // Should convert to initGit: false
|
||||
aliases: true, // Should convert to addAliases: true
|
||||
dryRun: true
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
439
tests/unit/manage-gitignore.test.js
Normal file
439
tests/unit/manage-gitignore.test.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Unit tests for manage-gitignore.js module
|
||||
* Tests the logic with Jest spies instead of mocked modules
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Import the module under test and its exports
|
||||
import manageGitignoreFile, {
|
||||
normalizeLine,
|
||||
isTaskLine,
|
||||
buildTaskFilesSection,
|
||||
TASK_FILES_COMMENT,
|
||||
TASK_JSON_PATTERN,
|
||||
TASK_DIR_PATTERN
|
||||
} from '../../src/utils/manage-gitignore.js';
|
||||
|
||||
describe('manage-gitignore.js Unit Tests', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manage-gitignore-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
describe('Constants', () => {
|
||||
test('should have correct constant values', () => {
|
||||
expect(TASK_FILES_COMMENT).toBe('# Task files');
|
||||
expect(TASK_JSON_PATTERN).toBe('tasks.json');
|
||||
expect(TASK_DIR_PATTERN).toBe('tasks/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeLine function', () => {
|
||||
test('should remove leading/trailing whitespace', () => {
|
||||
expect(normalizeLine(' test ')).toBe('test');
|
||||
});
|
||||
|
||||
test('should remove comment hash and trim', () => {
|
||||
expect(normalizeLine('# tasks.json')).toBe('tasks.json');
|
||||
expect(normalizeLine('#tasks/')).toBe('tasks/');
|
||||
});
|
||||
|
||||
test('should handle empty strings', () => {
|
||||
expect(normalizeLine('')).toBe('');
|
||||
expect(normalizeLine(' ')).toBe('');
|
||||
});
|
||||
|
||||
test('should handle lines without comments', () => {
|
||||
expect(normalizeLine('tasks.json')).toBe('tasks.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTaskLine function', () => {
|
||||
test('should identify task.json patterns', () => {
|
||||
expect(isTaskLine('tasks.json')).toBe(true);
|
||||
expect(isTaskLine('# tasks.json')).toBe(true);
|
||||
expect(isTaskLine(' # tasks.json ')).toBe(true);
|
||||
});
|
||||
|
||||
test('should identify tasks/ patterns', () => {
|
||||
expect(isTaskLine('tasks/')).toBe(true);
|
||||
expect(isTaskLine('# tasks/')).toBe(true);
|
||||
expect(isTaskLine(' # tasks/ ')).toBe(true);
|
||||
});
|
||||
|
||||
test('should reject non-task patterns', () => {
|
||||
expect(isTaskLine('node_modules/')).toBe(false);
|
||||
expect(isTaskLine('# Some comment')).toBe(false);
|
||||
expect(isTaskLine('')).toBe(false);
|
||||
expect(isTaskLine('tasks.txt')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTaskFilesSection function', () => {
|
||||
test('should build commented section when storeTasksInGit is true (tasks stored in git)', () => {
|
||||
const result = buildTaskFilesSection(true);
|
||||
expect(result).toEqual(['# Task files', '# tasks.json', '# tasks/ ']);
|
||||
});
|
||||
|
||||
test('should build uncommented section when storeTasksInGit is false (tasks ignored)', () => {
|
||||
const result = buildTaskFilesSection(false);
|
||||
expect(result).toEqual(['# Task files', 'tasks.json', 'tasks/ ']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('manageGitignoreFile function - Input Validation', () => {
|
||||
test('should throw error for invalid targetPath', () => {
|
||||
expect(() => {
|
||||
manageGitignoreFile('', 'content', false);
|
||||
}).toThrow('targetPath must be a non-empty string');
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile(null, 'content', false);
|
||||
}).toThrow('targetPath must be a non-empty string');
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('invalid.txt', 'content', false);
|
||||
}).toThrow('targetPath must end with .gitignore');
|
||||
});
|
||||
|
||||
test('should throw error for invalid content', () => {
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', '', false);
|
||||
}).toThrow('content must be a non-empty string');
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', null, false);
|
||||
}).toThrow('content must be a non-empty string');
|
||||
});
|
||||
|
||||
test('should throw error for invalid storeTasksInGit', () => {
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', 'content', 'not-boolean');
|
||||
}).toThrow('storeTasksInGit must be a boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('manageGitignoreFile function - File Operations with Spies', () => {
|
||||
let writeFileSyncSpy;
|
||||
let readFileSyncSpy;
|
||||
let existsSyncSpy;
|
||||
let mockLog;
|
||||
|
||||
beforeEach(() => {
|
||||
// Set up spies
|
||||
writeFileSyncSpy = jest
|
||||
.spyOn(fs, 'writeFileSync')
|
||||
.mockImplementation(() => {});
|
||||
readFileSyncSpy = jest
|
||||
.spyOn(fs, 'readFileSync')
|
||||
.mockImplementation(() => '');
|
||||
existsSyncSpy = jest
|
||||
.spyOn(fs, 'existsSync')
|
||||
.mockImplementation(() => false);
|
||||
mockLog = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original implementations
|
||||
writeFileSyncSpy.mockRestore();
|
||||
readFileSyncSpy.mockRestore();
|
||||
existsSyncSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('New File Creation', () => {
|
||||
const templateContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
test('should create new file with commented task lines when storeTasksInGit is true', () => {
|
||||
existsSyncSpy.mockReturnValue(false); // File doesn't exist
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
`# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/ `
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'success',
|
||||
'Created .gitignore with full template'
|
||||
);
|
||||
});
|
||||
|
||||
test('should create new file with uncommented task lines when storeTasksInGit is false', () => {
|
||||
existsSyncSpy.mockReturnValue(false); // File doesn't exist
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
`# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'success',
|
||||
'Created .gitignore with full template'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle write errors gracefully', () => {
|
||||
existsSyncSpy.mockReturnValue(false);
|
||||
const writeError = new Error('Permission denied');
|
||||
writeFileSyncSpy.mockImplementation(() => {
|
||||
throw writeError;
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
}).toThrow('Permission denied');
|
||||
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Failed to create .gitignore: Permission denied'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Merging', () => {
|
||||
const templateContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
test('should merge with existing file and add new content', () => {
|
||||
const existingContent = `# Old content
|
||||
old-file.txt
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/`;
|
||||
|
||||
existsSyncSpy.mockReturnValue(true); // File exists
|
||||
readFileSyncSpy.mockReturnValue(existingContent);
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
expect.stringContaining('# Old content')
|
||||
);
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
expect.stringContaining('# Logs')
|
||||
);
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
expect.stringContaining('# Dependencies')
|
||||
);
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||
'.gitignore',
|
||||
expect.stringContaining('# Task files')
|
||||
);
|
||||
});
|
||||
|
||||
test('should remove existing task section and replace with new preferences', () => {
|
||||
const existingContent = `# Existing
|
||||
existing.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/
|
||||
|
||||
# More content
|
||||
more.txt`;
|
||||
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
readFileSyncSpy.mockReturnValue(existingContent);
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
|
||||
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||
|
||||
// Should contain existing non-task content
|
||||
expect(writtenContent).toContain('# Existing');
|
||||
expect(writtenContent).toContain('existing.txt');
|
||||
expect(writtenContent).toContain('# More content');
|
||||
expect(writtenContent).toContain('more.txt');
|
||||
|
||||
// Should contain new template content
|
||||
expect(writtenContent).toContain('# Logs');
|
||||
expect(writtenContent).toContain('# Dependencies');
|
||||
|
||||
// Should have uncommented task lines (storeTasksInGit = false means ignore tasks)
|
||||
expect(writtenContent).toMatch(
|
||||
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle different task preferences correctly', () => {
|
||||
const existingContent = `# Existing
|
||||
existing.txt
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/`;
|
||||
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
readFileSyncSpy.mockReturnValue(existingContent);
|
||||
|
||||
// Test with storeTasksInGit = true (commented)
|
||||
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||
|
||||
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||
expect(writtenContent).toMatch(
|
||||
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||
);
|
||||
});
|
||||
|
||||
test('should not duplicate existing template content', () => {
|
||||
const existingContent = `# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/`;
|
||||
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
readFileSyncSpy.mockReturnValue(existingContent);
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
|
||||
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||
|
||||
// Should not duplicate the logs section
|
||||
const logsCount = (writtenContent.match(/# Logs/g) || []).length;
|
||||
expect(logsCount).toBe(1);
|
||||
|
||||
// Should not duplicate dependencies
|
||||
const depsCount = (writtenContent.match(/# Dependencies/g) || [])
|
||||
.length;
|
||||
expect(depsCount).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle read errors gracefully', () => {
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
const readError = new Error('File not readable');
|
||||
readFileSyncSpy.mockImplementation(() => {
|
||||
throw readError;
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
}).toThrow('File not readable');
|
||||
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Failed to merge content with .gitignore: File not readable'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle write errors during merge gracefully', () => {
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
readFileSyncSpy.mockReturnValue('existing content');
|
||||
|
||||
const writeError = new Error('Disk full');
|
||||
writeFileSyncSpy.mockImplementation(() => {
|
||||
throw writeError;
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
}).toThrow('Disk full');
|
||||
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Failed to merge content with .gitignore: Disk full'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should work without log function', () => {
|
||||
existsSyncSpy.mockReturnValue(false);
|
||||
const templateContent = `# Test
|
||||
test.txt
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/`;
|
||||
|
||||
expect(() => {
|
||||
manageGitignoreFile('.gitignore', templateContent, false);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle empty existing file', () => {
|
||||
existsSyncSpy.mockReturnValue(true);
|
||||
readFileSyncSpy.mockReturnValue('');
|
||||
|
||||
const templateContent = `# Task files
|
||||
tasks.json
|
||||
tasks/`;
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled();
|
||||
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||
expect(writtenContent).toContain('# Task files');
|
||||
});
|
||||
|
||||
test('should handle template with only task files', () => {
|
||||
existsSyncSpy.mockReturnValue(false);
|
||||
const templateContent = `# Task files
|
||||
tasks.json
|
||||
tasks/ `;
|
||||
|
||||
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||
|
||||
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||
expect(writtenContent).toBe(`# Task files
|
||||
# tasks.json
|
||||
# tasks/ `);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user