feat: create tm-core and apps/cli (#1093)
- add typescript - add npm workspaces
This commit is contained in:
422
packages/tm-core/tests/integration/list-tasks.test.ts
Normal file
422
packages/tm-core/tests/integration/list-tasks.test.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* @fileoverview End-to-end integration test for listTasks functionality
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
type Task,
|
||||
type TaskMasterCore,
|
||||
type TaskStatus,
|
||||
createTaskMasterCore
|
||||
} from '../../src/index';
|
||||
|
||||
describe('TaskMasterCore - listTasks E2E', () => {
|
||||
let tmpDir: string;
|
||||
let tmCore: TaskMasterCore;
|
||||
|
||||
// Sample tasks data
|
||||
const sampleTasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Setup project',
|
||||
description: 'Initialize the project structure',
|
||||
status: 'done',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Create all necessary directories and config files',
|
||||
testStrategy: 'Manual verification',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
parentId: '1',
|
||||
title: 'Create directories',
|
||||
description: 'Create project directories',
|
||||
status: 'done',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Create src, tests, docs directories',
|
||||
testStrategy: 'Check directories exist'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
parentId: '1',
|
||||
title: 'Initialize package.json',
|
||||
description: 'Create package.json file',
|
||||
status: 'done',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Run npm init',
|
||||
testStrategy: 'Verify package.json exists'
|
||||
}
|
||||
],
|
||||
tags: ['setup', 'infrastructure']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Implement core features',
|
||||
description: 'Build the main functionality',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: ['1'],
|
||||
details: 'Implement all core business logic',
|
||||
testStrategy: 'Unit tests for all features',
|
||||
subtasks: [],
|
||||
tags: ['feature', 'core'],
|
||||
assignee: 'developer1'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Write documentation',
|
||||
description: 'Create user and developer docs',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: ['2'],
|
||||
details: 'Write comprehensive documentation',
|
||||
testStrategy: 'Review by team',
|
||||
subtasks: [],
|
||||
tags: ['documentation'],
|
||||
complexity: 'simple'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Performance optimization',
|
||||
description: 'Optimize for speed and efficiency',
|
||||
status: 'blocked',
|
||||
priority: 'low',
|
||||
dependencies: ['2'],
|
||||
details: 'Profile and optimize bottlenecks',
|
||||
testStrategy: 'Performance benchmarks',
|
||||
subtasks: [],
|
||||
assignee: 'developer2',
|
||||
complexity: 'complex'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Security audit',
|
||||
description: 'Review security vulnerabilities',
|
||||
status: 'deferred',
|
||||
priority: 'critical',
|
||||
dependencies: [],
|
||||
details: 'Complete security assessment',
|
||||
testStrategy: 'Security scanning tools',
|
||||
subtasks: [],
|
||||
tags: ['security', 'audit']
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temp directory for testing
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-core-test-'));
|
||||
|
||||
// Create .taskmaster/tasks directory
|
||||
const tasksDir = path.join(tmpDir, '.taskmaster', 'tasks');
|
||||
await fs.mkdir(tasksDir, { recursive: true });
|
||||
|
||||
// Write sample tasks.json
|
||||
const tasksFile = path.join(tasksDir, 'tasks.json');
|
||||
const tasksData = {
|
||||
tasks: sampleTasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: sampleTasks.length,
|
||||
completedCount: 1
|
||||
}
|
||||
};
|
||||
await fs.writeFile(tasksFile, JSON.stringify(tasksData, null, 2));
|
||||
|
||||
// Create TaskMasterCore instance
|
||||
tmCore = createTaskMasterCore(tmpDir);
|
||||
await tmCore.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Cleanup
|
||||
if (tmCore) {
|
||||
await tmCore.close();
|
||||
}
|
||||
|
||||
// Remove temp directory
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Basic listing', () => {
|
||||
it('should list all tasks', async () => {
|
||||
const result = await tmCore.listTasks();
|
||||
|
||||
expect(result.tasks).toHaveLength(5);
|
||||
expect(result.total).toBe(5);
|
||||
expect(result.filtered).toBe(5);
|
||||
expect(result.tag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include subtasks by default', async () => {
|
||||
const result = await tmCore.listTasks();
|
||||
const setupTask = result.tasks.find((t) => t.id === '1');
|
||||
|
||||
expect(setupTask?.subtasks).toHaveLength(2);
|
||||
expect(setupTask?.subtasks[0].title).toBe('Create directories');
|
||||
});
|
||||
|
||||
it('should exclude subtasks when requested', async () => {
|
||||
const result = await tmCore.listTasks({ includeSubtasks: false });
|
||||
const setupTask = result.tasks.find((t) => t.id === '1');
|
||||
|
||||
expect(setupTask?.subtasks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering', () => {
|
||||
it('should filter by status', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { status: 'done' }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('should filter by multiple statuses', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { status: ['done', 'in-progress'] }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(2);
|
||||
const ids = result.tasks.map((t) => t.id);
|
||||
expect(ids).toContain('1');
|
||||
expect(ids).toContain('2');
|
||||
});
|
||||
|
||||
it('should filter by priority', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { priority: 'high' }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(2);
|
||||
});
|
||||
|
||||
it('should filter by tags', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { tags: ['setup'] }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('should filter by assignee', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { assignee: 'developer1' }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('2');
|
||||
});
|
||||
|
||||
it('should filter by complexity', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { complexity: 'complex' }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('4');
|
||||
});
|
||||
|
||||
it('should filter by search term', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: { search: 'documentation' }
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('should filter by hasSubtasks', async () => {
|
||||
const withSubtasks = await tmCore.listTasks({
|
||||
filter: { hasSubtasks: true }
|
||||
});
|
||||
|
||||
expect(withSubtasks.filtered).toBe(1);
|
||||
expect(withSubtasks.tasks[0].id).toBe('1');
|
||||
|
||||
const withoutSubtasks = await tmCore.listTasks({
|
||||
filter: { hasSubtasks: false }
|
||||
});
|
||||
|
||||
expect(withoutSubtasks.filtered).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle combined filters', async () => {
|
||||
const result = await tmCore.listTasks({
|
||||
filter: {
|
||||
priority: ['high', 'critical'],
|
||||
status: ['pending', 'deferred']
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.filtered).toBe(1);
|
||||
expect(result.tasks[0].id).toBe('5'); // Critical priority, deferred status
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper methods', () => {
|
||||
it('should get task by ID', async () => {
|
||||
const task = await tmCore.getTask('2');
|
||||
|
||||
expect(task).not.toBeNull();
|
||||
expect(task?.title).toBe('Implement core features');
|
||||
});
|
||||
|
||||
it('should return null for non-existent task', async () => {
|
||||
const task = await tmCore.getTask('999');
|
||||
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
|
||||
it('should get tasks by status', async () => {
|
||||
const pendingTasks = await tmCore.getTasksByStatus('pending');
|
||||
|
||||
expect(pendingTasks).toHaveLength(1);
|
||||
expect(pendingTasks[0].id).toBe('3');
|
||||
|
||||
const multipleTasks = await tmCore.getTasksByStatus(['done', 'blocked']);
|
||||
|
||||
expect(multipleTasks).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should get task statistics', async () => {
|
||||
const stats = await tmCore.getTaskStats();
|
||||
|
||||
expect(stats.total).toBe(5);
|
||||
expect(stats.byStatus.done).toBe(1);
|
||||
expect(stats.byStatus['in-progress']).toBe(1);
|
||||
expect(stats.byStatus.pending).toBe(1);
|
||||
expect(stats.byStatus.blocked).toBe(1);
|
||||
expect(stats.byStatus.deferred).toBe(1);
|
||||
expect(stats.byStatus.cancelled).toBe(0);
|
||||
expect(stats.byStatus.review).toBe(0);
|
||||
expect(stats.withSubtasks).toBe(1);
|
||||
expect(stats.blocked).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle missing tasks file gracefully', async () => {
|
||||
// Create new instance with empty directory
|
||||
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-empty-'));
|
||||
const emptyCore = createTaskMasterCore(emptyDir);
|
||||
|
||||
try {
|
||||
const result = await emptyCore.listTasks();
|
||||
|
||||
expect(result.tasks).toHaveLength(0);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.filtered).toBe(0);
|
||||
} finally {
|
||||
await emptyCore.close();
|
||||
await fs.rm(emptyDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate task entities', async () => {
|
||||
// Write invalid task data
|
||||
const invalidDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'tm-invalid-')
|
||||
);
|
||||
const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
|
||||
await fs.mkdir(tasksDir, { recursive: true });
|
||||
|
||||
const invalidData = {
|
||||
tasks: [
|
||||
{
|
||||
id: '', // Invalid: empty ID
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
status: 'done',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Test',
|
||||
testStrategy: 'Test',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 1,
|
||||
completedCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tasksDir, 'tasks.json'),
|
||||
JSON.stringify(invalidData)
|
||||
);
|
||||
|
||||
const invalidCore = createTaskMasterCore(invalidDir);
|
||||
|
||||
try {
|
||||
await expect(invalidCore.listTasks()).rejects.toThrow();
|
||||
} finally {
|
||||
await invalidCore.close();
|
||||
await fs.rm(invalidDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tags support', () => {
|
||||
beforeEach(async () => {
|
||||
// Create tasks for a different tag
|
||||
const taggedTasks = [
|
||||
{
|
||||
id: 'tag-1',
|
||||
title: 'Tagged task',
|
||||
description: 'Task with tag',
|
||||
status: 'pending' as TaskStatus,
|
||||
priority: 'medium' as const,
|
||||
dependencies: [],
|
||||
details: 'Tagged task details',
|
||||
testStrategy: 'Test',
|
||||
subtasks: []
|
||||
}
|
||||
];
|
||||
|
||||
const tagFile = path.join(
|
||||
tmpDir,
|
||||
'.taskmaster',
|
||||
'tasks',
|
||||
'feature-branch.json'
|
||||
);
|
||||
await fs.writeFile(
|
||||
tagFile,
|
||||
JSON.stringify({
|
||||
tasks: taggedTasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 1,
|
||||
completedCount: 0
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should list tasks for specific tag', async () => {
|
||||
const result = await tmCore.listTasks({ tag: 'feature-branch' });
|
||||
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0].id).toBe('tag-1');
|
||||
expect(result.tag).toBe('feature-branch');
|
||||
});
|
||||
|
||||
it('should list default tasks when no tag specified', async () => {
|
||||
const result = await tmCore.listTasks();
|
||||
|
||||
expect(result.tasks).toHaveLength(5);
|
||||
expect(result.tasks[0].id).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
210
packages/tm-core/tests/mocks/mock-provider.ts
Normal file
210
packages/tm-core/tests/mocks/mock-provider.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @fileoverview Mock provider for testing BaseProvider functionality
|
||||
*/
|
||||
|
||||
import type {
|
||||
AIModel,
|
||||
AIOptions,
|
||||
AIResponse,
|
||||
ProviderInfo,
|
||||
ProviderUsageStats
|
||||
} from '../../src/interfaces/ai-provider.interface';
|
||||
import {
|
||||
BaseProvider,
|
||||
type BaseProviderConfig,
|
||||
type CompletionResult
|
||||
} from '../../src/providers/ai/base-provider';
|
||||
|
||||
/**
|
||||
* Configuration for MockProvider behavior
|
||||
*/
|
||||
export interface MockProviderOptions extends BaseProviderConfig {
|
||||
shouldFail?: boolean;
|
||||
failAfterAttempts?: number;
|
||||
simulateRateLimit?: boolean;
|
||||
simulateTimeout?: boolean;
|
||||
responseDelay?: number;
|
||||
tokenMultiplier?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock provider for testing BaseProvider functionality
|
||||
*/
|
||||
export class MockProvider extends BaseProvider {
|
||||
private attemptCount = 0;
|
||||
private readonly options: MockProviderOptions;
|
||||
|
||||
constructor(options: MockProviderOptions) {
|
||||
super(options);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate completion generation with configurable behavior
|
||||
*/
|
||||
protected async generateCompletionInternal(
|
||||
prompt: string,
|
||||
_options?: AIOptions
|
||||
): Promise<CompletionResult> {
|
||||
this.attemptCount++;
|
||||
|
||||
// Simulate delay if configured
|
||||
if (this.options.responseDelay) {
|
||||
await this.sleep(this.options.responseDelay);
|
||||
}
|
||||
|
||||
// Simulate failures based on configuration
|
||||
if (this.options.shouldFail) {
|
||||
throw new Error('Mock provider error');
|
||||
}
|
||||
|
||||
if (
|
||||
this.options.failAfterAttempts &&
|
||||
this.attemptCount <= this.options.failAfterAttempts
|
||||
) {
|
||||
if (this.options.simulateRateLimit) {
|
||||
throw new Error('Rate limit exceeded - too many requests (429)');
|
||||
}
|
||||
if (this.options.simulateTimeout) {
|
||||
throw new Error('Request timeout - ECONNRESET');
|
||||
}
|
||||
throw new Error('Temporary failure');
|
||||
}
|
||||
|
||||
// Return successful mock response
|
||||
return {
|
||||
content: `Mock response to: ${prompt}`,
|
||||
inputTokens: this.calculateTokens(prompt),
|
||||
outputTokens: this.calculateTokens(`Mock response to: ${prompt}`),
|
||||
finishReason: 'complete',
|
||||
model: this.model
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple token calculation for testing
|
||||
*/
|
||||
calculateTokens(text: string, _model?: string): number {
|
||||
const multiplier = this.options.tokenMultiplier || 1;
|
||||
// Rough approximation: 1 token per 4 characters
|
||||
return Math.ceil((text.length / 4) * multiplier);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'mock';
|
||||
}
|
||||
|
||||
getDefaultModel(): string {
|
||||
return 'mock-model-v1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of attempts made
|
||||
*/
|
||||
getAttemptCount(): number {
|
||||
return this.attemptCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset attempt counter
|
||||
*/
|
||||
resetAttempts(): void {
|
||||
this.attemptCount = 0;
|
||||
}
|
||||
|
||||
// Implement remaining abstract methods
|
||||
async generateStreamingCompletion(
|
||||
prompt: string,
|
||||
_options?: AIOptions
|
||||
): AsyncIterator<Partial<AIResponse>> {
|
||||
// Simple mock implementation
|
||||
const response: Partial<AIResponse> = {
|
||||
content: `Mock streaming response to: ${prompt}`,
|
||||
provider: this.getName(),
|
||||
model: this.model
|
||||
};
|
||||
|
||||
return {
|
||||
async next() {
|
||||
return { value: response, done: true };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return !this.options.shouldFail;
|
||||
}
|
||||
|
||||
getProviderInfo(): ProviderInfo {
|
||||
return {
|
||||
name: 'mock',
|
||||
displayName: 'Mock Provider',
|
||||
description: 'Mock provider for testing',
|
||||
models: this.getAvailableModels(),
|
||||
defaultModel: this.getDefaultModel(),
|
||||
requiresApiKey: true,
|
||||
features: {
|
||||
streaming: true,
|
||||
functions: false,
|
||||
vision: false,
|
||||
embeddings: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getAvailableModels(): AIModel[] {
|
||||
return [
|
||||
{
|
||||
id: 'mock-model-v1',
|
||||
name: 'Mock Model v1',
|
||||
description: 'First mock model',
|
||||
contextLength: 4096,
|
||||
inputCostPer1K: 0.001,
|
||||
outputCostPer1K: 0.002,
|
||||
supportsStreaming: true
|
||||
},
|
||||
{
|
||||
id: 'mock-model-v2',
|
||||
name: 'Mock Model v2',
|
||||
description: 'Second mock model',
|
||||
contextLength: 8192,
|
||||
inputCostPer1K: 0.002,
|
||||
outputCostPer1K: 0.004,
|
||||
supportsStreaming: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async validateCredentials(): Promise<boolean> {
|
||||
return this.apiKey === 'valid-key';
|
||||
}
|
||||
|
||||
async getUsageStats(): Promise<ProviderUsageStats | null> {
|
||||
return {
|
||||
totalRequests: this.attemptCount,
|
||||
totalTokens: 1000,
|
||||
totalCost: 0.01,
|
||||
requestsToday: this.attemptCount,
|
||||
tokensToday: 1000,
|
||||
costToday: 0.01,
|
||||
averageResponseTime: 100,
|
||||
successRate: 0.9,
|
||||
lastRequestAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// No-op for mock
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
// No-op for mock
|
||||
}
|
||||
|
||||
// Override retry configuration for testing
|
||||
protected getMaxRetries(): number {
|
||||
return this.options.failAfterAttempts
|
||||
? this.options.failAfterAttempts + 1
|
||||
: 3;
|
||||
}
|
||||
}
|
||||
21
packages/tm-core/tests/setup.ts
Normal file
21
packages/tm-core/tests/setup.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @fileoverview Vitest test setup file
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
|
||||
// Setup any global test configuration here
|
||||
// For example, increase timeout for slow CI environments
|
||||
if (process.env.CI) {
|
||||
// Vitest timeout is configured in vitest.config.ts
|
||||
}
|
||||
|
||||
// Suppress console errors during tests unless explicitly testing them
|
||||
const originalError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = vi.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
});
|
||||
265
packages/tm-core/tests/unit/base-provider.test.ts
Normal file
265
packages/tm-core/tests/unit/base-provider.test.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for BaseProvider abstract class
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TaskMasterError
|
||||
} from '../../src/errors/task-master-error';
|
||||
import { MockProvider } from '../mocks/mock-provider';
|
||||
|
||||
describe('BaseProvider', () => {
|
||||
describe('constructor', () => {
|
||||
it('should require an API key', () => {
|
||||
expect(() => {
|
||||
new MockProvider({ apiKey: '' });
|
||||
}).toThrow(TaskMasterError);
|
||||
});
|
||||
|
||||
it('should initialize with provided API key and model', () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
model: 'mock-model-v2'
|
||||
});
|
||||
|
||||
expect(provider.getModel()).toBe('mock-model-v2');
|
||||
});
|
||||
|
||||
it('should use default model if not provided', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
expect(provider.getModel()).toBe('mock-model-v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCompletion', () => {
|
||||
let provider: MockProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new MockProvider({ apiKey: 'test-key' });
|
||||
});
|
||||
|
||||
it('should successfully generate a completion', async () => {
|
||||
const response = await provider.generateCompletion('Test prompt');
|
||||
|
||||
expect(response).toMatchObject({
|
||||
content: 'Mock response to: Test prompt',
|
||||
provider: 'mock',
|
||||
model: 'mock-model-v1',
|
||||
inputTokens: expect.any(Number),
|
||||
outputTokens: expect.any(Number),
|
||||
totalTokens: expect.any(Number),
|
||||
duration: expect.any(Number),
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate empty prompts', async () => {
|
||||
await expect(provider.generateCompletion('')).rejects.toThrow(
|
||||
'Prompt must be a non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate prompt type', async () => {
|
||||
await expect(provider.generateCompletion(null as any)).rejects.toThrow(
|
||||
'Prompt must be a non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate temperature range', async () => {
|
||||
await expect(
|
||||
provider.generateCompletion('Test', { temperature: 3 })
|
||||
).rejects.toThrow('Temperature must be between 0 and 2');
|
||||
});
|
||||
|
||||
it('should validate maxTokens range', async () => {
|
||||
await expect(
|
||||
provider.generateCompletion('Test', { maxTokens: 0 })
|
||||
).rejects.toThrow('Max tokens must be between 1 and 100000');
|
||||
});
|
||||
|
||||
it('should validate topP range', async () => {
|
||||
await expect(
|
||||
provider.generateCompletion('Test', { topP: 1.5 })
|
||||
).rejects.toThrow('Top-p must be between 0 and 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('retry logic', () => {
|
||||
it('should retry on rate limit errors', async () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
failAfterAttempts: 2,
|
||||
simulateRateLimit: true,
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
const response = await provider.generateCompletion('Test prompt');
|
||||
|
||||
expect(response.content).toBe('Mock response to: Test prompt');
|
||||
expect(provider.getAttemptCount()).toBe(3); // 2 failures + 1 success
|
||||
});
|
||||
|
||||
it('should retry on timeout errors', async () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
failAfterAttempts: 1,
|
||||
simulateTimeout: true
|
||||
});
|
||||
|
||||
const response = await provider.generateCompletion('Test prompt');
|
||||
|
||||
expect(response.content).toBe('Mock response to: Test prompt');
|
||||
expect(provider.getAttemptCount()).toBe(2); // 1 failure + 1 success
|
||||
});
|
||||
|
||||
it('should fail after max retries', async () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
shouldFail: true
|
||||
});
|
||||
|
||||
await expect(provider.generateCompletion('Test prompt')).rejects.toThrow(
|
||||
'mock provider error'
|
||||
);
|
||||
});
|
||||
|
||||
it('should calculate exponential backoff delays', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
|
||||
// Access protected method through type assertion
|
||||
const calculateDelay = (provider as any).calculateBackoffDelay.bind(
|
||||
provider
|
||||
);
|
||||
|
||||
const delay1 = calculateDelay(1);
|
||||
const delay2 = calculateDelay(2);
|
||||
const delay3 = calculateDelay(3);
|
||||
|
||||
// Check exponential growth (with jitter, so use ranges)
|
||||
expect(delay1).toBeGreaterThanOrEqual(900);
|
||||
expect(delay1).toBeLessThanOrEqual(1100);
|
||||
|
||||
expect(delay2).toBeGreaterThanOrEqual(1800);
|
||||
expect(delay2).toBeLessThanOrEqual(2200);
|
||||
|
||||
expect(delay3).toBeGreaterThanOrEqual(3600);
|
||||
expect(delay3).toBeLessThanOrEqual(4400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should wrap provider errors properly', async () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
shouldFail: true
|
||||
});
|
||||
|
||||
try {
|
||||
await provider.generateCompletion('Test prompt');
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
const tmError = error as TaskMasterError;
|
||||
expect(tmError.code).toBe(ERROR_CODES.PROVIDER_ERROR);
|
||||
expect(tmError.context.operation).toBe('generateCompletion');
|
||||
expect(tmError.context.resource).toBe('mock');
|
||||
}
|
||||
});
|
||||
|
||||
it('should identify rate limit errors correctly', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
const isRateLimitError = (provider as any).isRateLimitError.bind(
|
||||
provider
|
||||
);
|
||||
|
||||
expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true);
|
||||
expect(isRateLimitError(new Error('Too many requests'))).toBe(true);
|
||||
expect(isRateLimitError(new Error('Status: 429'))).toBe(true);
|
||||
expect(isRateLimitError(new Error('Some other error'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should identify timeout errors correctly', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
const isTimeoutError = (provider as any).isTimeoutError.bind(provider);
|
||||
|
||||
expect(isTimeoutError(new Error('Request timeout'))).toBe(true);
|
||||
expect(isTimeoutError(new Error('Operation timed out'))).toBe(true);
|
||||
expect(isTimeoutError(new Error('ECONNRESET'))).toBe(true);
|
||||
expect(isTimeoutError(new Error('Some other error'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should identify network errors correctly', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
const isNetworkError = (provider as any).isNetworkError.bind(provider);
|
||||
|
||||
expect(isNetworkError(new Error('Network error'))).toBe(true);
|
||||
expect(isNetworkError(new Error('ENOTFOUND'))).toBe(true);
|
||||
expect(isNetworkError(new Error('ECONNREFUSED'))).toBe(true);
|
||||
expect(isNetworkError(new Error('Some other error'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('model management', () => {
|
||||
it('should get and set model', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
|
||||
expect(provider.getModel()).toBe('mock-model-v1');
|
||||
|
||||
provider.setModel('mock-model-v2');
|
||||
expect(provider.getModel()).toBe('mock-model-v2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider information', () => {
|
||||
it('should return provider info', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
const info = provider.getProviderInfo();
|
||||
|
||||
expect(info.name).toBe('mock');
|
||||
expect(info.displayName).toBe('Mock Provider');
|
||||
expect(info.requiresApiKey).toBe(true);
|
||||
expect(info.models).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return available models', () => {
|
||||
const provider = new MockProvider({ apiKey: 'test-key' });
|
||||
const models = provider.getAvailableModels();
|
||||
|
||||
expect(models).toHaveLength(2);
|
||||
expect(models[0].id).toBe('mock-model-v1');
|
||||
expect(models[1].id).toBe('mock-model-v2');
|
||||
});
|
||||
|
||||
it('should validate credentials', async () => {
|
||||
const validProvider = new MockProvider({ apiKey: 'valid-key' });
|
||||
const invalidProvider = new MockProvider({ apiKey: 'invalid-key' });
|
||||
|
||||
expect(await validProvider.validateCredentials()).toBe(true);
|
||||
expect(await invalidProvider.validateCredentials()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('template method pattern', () => {
|
||||
it('should follow the template method flow', async () => {
|
||||
const provider = new MockProvider({
|
||||
apiKey: 'test-key',
|
||||
responseDelay: 50
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await provider.generateCompletion('Test prompt', {
|
||||
temperature: 0.5,
|
||||
maxTokens: 100
|
||||
});
|
||||
const endTime = Date.now();
|
||||
|
||||
// Verify the response was processed through the template
|
||||
expect(response.content).toBeDefined();
|
||||
expect(response.duration).toBeGreaterThanOrEqual(50);
|
||||
expect(response.duration).toBeLessThanOrEqual(endTime - startTime + 10);
|
||||
expect(response.timestamp).toBeDefined();
|
||||
expect(response.provider).toBe('mock');
|
||||
});
|
||||
});
|
||||
});
|
||||
139
packages/tm-core/tests/unit/smoke.test.ts
Normal file
139
packages/tm-core/tests/unit/smoke.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Smoke tests to verify basic package functionality and imports
|
||||
*/
|
||||
|
||||
import {
|
||||
PlaceholderParser,
|
||||
PlaceholderStorage,
|
||||
StorageError,
|
||||
TaskNotFoundError,
|
||||
TmCoreError,
|
||||
ValidationError,
|
||||
formatDate,
|
||||
generateTaskId,
|
||||
isValidTaskId,
|
||||
name,
|
||||
version
|
||||
} from '@tm/core';
|
||||
|
||||
import type {
|
||||
PlaceholderTask,
|
||||
TaskId,
|
||||
TaskPriority,
|
||||
TaskStatus
|
||||
} from '@tm/core';
|
||||
|
||||
describe('tm-core smoke tests', () => {
|
||||
describe('package metadata', () => {
|
||||
it('should export correct package name and version', () => {
|
||||
expect(name).toBe('@task-master/tm-core');
|
||||
expect(version).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('utility functions', () => {
|
||||
it('should generate valid task IDs', () => {
|
||||
const id1 = generateTaskId();
|
||||
const id2 = generateTaskId();
|
||||
|
||||
expect(typeof id1).toBe('string');
|
||||
expect(typeof id2).toBe('string');
|
||||
expect(id1).not.toBe(id2); // Should be unique
|
||||
expect(isValidTaskId(id1)).toBe(true);
|
||||
expect(isValidTaskId('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should format dates', () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
const formatted = formatDate(date);
|
||||
expect(formatted).toBe('2023-01-01T00:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder storage', () => {
|
||||
it('should perform basic storage operations', async () => {
|
||||
const storage = new PlaceholderStorage();
|
||||
const testPath = 'test/path';
|
||||
const testData = 'test data';
|
||||
|
||||
// Initially should not exist
|
||||
expect(await storage.exists(testPath)).toBe(false);
|
||||
expect(await storage.read(testPath)).toBe(null);
|
||||
|
||||
// Write and verify
|
||||
await storage.write(testPath, testData);
|
||||
expect(await storage.exists(testPath)).toBe(true);
|
||||
expect(await storage.read(testPath)).toBe(testData);
|
||||
|
||||
// Delete and verify
|
||||
await storage.delete(testPath);
|
||||
expect(await storage.exists(testPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder parser', () => {
|
||||
it('should parse simple task lists', async () => {
|
||||
const parser = new PlaceholderParser();
|
||||
const content = `
|
||||
- Task 1
|
||||
- Task 2
|
||||
- Task 3
|
||||
`;
|
||||
|
||||
const isValid = await parser.validate(content);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
const tasks = await parser.parse(content);
|
||||
expect(tasks).toHaveLength(3);
|
||||
expect(tasks[0]?.title).toBe('Task 1');
|
||||
expect(tasks[1]?.title).toBe('Task 2');
|
||||
expect(tasks[2]?.title).toBe('Task 3');
|
||||
|
||||
tasks.forEach((task) => {
|
||||
expect(task.status).toBe('pending');
|
||||
expect(task.priority).toBe('medium');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error classes', () => {
|
||||
it('should create and throw custom errors', () => {
|
||||
const baseError = new TmCoreError('Base error');
|
||||
expect(baseError.name).toBe('TmCoreError');
|
||||
expect(baseError.message).toBe('Base error');
|
||||
|
||||
const taskNotFound = new TaskNotFoundError('task-123');
|
||||
expect(taskNotFound.name).toBe('TaskNotFoundError');
|
||||
expect(taskNotFound.code).toBe('TASK_NOT_FOUND');
|
||||
expect(taskNotFound.message).toContain('task-123');
|
||||
|
||||
const validationError = new ValidationError('Invalid data');
|
||||
expect(validationError.name).toBe('ValidationError');
|
||||
expect(validationError.code).toBe('VALIDATION_ERROR');
|
||||
|
||||
const storageError = new StorageError('Storage failed');
|
||||
expect(storageError.name).toBe('StorageError');
|
||||
expect(storageError.code).toBe('STORAGE_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type definitions', () => {
|
||||
it('should have correct types available', () => {
|
||||
// These are compile-time checks that verify types exist
|
||||
const taskId: TaskId = 'test-id';
|
||||
const status: TaskStatus = 'pending';
|
||||
const priority: TaskPriority = 'high';
|
||||
|
||||
const task: PlaceholderTask = {
|
||||
id: taskId,
|
||||
title: 'Test Task',
|
||||
status: status,
|
||||
priority: priority
|
||||
};
|
||||
|
||||
expect(task.id).toBe('test-id');
|
||||
expect(task.status).toBe('pending');
|
||||
expect(task.priority).toBe('high');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user