mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
/**
|
|
* @fileoverview Integration tests for CursorProfile
|
|
*
|
|
* These tests verify actual filesystem operations using addSlashCommands
|
|
* and removeSlashCommands methods. Tests ensure that:
|
|
* - Directory creation works correctly (files go to .cursor/commands/tm/)
|
|
* - Files are written with correct content (no transformation)
|
|
* - Commands can be added and removed
|
|
* - User files are preserved during cleanup
|
|
* - Empty directories are cleaned up
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { CursorProfile } from '../../src/slash-commands/profiles/cursor-profile.js';
|
|
import {
|
|
staticCommand,
|
|
dynamicCommand
|
|
} from '../../src/slash-commands/factories.js';
|
|
|
|
describe('CursorProfile - Integration Tests', () => {
|
|
let tempDir: string;
|
|
let cursorProfile: CursorProfile;
|
|
|
|
// Test commands created inline
|
|
const testStaticCommand = staticCommand({
|
|
name: 'help',
|
|
description: 'Show available commands',
|
|
content: '# Help\n\nList of available Task Master commands.'
|
|
});
|
|
|
|
const testDynamicCommand = dynamicCommand(
|
|
'goham',
|
|
'Start Working with Hamster Brief',
|
|
'[brief-url]',
|
|
'# Start Working\n\nBrief URL: $ARGUMENTS\n\nThis command helps you start working on a Hamster brief.'
|
|
);
|
|
|
|
const testCommands = [testStaticCommand, testDynamicCommand];
|
|
|
|
beforeEach(() => {
|
|
// Create temporary directory for each test
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-profile-test-'));
|
|
cursorProfile = new CursorProfile();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up temporary directory
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('addSlashCommands', () => {
|
|
it('should create the .cursor/commands/tm directory (nested structure)', () => {
|
|
// Verify directory doesn't exist before
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
expect(fs.existsSync(tmDir)).toBe(false);
|
|
|
|
// Add commands
|
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
|
|
// Verify tm directory exists after (nested structure)
|
|
expect(fs.existsSync(tmDir)).toBe(true);
|
|
expect(fs.statSync(tmDir).isDirectory()).toBe(true);
|
|
});
|
|
|
|
it('should write files with content unchanged (no transformation)', () => {
|
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
|
|
// Cursor supports nested commands, files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Verify static command (help.md)
|
|
const helpPath = path.join(tmDir, 'help.md');
|
|
expect(fs.existsSync(helpPath)).toBe(true);
|
|
const helpContent = fs.readFileSync(helpPath, 'utf-8');
|
|
expect(helpContent).toBe(
|
|
'# Help\n\nList of available Task Master commands.'
|
|
);
|
|
|
|
// Verify dynamic command (goham.md)
|
|
const gohamPath = path.join(tmDir, 'goham.md');
|
|
expect(fs.existsSync(gohamPath)).toBe(true);
|
|
const gohamContent = fs.readFileSync(gohamPath, 'utf-8');
|
|
expect(gohamContent).toBe(
|
|
'# Start Working\n\nBrief URL: $ARGUMENTS\n\nThis command helps you start working on a Hamster brief.'
|
|
);
|
|
|
|
// Verify $ARGUMENTS placeholder is NOT transformed
|
|
expect(gohamContent).toContain('$ARGUMENTS');
|
|
});
|
|
|
|
it('should return success result with correct count', () => {
|
|
const result = cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.count).toBe(2);
|
|
// Path includes tm/ subdirectory for nested structure
|
|
expect(result.directory).toBe(
|
|
path.join(tempDir, '.cursor', 'commands', 'tm')
|
|
);
|
|
expect(result.files).toEqual(['help.md', 'goham.md']);
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
it('should overwrite existing files on re-run', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// First run
|
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
const originalContent = fs.readFileSync(
|
|
path.join(tmDir, 'help.md'),
|
|
'utf-8'
|
|
);
|
|
expect(originalContent).toBe(
|
|
'# Help\n\nList of available Task Master commands.'
|
|
);
|
|
|
|
// Modify the content of test command
|
|
const modifiedCommand = staticCommand({
|
|
name: 'help',
|
|
description: 'Show available commands',
|
|
content: '# Help - Updated\n\nThis is updated content.'
|
|
});
|
|
|
|
// Second run with modified command
|
|
const result = cursorProfile.addSlashCommands(tempDir, [modifiedCommand]);
|
|
|
|
// Verify file was overwritten
|
|
const updatedContent = fs.readFileSync(
|
|
path.join(tmDir, 'help.md'),
|
|
'utf-8'
|
|
);
|
|
expect(updatedContent).toBe(
|
|
'# Help - Updated\n\nThis is updated content.'
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.count).toBe(1);
|
|
});
|
|
|
|
it('should handle commands with special characters in content', () => {
|
|
const specialCommand = staticCommand({
|
|
name: 'special',
|
|
description: 'Command with special characters',
|
|
content:
|
|
'# Special\n\n```bash\necho "Hello $USER"\n```\n\n- Item 1\n- Item 2\n\n**Bold** and *italic*'
|
|
});
|
|
|
|
cursorProfile.addSlashCommands(tempDir, [specialCommand]);
|
|
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
const specialPath = path.join(tmDir, 'special.md');
|
|
const content = fs.readFileSync(specialPath, 'utf-8');
|
|
|
|
// Verify content is preserved exactly
|
|
expect(content).toBe(
|
|
'# Special\n\n```bash\necho "Hello $USER"\n```\n\n- Item 1\n- Item 2\n\n**Bold** and *italic*'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('removeSlashCommands', () => {
|
|
beforeEach(() => {
|
|
// Add commands before testing removal
|
|
cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
});
|
|
|
|
it('should remove only TaskMaster commands (preserve user files)', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Create a user's custom command file in the tm directory
|
|
const userCommandPath = path.join(tmDir, 'custom-user-command.md');
|
|
fs.writeFileSync(
|
|
userCommandPath,
|
|
'# Custom User Command\n\nThis is a user-created command.'
|
|
);
|
|
|
|
// Verify all files exist before removal
|
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
|
expect(fs.existsSync(path.join(tmDir, 'goham.md'))).toBe(true);
|
|
expect(fs.existsSync(userCommandPath)).toBe(true);
|
|
|
|
// Remove TaskMaster commands
|
|
const result = cursorProfile.removeSlashCommands(tempDir, testCommands);
|
|
|
|
// Verify TaskMaster commands removed
|
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(false);
|
|
expect(fs.existsSync(path.join(tmDir, 'goham.md'))).toBe(false);
|
|
|
|
// Verify user's custom file preserved
|
|
expect(fs.existsSync(userCommandPath)).toBe(true);
|
|
expect(fs.readFileSync(userCommandPath, 'utf-8')).toBe(
|
|
'# Custom User Command\n\nThis is a user-created command.'
|
|
);
|
|
|
|
// Verify result
|
|
expect(result.success).toBe(true);
|
|
expect(result.count).toBe(2);
|
|
// File order is not guaranteed, so check both files are present
|
|
expect(result.files).toHaveLength(2);
|
|
expect(result.files).toContain('help.md');
|
|
expect(result.files).toContain('goham.md');
|
|
});
|
|
|
|
it('should remove empty tm directory after cleanup', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Verify directory exists with files
|
|
expect(fs.existsSync(tmDir)).toBe(true);
|
|
expect(fs.readdirSync(tmDir).length).toBe(2);
|
|
|
|
// Remove all commands (should cleanup empty directory)
|
|
const result = cursorProfile.removeSlashCommands(
|
|
tempDir,
|
|
testCommands,
|
|
true
|
|
);
|
|
|
|
// Verify tm directory removed
|
|
expect(fs.existsSync(tmDir)).toBe(false);
|
|
expect(result.success).toBe(true);
|
|
expect(result.count).toBe(2);
|
|
});
|
|
|
|
it('should not remove directory if removeEmptyDir is false', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Remove commands but keep directory
|
|
const result = cursorProfile.removeSlashCommands(
|
|
tempDir,
|
|
testCommands,
|
|
false
|
|
);
|
|
|
|
// Verify directory still exists (but empty)
|
|
expect(fs.existsSync(tmDir)).toBe(true);
|
|
expect(fs.statSync(tmDir).isDirectory()).toBe(true);
|
|
expect(fs.readdirSync(tmDir).length).toBe(0);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should handle removal when directory does not exist', () => {
|
|
const nonExistentDir = path.join(tempDir, 'nonexistent');
|
|
|
|
// Remove commands from non-existent directory
|
|
const result = cursorProfile.removeSlashCommands(
|
|
nonExistentDir,
|
|
testCommands
|
|
);
|
|
|
|
// Should succeed with 0 count
|
|
expect(result.success).toBe(true);
|
|
expect(result.count).toBe(0);
|
|
expect(result.files).toEqual([]);
|
|
});
|
|
|
|
it('should be case-insensitive when matching command names', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Check if filesystem is case-sensitive (Linux) or case-insensitive (macOS/Windows)
|
|
const testFile = path.join(tmDir, 'TEST-CASE.md');
|
|
fs.writeFileSync(testFile, 'test');
|
|
const isCaseSensitive = !fs.existsSync(path.join(tmDir, 'test-case.md'));
|
|
fs.rmSync(testFile);
|
|
|
|
// Create command with different casing from test commands
|
|
const upperCaseFile = path.join(tmDir, 'HELP.md');
|
|
fs.writeFileSync(upperCaseFile, '# Upper case help');
|
|
|
|
// Remove using lowercase name
|
|
const result = cursorProfile.removeSlashCommands(tempDir, testCommands);
|
|
|
|
// help.md should always be removed
|
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(false);
|
|
|
|
if (isCaseSensitive) {
|
|
// On case-sensitive filesystems, HELP.md is treated as different file
|
|
expect(fs.existsSync(upperCaseFile)).toBe(true);
|
|
expect(result.count).toBe(2); // help.md, goham.md
|
|
// Clean up
|
|
fs.rmSync(upperCaseFile);
|
|
} else {
|
|
// On case-insensitive filesystems (macOS/Windows), both should be removed
|
|
// because the filesystem treats help.md and HELP.md as the same file
|
|
expect(fs.existsSync(upperCaseFile)).toBe(false);
|
|
expect(result.count).toBe(2); // help.md (which is the same as HELP.md), goham.md
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Profile configuration', () => {
|
|
it('should have correct profile properties', () => {
|
|
expect(cursorProfile.name).toBe('cursor');
|
|
expect(cursorProfile.displayName).toBe('Cursor');
|
|
expect(cursorProfile.commandsDir).toBe('.cursor/commands');
|
|
expect(cursorProfile.extension).toBe('.md');
|
|
expect(cursorProfile.supportsCommands).toBe(true);
|
|
expect(cursorProfile.supportsNestedCommands).toBe(true);
|
|
});
|
|
|
|
it('should generate correct filenames (no prefix for nested structure)', () => {
|
|
// Cursor supports nested commands, so no tm- prefix
|
|
expect(cursorProfile.getFilename('help')).toBe('help.md');
|
|
expect(cursorProfile.getFilename('goham')).toBe('goham.md');
|
|
});
|
|
|
|
it('should generate correct commands path with tm subdirectory', () => {
|
|
// Path includes tm/ subdirectory for nested structure
|
|
const expectedPath = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
expect(cursorProfile.getCommandsPath(tempDir)).toBe(expectedPath);
|
|
});
|
|
});
|
|
|
|
describe('Round-trip operations', () => {
|
|
it('should successfully add, remove, and re-add commands', () => {
|
|
// Files go to .cursor/commands/tm/
|
|
const tmDir = path.join(tempDir, '.cursor', 'commands', 'tm');
|
|
|
|
// Add commands
|
|
const addResult1 = cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
expect(addResult1.success).toBe(true);
|
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
|
|
|
// Remove commands
|
|
const removeResult = cursorProfile.removeSlashCommands(
|
|
tempDir,
|
|
testCommands
|
|
);
|
|
expect(removeResult.success).toBe(true);
|
|
expect(fs.existsSync(tmDir)).toBe(false);
|
|
|
|
// Re-add commands
|
|
const addResult2 = cursorProfile.addSlashCommands(tempDir, testCommands);
|
|
expect(addResult2.success).toBe(true);
|
|
expect(fs.existsSync(path.join(tmDir, 'help.md'))).toBe(true);
|
|
|
|
// Verify content is still correct
|
|
const content = fs.readFileSync(path.join(tmDir, 'help.md'), 'utf-8');
|
|
expect(content).toBe('# Help\n\nList of available Task Master commands.');
|
|
});
|
|
});
|
|
});
|