mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-07-04 23:50:50 +00:00
443 lines
12 KiB
JavaScript
443 lines
12 KiB
JavaScript
/**
|
|
* 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/
|
|
`);
|
|
});
|
|
});
|
|
});
|
|
});
|