feat: create tm-core and apps/cli (#1093)

- add typescript
- add npm workspaces
This commit is contained in:
Ralph Khreish
2025-09-01 21:44:43 +02:00
parent e81040def5
commit dc811eb45e
162 changed files with 22235 additions and 706 deletions

View 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');
});
});
});

View 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;
}
}

View 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;
});

View 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');
});
});
});

View 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');
});
});
});