feat: Enhance testing, CLI flag validation, and AI capabilities
This commit introduces several significant improvements:
- **Enhanced Unit Testing:** Vastly improved unit tests for the module, covering core functions, edge cases, and error handling. Simplified test functions and comprehensive mocking were implemented for better isolation and reliability. Added new section to tests.mdc detailing reliable testing techniques.
- **CLI Kebab-Case Flag Enforcement:** The CLI now enforces kebab-case for flags, providing helpful error messages when camelCase is used. This improves consistency and user experience.
- **AI Enhancements:**
- Enabled 128k token output for Claude 3.7 Sonnet by adding the header.
- Added a new task to to document this change and its testing strategy.
- Added unit tests to verify the Anthropic client configuration.
- Added and utility functions.
- **Improved Test Coverage:** Added tests for the new CLI flag validation logic.
This commit is contained in:
@@ -10,14 +10,17 @@ const mockLog = jest.fn();
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@anthropic-ai/sdk', () => {
|
||||
const mockCreate = jest.fn().mockResolvedValue({
|
||||
content: [{ text: 'AI response' }],
|
||||
});
|
||||
const mockAnthropicInstance = {
|
||||
messages: {
|
||||
create: mockCreate
|
||||
}
|
||||
};
|
||||
const mockAnthropicConstructor = jest.fn().mockImplementation(() => mockAnthropicInstance);
|
||||
return {
|
||||
Anthropic: jest.fn().mockImplementation(() => ({
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
content: [{ text: 'AI response' }],
|
||||
}),
|
||||
},
|
||||
})),
|
||||
Anthropic: mockAnthropicConstructor
|
||||
};
|
||||
});
|
||||
|
||||
@@ -68,6 +71,9 @@ global.anthropic = {
|
||||
// Mock process.env
|
||||
const originalEnv = process.env;
|
||||
|
||||
// Import Anthropic for testing constructor arguments
|
||||
import { Anthropic } from '@anthropic-ai/sdk';
|
||||
|
||||
describe('AI Services Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -370,4 +376,17 @@ These subtasks will help you implement the parent task efficiently.`;
|
||||
expect(result).toContain('Something unexpected happened');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Anthropic client configuration', () => {
|
||||
test('should include output-128k beta header in client configuration', async () => {
|
||||
// Read the file content to verify the change is present
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const filePath = path.resolve('./scripts/modules/ai-services.js');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check if the beta header is in the file
|
||||
expect(fileContent).toContain("'anthropic-beta': 'output-128k-2025-02-19'");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,20 @@ jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
CONFIG: {
|
||||
projectVersion: '1.5.0'
|
||||
},
|
||||
log: jest.fn()
|
||||
log: jest.fn(),
|
||||
detectCamelCaseFlags: jest.fn().mockImplementation((args) => {
|
||||
const camelCaseRegex = /--([a-z]+[A-Z][a-zA-Z]+)/;
|
||||
const flags = [];
|
||||
for (const arg of args) {
|
||||
const match = camelCaseRegex.exec(arg);
|
||||
if (match) {
|
||||
const original = match[1];
|
||||
const kebabCase = original.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
flags.push({ original, kebabCase });
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
})
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
@@ -26,6 +39,7 @@ import { setupCLI } from '../../scripts/modules/commands.js';
|
||||
import { program } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { detectCamelCaseFlags } from '../../scripts/modules/utils.js';
|
||||
|
||||
describe('Commands Module', () => {
|
||||
// Set up spies on the mocked modules
|
||||
@@ -116,4 +130,103 @@ describe('Commands Module', () => {
|
||||
expect(result).toBe('1.5.0'); // Updated to match the actual CONFIG.projectVersion
|
||||
});
|
||||
});
|
||||
|
||||
// Add a new describe block for kebab-case validation tests
|
||||
describe('Kebab Case Validation', () => {
|
||||
// Save the original process.argv
|
||||
const originalArgv = process.argv;
|
||||
|
||||
// Reset process.argv after each test
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
|
||||
test('should detect camelCase flags correctly', () => {
|
||||
// Set up process.argv with a camelCase flag
|
||||
process.argv = ['node', 'task-master', 'add-task', '--promptText=test'];
|
||||
|
||||
// Mock process.exit to prevent the test from actually exiting
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
// Mock console.error to capture the error message
|
||||
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Create an action function similar to what's in task-master.js
|
||||
const action = () => {
|
||||
const camelCaseFlags = detectCamelCaseFlags(process.argv);
|
||||
if (camelCaseFlags.length > 0) {
|
||||
console.error('\nError: Please use kebab-case for CLI flags:');
|
||||
camelCaseFlags.forEach(flag => {
|
||||
console.error(` Instead of: --${flag.original}`);
|
||||
console.error(` Use: --${flag.kebabCase}`);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Call the action function
|
||||
action();
|
||||
|
||||
// Verify that process.exit was called with 1
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
|
||||
// Verify console.error messages
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Please use kebab-case for CLI flags')
|
||||
);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Instead of: --promptText')
|
||||
);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Use: --prompt-text')
|
||||
);
|
||||
|
||||
// Clean up
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('should accept kebab-case flags correctly', () => {
|
||||
// Import the function we're testing
|
||||
jest.resetModules();
|
||||
|
||||
// Mock process.exit to prevent the test from actually exiting
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
// Mock console.error to verify it's not called with kebab-case error
|
||||
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
// Set up process.argv with a valid kebab-case flag
|
||||
process.argv = ['node', 'task-master', 'add-task', '--prompt-text=test'];
|
||||
|
||||
// Mock the runDevScript function to prevent actual execution
|
||||
jest.doMock('../../bin/task-master.js', () => {
|
||||
const actual = jest.requireActual('../../bin/task-master.js');
|
||||
return {
|
||||
...actual,
|
||||
runDevScript: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Run the module which should not error for kebab-case
|
||||
try {
|
||||
require('../../bin/task-master.js');
|
||||
} catch (e) {
|
||||
// Ignore any errors from the module
|
||||
}
|
||||
|
||||
// Verify that process.exit was not called with error code 1
|
||||
// Note: It might be called for other reasons so we just check it's not called with 1
|
||||
expect(mockExit).not.toHaveBeenCalledWith(1);
|
||||
|
||||
// Verify that console.error was not called with kebab-case error message
|
||||
expect(mockConsoleError).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Please use kebab-case for CLI flags')
|
||||
);
|
||||
|
||||
// Clean up
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
44
tests/unit/kebab-case-validation.test.js
Normal file
44
tests/unit/kebab-case-validation.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Tests for kebab-case validation functionality
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Create a mock implementation of the helper function to avoid loading the entire module
|
||||
jest.mock('../../bin/task-master.js', () => ({
|
||||
detectCamelCaseFlags: jest.requireActual('../../bin/task-master.js').detectCamelCaseFlags
|
||||
}));
|
||||
|
||||
// Import the module after mocking - use dynamic import for ES modules
|
||||
import { detectCamelCaseFlags } from '../../scripts/modules/utils.js';
|
||||
|
||||
describe('Kebab Case Validation', () => {
|
||||
test('should properly detect camelCase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--promptText=test', '--userID=123'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userID',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
|
||||
test('should not flag kebab-case or lowercase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--prompt=test', '--user-id=123'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not flag single-word lowercase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--prompt="test"', '--file=file.json'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
formatTaskId,
|
||||
findCycles,
|
||||
CONFIG,
|
||||
LOG_LEVELS
|
||||
LOG_LEVELS,
|
||||
findTaskById,
|
||||
detectCamelCaseFlags,
|
||||
toKebabCase
|
||||
} from '../../scripts/modules/utils.js';
|
||||
|
||||
// Mock chalk functions
|
||||
@@ -477,4 +480,42 @@ describe('Utils Module', () => {
|
||||
expect(cycles).toContain('B');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLI Flag Format Validation', () => {
|
||||
test('toKebabCase should convert camelCase to kebab-case', () => {
|
||||
expect(toKebabCase('promptText')).toBe('prompt-text');
|
||||
expect(toKebabCase('userID')).toBe('user-id');
|
||||
expect(toKebabCase('numTasks')).toBe('num-tasks');
|
||||
expect(toKebabCase('alreadyKebabCase')).toBe('already-kebab-case');
|
||||
});
|
||||
|
||||
test('detectCamelCaseFlags should identify camelCase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--promptText=test', '--userID=123'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userID',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
|
||||
test('detectCamelCaseFlags should not flag kebab-case flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--prompt-text=test', '--user-id=123'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('detectCamelCaseFlags should not flag simple lowercase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--prompt=test', '--file=tasks.json'];
|
||||
const flags = detectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user