test: add unit tests for exec-utils, feature verification, project analysis, and task execution

- Introduced comprehensive unit tests for exec-utils, covering execAsync, extendedPath, execEnv, and isENOENT functions.
- Added tests for FeatureVerificationService to validate feature verification and commit processes.
- Implemented tests for ProjectAnalyzer to ensure project analysis functionality.
- Created tests for TaskExecutor to validate task execution and event emissions.

These additions enhance test coverage and ensure the reliability of core functionalities.
This commit is contained in:
Kacper
2025-12-23 00:52:59 +01:00
parent a1c5d0cb0d
commit 51641bad9c
4 changed files with 874 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// Store original platform and env
const originalPlatform = process.platform;
const originalEnv = { ...process.env };
describe('exec-utils.ts', () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
process.env = { ...originalEnv };
});
describe('execAsync', () => {
it('should be a promisified exec function', async () => {
const { execAsync } = await import('@/lib/exec-utils.js');
expect(typeof execAsync).toBe('function');
});
it('should execute shell commands successfully', async () => {
const { execAsync } = await import('@/lib/exec-utils.js');
const result = await execAsync('echo "hello"');
expect(result.stdout.trim()).toBe('hello');
});
it('should reject on invalid commands', async () => {
const { execAsync } = await import('@/lib/exec-utils.js');
await expect(execAsync('nonexistent-command-12345')).rejects.toThrow();
});
});
describe('extendedPath', () => {
it('should include the original PATH', async () => {
const { extendedPath } = await import('@/lib/exec-utils.js');
expect(extendedPath).toContain(process.env.PATH);
});
it('should include additional Unix paths on non-Windows', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });
vi.resetModules();
const { extendedPath } = await import('@/lib/exec-utils.js');
expect(extendedPath).toContain('/opt/homebrew/bin');
expect(extendedPath).toContain('/usr/local/bin');
});
});
describe('execEnv', () => {
it('should have PATH set to extendedPath', async () => {
const { execEnv, extendedPath } = await import('@/lib/exec-utils.js');
expect(execEnv.PATH).toBe(extendedPath);
});
it('should include all original environment variables', async () => {
const { execEnv } = await import('@/lib/exec-utils.js');
// Should have common env vars
expect(execEnv.HOME || execEnv.USERPROFILE).toBeDefined();
});
});
describe('isENOENT', () => {
it('should return true for ENOENT errors', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
const error = { code: 'ENOENT' };
expect(isENOENT(error)).toBe(true);
});
it('should return false for other error codes', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
const error = { code: 'EACCES' };
expect(isENOENT(error)).toBe(false);
});
it('should return false for null', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
expect(isENOENT(null)).toBe(false);
});
it('should return false for undefined', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
expect(isENOENT(undefined)).toBe(false);
});
it('should return false for non-objects', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
expect(isENOENT('ENOENT')).toBe(false);
expect(isENOENT(123)).toBe(false);
});
it('should return false for objects without code property', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
expect(isENOENT({})).toBe(false);
expect(isENOENT({ message: 'error' })).toBe(false);
});
it('should handle Error objects with code', async () => {
const { isENOENT } = await import('@/lib/exec-utils.js');
const error = new Error('File not found') as Error & { code: string };
error.code = 'ENOENT';
expect(isENOENT(error)).toBe(true);
});
});
describe('Windows platform handling', () => {
it('should use semicolon as path separator on Windows', async () => {
Object.defineProperty(process, 'platform', { value: 'win32' });
process.env.LOCALAPPDATA = 'C:\\Users\\Test\\AppData\\Local';
process.env.PROGRAMFILES = 'C:\\Program Files';
vi.resetModules();
const { extendedPath } = await import('@/lib/exec-utils.js');
// Windows uses semicolon separator
expect(extendedPath).toContain(';');
expect(extendedPath).toContain('\\Git\\cmd');
});
});
describe('Unix platform handling', () => {
it('should use colon as path separator on Unix', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
process.env.HOME = '/home/testuser';
vi.resetModules();
const { extendedPath } = await import('@/lib/exec-utils.js');
// Unix uses colon separator
expect(extendedPath).toContain(':');
expect(extendedPath).toContain('/home/linuxbrew/.linuxbrew/bin');
});
it('should include HOME/.local/bin path', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });
process.env.HOME = '/Users/testuser';
vi.resetModules();
const { extendedPath } = await import('@/lib/exec-utils.js');
expect(extendedPath).toContain('/Users/testuser/.local/bin');
});
});
});

View File

@@ -0,0 +1,238 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FeatureVerificationService } from '@/services/auto-mode/feature-verification.js';
// Mock dependencies
vi.mock('@automaker/platform', () => ({
secureFs: {
access: vi.fn(),
readFile: vi.fn(),
},
getFeatureDir: vi.fn(
(projectPath: string, featureId: string) => `${projectPath}/.automaker/features/${featureId}`
),
}));
vi.mock('@automaker/git-utils', () => ({
runVerificationChecks: vi.fn(),
hasUncommittedChanges: vi.fn(),
commitAll: vi.fn(),
shortHash: vi.fn((hash: string) => hash.substring(0, 7)),
}));
vi.mock('@automaker/prompts', () => ({
extractTitleFromDescription: vi.fn((desc: string) => desc.split('\n')[0]),
}));
import { secureFs, getFeatureDir } from '@automaker/platform';
import { runVerificationChecks, hasUncommittedChanges, commitAll } from '@automaker/git-utils';
describe('FeatureVerificationService', () => {
let service: FeatureVerificationService;
let mockEvents: { emit: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockEvents = { emit: vi.fn() };
service = new FeatureVerificationService(mockEvents as any);
});
describe('constructor', () => {
it('should create service instance', () => {
expect(service).toBeInstanceOf(FeatureVerificationService);
});
});
describe('resolveWorkDir', () => {
it('should return worktree path if it exists', async () => {
vi.mocked(secureFs.access).mockResolvedValue(undefined);
const result = await service.resolveWorkDir('/project', 'feature-1');
expect(result).toBe('/project/.worktrees/feature-1');
expect(secureFs.access).toHaveBeenCalledWith('/project/.worktrees/feature-1');
});
it('should return project path if worktree does not exist', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
const result = await service.resolveWorkDir('/project', 'feature-1');
expect(result).toBe('/project');
});
});
describe('verify', () => {
it('should emit success event when verification passes', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(runVerificationChecks).mockResolvedValue({ success: true });
const result = await service.verify('/project', 'feature-1');
expect(result.success).toBe(true);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feature-1',
passes: true,
message: 'All verification checks passed',
});
});
it('should emit failure event when verification fails', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(runVerificationChecks).mockResolvedValue({
success: false,
failedCheck: 'lint',
});
const result = await service.verify('/project', 'feature-1');
expect(result.success).toBe(false);
expect(result.failedCheck).toBe('lint');
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feature-1',
passes: false,
message: 'Verification failed: lint',
});
});
it('should use worktree path if available', async () => {
vi.mocked(secureFs.access).mockResolvedValue(undefined);
vi.mocked(runVerificationChecks).mockResolvedValue({ success: true });
await service.verify('/project', 'feature-1');
expect(runVerificationChecks).toHaveBeenCalledWith('/project/.worktrees/feature-1');
});
});
describe('commit', () => {
it('should return null hash when no changes', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(false);
const result = await service.commit('/project', 'feature-1', null);
expect(result.hash).toBeNull();
expect(commitAll).not.toHaveBeenCalled();
});
it('should commit changes and return hash', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
vi.mocked(commitAll).mockResolvedValue('abc123def456');
const result = await service.commit('/project', 'feature-1', {
id: 'feature-1',
description: 'Add login button\nWith authentication',
} as any);
expect(result.hash).toBe('abc123def456');
expect(result.shortHash).toBe('abc123d');
expect(commitAll).toHaveBeenCalledWith(
'/project',
expect.stringContaining('feat: Add login button')
);
});
it('should use provided worktree path', async () => {
vi.mocked(secureFs.access).mockResolvedValue(undefined);
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
vi.mocked(commitAll).mockResolvedValue('abc123');
await service.commit('/project', 'feature-1', null, '/custom/worktree');
expect(hasUncommittedChanges).toHaveBeenCalledWith('/custom/worktree');
});
it('should fall back to project path if provided worktree does not exist', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(false);
await service.commit('/project', 'feature-1', null, '/nonexistent/worktree');
expect(hasUncommittedChanges).toHaveBeenCalledWith('/project');
});
it('should use feature ID in commit message when no feature provided', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
vi.mocked(commitAll).mockResolvedValue('abc123');
await service.commit('/project', 'feature-123', null);
expect(commitAll).toHaveBeenCalledWith(
'/project',
expect.stringContaining('feat: Feature feature-123')
);
});
it('should emit event on successful commit', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
vi.mocked(commitAll).mockResolvedValue('abc123def');
await service.commit('/project', 'feature-1', null);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feature-1',
passes: true,
message: expect.stringContaining('Changes committed:'),
});
});
it('should return null hash when commit fails', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
vi.mocked(hasUncommittedChanges).mockResolvedValue(true);
vi.mocked(commitAll).mockResolvedValue(null);
const result = await service.commit('/project', 'feature-1', null);
expect(result.hash).toBeNull();
});
});
describe('contextExists', () => {
it('should return true if context file exists', async () => {
vi.mocked(secureFs.access).mockResolvedValue(undefined);
const result = await service.contextExists('/project', 'feature-1');
expect(result).toBe(true);
expect(secureFs.access).toHaveBeenCalledWith(
'/project/.automaker/features/feature-1/agent-output.md'
);
});
it('should return false if context file does not exist', async () => {
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
const result = await service.contextExists('/project', 'feature-1');
expect(result).toBe(false);
});
});
describe('loadContext', () => {
it('should return context content if file exists', async () => {
vi.mocked(secureFs.readFile).mockResolvedValue('# Agent Output\nSome content');
const result = await service.loadContext('/project', 'feature-1');
expect(result).toBe('# Agent Output\nSome content');
expect(secureFs.readFile).toHaveBeenCalledWith(
'/project/.automaker/features/feature-1/agent-output.md',
'utf-8'
);
});
it('should return null if file does not exist', async () => {
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
const result = await service.loadContext('/project', 'feature-1');
expect(result).toBeNull();
});
});
});

View File

@@ -0,0 +1,161 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Use vi.hoisted for mock functions that need to be used in vi.mock factories
const {
mockExecuteQuery,
mockProcessStream,
mockMkdir,
mockWriteFile,
mockValidateWorkingDirectory,
} = vi.hoisted(() => ({
mockExecuteQuery: vi.fn(),
mockProcessStream: vi.fn(),
mockMkdir: vi.fn(),
mockWriteFile: vi.fn(),
mockValidateWorkingDirectory: vi.fn(),
}));
// Mock dependencies
vi.mock('@automaker/platform', () => ({
secureFs: {
mkdir: mockMkdir,
writeFile: mockWriteFile,
},
getAutomakerDir: (projectPath: string) => `${projectPath}/.automaker`,
}));
vi.mock('@automaker/utils', async () => {
const actual = await vi.importActual('@automaker/utils');
return {
...actual,
processStream: mockProcessStream,
};
});
vi.mock('@automaker/model-resolver', () => ({
resolveModelString: () => 'claude-sonnet-4-20250514',
DEFAULT_MODELS: { claude: 'claude-sonnet-4-20250514' },
}));
vi.mock('@/providers/provider-factory.js', () => ({
ProviderFactory: {
getProviderForModel: () => ({
executeQuery: mockExecuteQuery,
}),
},
}));
vi.mock('@/lib/sdk-options.js', () => ({
validateWorkingDirectory: mockValidateWorkingDirectory,
}));
import { ProjectAnalyzer } from '@/services/auto-mode/project-analyzer.js';
describe('ProjectAnalyzer', () => {
let analyzer: ProjectAnalyzer;
let mockEvents: { emit: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockEvents = { emit: vi.fn() };
mockExecuteQuery.mockReturnValue(
(async function* () {
yield { type: 'text', text: 'Analysis result' };
})()
);
mockProcessStream.mockResolvedValue({
text: '# Project Analysis\nThis is a test project.',
toolUses: [],
});
analyzer = new ProjectAnalyzer(mockEvents as any);
});
describe('constructor', () => {
it('should create analyzer instance', () => {
expect(analyzer).toBeInstanceOf(ProjectAnalyzer);
});
});
describe('analyze', () => {
it('should validate working directory', async () => {
await analyzer.analyze('/project');
expect(mockValidateWorkingDirectory).toHaveBeenCalledWith('/project');
});
it('should emit start event', async () => {
await analyzer.analyze('/project');
expect(mockEvents.emit).toHaveBeenCalledWith(
'auto-mode:event',
expect.objectContaining({
type: 'auto_mode_feature_start',
projectPath: '/project',
})
);
});
it('should call provider executeQuery with correct options', async () => {
await analyzer.analyze('/project');
expect(mockExecuteQuery).toHaveBeenCalledWith(
expect.objectContaining({
cwd: '/project',
maxTurns: 5,
allowedTools: ['Read', 'Glob', 'Grep'],
})
);
});
it('should save analysis to file', async () => {
await analyzer.analyze('/project');
expect(mockMkdir).toHaveBeenCalledWith('/project/.automaker', { recursive: true });
expect(mockWriteFile).toHaveBeenCalledWith(
'/project/.automaker/project-analysis.md',
'# Project Analysis\nThis is a test project.'
);
});
it('should emit complete event on success', async () => {
await analyzer.analyze('/project');
expect(mockEvents.emit).toHaveBeenCalledWith(
'auto-mode:event',
expect.objectContaining({
type: 'auto_mode_feature_complete',
passes: true,
message: 'Project analysis completed',
})
);
});
it('should emit error event on failure', async () => {
mockProcessStream.mockRejectedValue(new Error('Analysis failed'));
await analyzer.analyze('/project');
expect(mockEvents.emit).toHaveBeenCalledWith(
'auto-mode:event',
expect.objectContaining({
type: 'auto_mode_error',
error: expect.stringContaining('Analysis failed'),
})
);
});
it('should handle stream with onText callback', async () => {
await analyzer.analyze('/project');
expect(mockProcessStream).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
onText: expect.any(Function),
})
);
});
});
});

View File

@@ -0,0 +1,332 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TaskExecutor } from '@/services/auto-mode/task-executor.js';
import type { ParsedTask } from '@automaker/types';
import type { TaskExecutionContext } from '@/services/auto-mode/types.js';
// Use vi.hoisted for mock functions
const { mockBuildTaskPrompt, mockProcessStream } = vi.hoisted(() => ({
mockBuildTaskPrompt: vi.fn(),
mockProcessStream: vi.fn(),
}));
// Mock dependencies
vi.mock('@automaker/prompts', () => ({
buildTaskPrompt: mockBuildTaskPrompt,
}));
vi.mock('@automaker/utils', async () => {
const actual = await vi.importActual('@automaker/utils');
return {
...actual,
processStream: mockProcessStream,
};
});
describe('TaskExecutor', () => {
let executor: TaskExecutor;
let mockEvents: { emit: ReturnType<typeof vi.fn> };
let mockProvider: { executeQuery: ReturnType<typeof vi.fn> };
let mockContext: TaskExecutionContext;
let mockTasks: ParsedTask[];
beforeEach(() => {
vi.clearAllMocks();
mockEvents = { emit: vi.fn() };
mockProvider = {
executeQuery: vi.fn().mockReturnValue(
(async function* () {
yield { type: 'text', text: 'Task output' };
})()
),
};
mockContext = {
featureId: 'feature-1',
projectPath: '/project',
workDir: '/project/worktree',
model: 'claude-sonnet-4-20250514',
maxTurns: 100,
allowedTools: ['Read', 'Write'],
abortController: new AbortController(),
planContent: '# Plan\nTask list',
userFeedback: undefined,
};
mockTasks = [
{ id: '1', description: 'Task 1', phase: 'Phase 1' },
{ id: '2', description: 'Task 2', phase: 'Phase 1' },
{ id: '3', description: 'Task 3', phase: 'Phase 2' },
];
mockBuildTaskPrompt.mockReturnValue('Generated task prompt');
mockProcessStream.mockResolvedValue({ text: 'Processed output', toolUses: [] });
executor = new TaskExecutor(mockEvents as any);
});
describe('constructor', () => {
it('should create executor instance', () => {
expect(executor).toBeInstanceOf(TaskExecutor);
});
});
describe('executeAll', () => {
it('should yield started and completed events for each task', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Should have 2 events per task (started + completed)
expect(results).toHaveLength(6);
expect(results[0]).toEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'started',
});
expect(results[1]).toEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'completed',
output: 'Processed output',
phaseComplete: undefined,
});
});
it('should emit task started events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_task_started',
featureId: 'feature-1',
projectPath: '/project',
taskId: '1',
taskDescription: 'Task 1',
taskIndex: 0,
tasksTotal: 3,
});
});
it('should emit task complete events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_task_complete',
featureId: 'feature-1',
projectPath: '/project',
taskId: '1',
tasksCompleted: 1,
tasksTotal: 3,
});
});
it('should throw on abort', async () => {
mockContext.abortController.abort();
const results: any[] = [];
await expect(async () => {
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
}).rejects.toThrow('Feature execution aborted');
});
it('should call provider executeQuery with correct options', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockProvider.executeQuery).toHaveBeenCalledWith({
prompt: 'Generated task prompt',
model: 'claude-sonnet-4-20250514',
maxTurns: 50, // Limited to 50 per task
cwd: '/project/worktree',
allowedTools: ['Read', 'Write'],
abortController: mockContext.abortController,
});
});
it('should detect phase completion', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Task 2 completes Phase 1 (next task is Phase 2)
const task2Completed = results.find((r) => r.taskId === '2' && r.status === 'completed');
expect(task2Completed?.phaseComplete).toBe(1);
// Task 3 completes Phase 2 (no more tasks)
const task3Completed = results.find((r) => r.taskId === '3' && r.status === 'completed');
expect(task3Completed?.phaseComplete).toBe(2);
});
it('should emit phase complete events', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_phase_complete',
featureId: 'feature-1',
projectPath: '/project',
phaseNumber: 1,
});
});
it('should yield failed status on error', async () => {
mockProcessStream.mockRejectedValueOnce(new Error('Task failed'));
const results: any[] = [];
await expect(async () => {
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
}).rejects.toThrow('Task failed');
expect(results).toContainEqual({
taskId: '1',
taskIndex: 0,
tasksTotal: 3,
status: 'failed',
output: 'Task failed',
});
});
});
describe('executeOne', () => {
it('should execute a single task and return output', async () => {
const result = await executor.executeOne(
mockTasks[0],
mockTasks,
0,
mockContext,
mockProvider as any
);
expect(result).toBe('Processed output');
});
it('should build prompt with correct parameters', async () => {
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockBuildTaskPrompt).toHaveBeenCalledWith(
mockTasks[0],
mockTasks,
0,
mockContext.planContent,
mockContext.userFeedback
);
});
it('should emit progress events for text output', async () => {
mockProcessStream.mockImplementation(async (_stream, options) => {
options.onText?.('Some output');
return { text: 'Some output', toolUses: [] };
});
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_progress',
featureId: 'feature-1',
content: 'Some output',
});
});
it('should emit tool events for tool use', async () => {
mockProcessStream.mockImplementation(async (_stream, options) => {
options.onToolUse?.('Read', { path: '/file.txt' });
return { text: 'Output', toolUses: [] };
});
await executor.executeOne(mockTasks[0], mockTasks, 0, mockContext, mockProvider as any);
expect(mockEvents.emit).toHaveBeenCalledWith('auto-mode:event', {
type: 'auto_mode_tool',
featureId: 'feature-1',
tool: 'Read',
input: { path: '/file.txt' },
});
});
});
describe('phase detection', () => {
it('should not detect phase completion for tasks without phase', async () => {
const tasksNoPhase = [
{ id: '1', description: 'Task 1' },
{ id: '2', description: 'Task 2' },
];
const results: any[] = [];
for await (const progress of executor.executeAll(
tasksNoPhase,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
const completedResults = results.filter((r) => r.status === 'completed');
expect(completedResults.every((r) => r.phaseComplete === undefined)).toBe(true);
});
it('should detect phase change when next task has different phase', async () => {
const results: any[] = [];
for await (const progress of executor.executeAll(
mockTasks,
mockContext,
mockProvider as any
)) {
results.push(progress);
}
// Task 2 (Phase 1) -> Task 3 (Phase 2) = phase complete
const task2Completed = results.find((r) => r.taskId === '2' && r.status === 'completed');
expect(task2Completed?.phaseComplete).toBe(1);
});
});
});