feat: Phase 1 - Complete TDD Workflow Automation System (#1289)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
540
apps/cli/tests/integration/commands/autopilot/workflow.test.ts
Normal file
540
apps/cli/tests/integration/commands/autopilot/workflow.test.ts
Normal file
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for autopilot workflow commands
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import type { WorkflowState } from '@tm/core';
|
||||
|
||||
// Track file system state in memory - must be in vi.hoisted() for mock access
|
||||
const {
|
||||
mockFileSystem,
|
||||
pathExistsFn,
|
||||
readJSONFn,
|
||||
writeJSONFn,
|
||||
ensureDirFn,
|
||||
removeFn
|
||||
} = vi.hoisted(() => {
|
||||
const mockFileSystem = new Map<string, string>();
|
||||
|
||||
return {
|
||||
mockFileSystem,
|
||||
pathExistsFn: vi.fn((path: string) =>
|
||||
Promise.resolve(mockFileSystem.has(path))
|
||||
),
|
||||
readJSONFn: vi.fn((path: string) => {
|
||||
const data = mockFileSystem.get(path);
|
||||
return data
|
||||
? Promise.resolve(JSON.parse(data))
|
||||
: Promise.reject(new Error('File not found'));
|
||||
}),
|
||||
writeJSONFn: vi.fn((path: string, data: any) => {
|
||||
mockFileSystem.set(path, JSON.stringify(data));
|
||||
return Promise.resolve();
|
||||
}),
|
||||
ensureDirFn: vi.fn(() => Promise.resolve()),
|
||||
removeFn: vi.fn((path: string) => {
|
||||
mockFileSystem.delete(path);
|
||||
return Promise.resolve();
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// Mock fs-extra before any imports
|
||||
vi.mock('fs-extra', () => ({
|
||||
default: {
|
||||
pathExists: pathExistsFn,
|
||||
readJSON: readJSONFn,
|
||||
writeJSON: writeJSONFn,
|
||||
ensureDir: ensureDirFn,
|
||||
remove: removeFn
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@tm/core', () => ({
|
||||
WorkflowOrchestrator: vi.fn().mockImplementation((context) => ({
|
||||
getCurrentPhase: vi.fn().mockReturnValue('SUBTASK_LOOP'),
|
||||
getCurrentTDDPhase: vi.fn().mockReturnValue('RED'),
|
||||
getContext: vi.fn().mockReturnValue(context),
|
||||
transition: vi.fn(),
|
||||
restoreState: vi.fn(),
|
||||
getState: vi.fn().mockReturnValue({ phase: 'SUBTASK_LOOP', context }),
|
||||
enableAutoPersist: vi.fn(),
|
||||
canResumeFromState: vi.fn().mockReturnValue(true),
|
||||
getCurrentSubtask: vi.fn().mockReturnValue({
|
||||
id: '1',
|
||||
title: 'Test Subtask',
|
||||
status: 'pending',
|
||||
attempts: 0
|
||||
}),
|
||||
getProgress: vi.fn().mockReturnValue({
|
||||
completed: 0,
|
||||
total: 3,
|
||||
current: 1,
|
||||
percentage: 0
|
||||
}),
|
||||
canProceed: vi.fn().mockReturnValue(false)
|
||||
})),
|
||||
GitAdapter: vi.fn().mockImplementation(() => ({
|
||||
ensureGitRepository: vi.fn().mockResolvedValue(undefined),
|
||||
ensureCleanWorkingTree: vi.fn().mockResolvedValue(undefined),
|
||||
createAndCheckoutBranch: vi.fn().mockResolvedValue(undefined),
|
||||
hasStagedChanges: vi.fn().mockResolvedValue(true),
|
||||
getStatus: vi.fn().mockResolvedValue({
|
||||
staged: ['file1.ts'],
|
||||
modified: ['file2.ts']
|
||||
}),
|
||||
createCommit: vi.fn().mockResolvedValue(undefined),
|
||||
getLastCommit: vi.fn().mockResolvedValue({
|
||||
hash: 'abc123def456',
|
||||
message: 'test commit'
|
||||
}),
|
||||
stageFiles: vi.fn().mockResolvedValue(undefined)
|
||||
})),
|
||||
CommitMessageGenerator: vi.fn().mockImplementation(() => ({
|
||||
generateMessage: vi.fn().mockReturnValue('feat: test commit message')
|
||||
})),
|
||||
createTaskMasterCore: vi.fn().mockResolvedValue({
|
||||
getTaskWithSubtask: vi.fn().mockResolvedValue({
|
||||
task: {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
subtasks: [
|
||||
{ id: '1', title: 'Subtask 1', status: 'pending' },
|
||||
{ id: '2', title: 'Subtask 2', status: 'pending' },
|
||||
{ id: '3', title: 'Subtask 3', status: 'pending' }
|
||||
],
|
||||
tag: 'test'
|
||||
}
|
||||
}),
|
||||
close: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
}));
|
||||
|
||||
// Import after mocks are set up
|
||||
import { Command } from 'commander';
|
||||
import { AutopilotCommand } from '../../../../src/commands/autopilot/index.js';
|
||||
|
||||
describe('Autopilot Workflow Integration Tests', () => {
|
||||
const projectRoot = '/test/project';
|
||||
let program: Command;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFileSystem.clear();
|
||||
|
||||
// Clear mock call history
|
||||
pathExistsFn.mockClear();
|
||||
readJSONFn.mockClear();
|
||||
writeJSONFn.mockClear();
|
||||
ensureDirFn.mockClear();
|
||||
removeFn.mockClear();
|
||||
|
||||
program = new Command();
|
||||
AutopilotCommand.register(program);
|
||||
|
||||
// Use exitOverride to handle Commander exits in tests
|
||||
program.exitOverride();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFileSystem.clear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('start command', () => {
|
||||
it('should initialize workflow and create branch', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'start',
|
||||
'1',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
]);
|
||||
|
||||
// Verify writeJSON was called with state
|
||||
expect(writeJSONFn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('workflow-state.json'),
|
||||
expect.objectContaining({
|
||||
phase: expect.any(String),
|
||||
context: expect.any(Object)
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject invalid task ID', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(
|
||||
program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'start',
|
||||
'invalid',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
])
|
||||
).rejects.toMatchObject({ exitCode: 1 });
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject starting when workflow exists without force', async () => {
|
||||
// Create existing state
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [],
|
||||
currentSubtaskIndex: 0,
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(
|
||||
program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'start',
|
||||
'1',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
])
|
||||
).rejects.toMatchObject({ exitCode: 1 });
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resume command', () => {
|
||||
beforeEach(() => {
|
||||
// Create saved state
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Subtask',
|
||||
status: 'pending',
|
||||
attempts: 0
|
||||
}
|
||||
],
|
||||
currentSubtaskIndex: 0,
|
||||
currentTDDPhase: 'RED',
|
||||
branchName: 'task-1',
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
});
|
||||
|
||||
it('should restore workflow from saved state', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'resume',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.success).toBe(true);
|
||||
expect(output.taskId).toBe('1');
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should error when no state exists', async () => {
|
||||
mockFileSystem.clear();
|
||||
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(
|
||||
program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'resume',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
])
|
||||
).rejects.toMatchObject({ exitCode: 1 });
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('next command', () => {
|
||||
beforeEach(() => {
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Subtask',
|
||||
status: 'pending',
|
||||
attempts: 0
|
||||
}
|
||||
],
|
||||
currentSubtaskIndex: 0,
|
||||
currentTDDPhase: 'RED',
|
||||
branchName: 'task-1',
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return next action in JSON format', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'next',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.action).toBe('generate_test');
|
||||
expect(output.phase).toBe('SUBTASK_LOOP');
|
||||
expect(output.tddPhase).toBe('RED');
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('status command', () => {
|
||||
beforeEach(() => {
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [
|
||||
{ id: '1', title: 'Subtask 1', status: 'completed', attempts: 1 },
|
||||
{ id: '2', title: 'Subtask 2', status: 'pending', attempts: 0 },
|
||||
{ id: '3', title: 'Subtask 3', status: 'pending', attempts: 0 }
|
||||
],
|
||||
currentSubtaskIndex: 1,
|
||||
currentTDDPhase: 'RED',
|
||||
branchName: 'task-1',
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
});
|
||||
|
||||
it('should display workflow progress', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'status',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--json'
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.taskId).toBe('1');
|
||||
expect(output.phase).toBe('SUBTASK_LOOP');
|
||||
expect(output.progress).toBeDefined();
|
||||
expect(output.subtasks).toHaveLength(3);
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('complete command', () => {
|
||||
beforeEach(() => {
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Subtask',
|
||||
status: 'in-progress',
|
||||
attempts: 0
|
||||
}
|
||||
],
|
||||
currentSubtaskIndex: 0,
|
||||
currentTDDPhase: 'RED',
|
||||
branchName: 'task-1',
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate RED phase has failures', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(
|
||||
program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'complete',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--results',
|
||||
'{"total":10,"passed":10,"failed":0,"skipped":0}',
|
||||
'--json'
|
||||
])
|
||||
).rejects.toMatchObject({ exitCode: 1 });
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should complete RED phase with failures', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'complete',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--results',
|
||||
'{"total":10,"passed":9,"failed":1,"skipped":0}',
|
||||
'--json'
|
||||
]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.success).toBe(true);
|
||||
expect(output.nextPhase).toBe('GREEN');
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('abort command', () => {
|
||||
beforeEach(() => {
|
||||
const mockState: WorkflowState = {
|
||||
phase: 'SUBTASK_LOOP',
|
||||
context: {
|
||||
taskId: '1',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Subtask',
|
||||
status: 'pending',
|
||||
attempts: 0
|
||||
}
|
||||
],
|
||||
currentSubtaskIndex: 0,
|
||||
currentTDDPhase: 'RED',
|
||||
branchName: 'task-1',
|
||||
errors: [],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
mockFileSystem.set(
|
||||
`${projectRoot}/.taskmaster/workflow-state.json`,
|
||||
JSON.stringify(mockState)
|
||||
);
|
||||
});
|
||||
|
||||
it('should abort workflow and delete state', async () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync([
|
||||
'node',
|
||||
'test',
|
||||
'autopilot',
|
||||
'abort',
|
||||
'--project-root',
|
||||
projectRoot,
|
||||
'--force',
|
||||
'--json'
|
||||
]);
|
||||
|
||||
// Verify remove was called
|
||||
expect(removeFn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('workflow-state.json')
|
||||
);
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
202
apps/cli/tests/unit/commands/autopilot/shared.test.ts
Normal file
202
apps/cli/tests/unit/commands/autopilot/shared.test.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for autopilot shared utilities
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
validateTaskId,
|
||||
parseSubtasks,
|
||||
OutputFormatter
|
||||
} from '../../../../src/commands/autopilot/shared.js';
|
||||
|
||||
// Mock fs-extra
|
||||
vi.mock('fs-extra', () => ({
|
||||
default: {
|
||||
pathExists: vi.fn(),
|
||||
readJSON: vi.fn(),
|
||||
writeJSON: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
remove: vi.fn()
|
||||
},
|
||||
pathExists: vi.fn(),
|
||||
readJSON: vi.fn(),
|
||||
writeJSON: vi.fn(),
|
||||
ensureDir: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}));
|
||||
|
||||
describe('Autopilot Shared Utilities', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const statePath = `${projectRoot}/.taskmaster/workflow-state.json`;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('validateTaskId', () => {
|
||||
it('should validate simple task IDs', () => {
|
||||
expect(validateTaskId('1')).toBe(true);
|
||||
expect(validateTaskId('10')).toBe(true);
|
||||
expect(validateTaskId('999')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate subtask IDs', () => {
|
||||
expect(validateTaskId('1.1')).toBe(true);
|
||||
expect(validateTaskId('1.2')).toBe(true);
|
||||
expect(validateTaskId('10.5')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate nested subtask IDs', () => {
|
||||
expect(validateTaskId('1.1.1')).toBe(true);
|
||||
expect(validateTaskId('1.2.3')).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid formats', () => {
|
||||
expect(validateTaskId('')).toBe(false);
|
||||
expect(validateTaskId('abc')).toBe(false);
|
||||
expect(validateTaskId('1.')).toBe(false);
|
||||
expect(validateTaskId('.1')).toBe(false);
|
||||
expect(validateTaskId('1..2')).toBe(false);
|
||||
expect(validateTaskId('1.2.3.')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSubtasks', () => {
|
||||
it('should parse subtasks from task data', () => {
|
||||
const task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
subtasks: [
|
||||
{ id: '1', title: 'Subtask 1', status: 'pending' },
|
||||
{ id: '2', title: 'Subtask 2', status: 'done' },
|
||||
{ id: '3', title: 'Subtask 3', status: 'in-progress' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = parseSubtasks(task, 5);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toEqual({
|
||||
id: '1',
|
||||
title: 'Subtask 1',
|
||||
status: 'pending',
|
||||
attempts: 0,
|
||||
maxAttempts: 5
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: '2',
|
||||
title: 'Subtask 2',
|
||||
status: 'completed',
|
||||
attempts: 0,
|
||||
maxAttempts: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for missing subtasks', () => {
|
||||
const task = { id: '1', title: 'Test Task' };
|
||||
expect(parseSubtasks(task)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use default maxAttempts', () => {
|
||||
const task = {
|
||||
subtasks: [{ id: '1', title: 'Subtask 1', status: 'pending' }]
|
||||
};
|
||||
|
||||
const result = parseSubtasks(task);
|
||||
expect(result[0].maxAttempts).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// State persistence tests omitted - covered in integration tests
|
||||
|
||||
describe('OutputFormatter', () => {
|
||||
let consoleLogSpy: any;
|
||||
let consoleErrorSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('JSON mode', () => {
|
||||
it('should output JSON for success', () => {
|
||||
const formatter = new OutputFormatter(true);
|
||||
formatter.success('Test message', { key: 'value' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.success).toBe(true);
|
||||
expect(output.message).toBe('Test message');
|
||||
expect(output.key).toBe('value');
|
||||
});
|
||||
|
||||
it('should output JSON for error', () => {
|
||||
const formatter = new OutputFormatter(true);
|
||||
formatter.error('Error message', { code: 'ERR001' });
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleErrorSpy.mock.calls[0][0]);
|
||||
expect(output.error).toBe('Error message');
|
||||
expect(output.code).toBe('ERR001');
|
||||
});
|
||||
|
||||
it('should output JSON for data', () => {
|
||||
const formatter = new OutputFormatter(true);
|
||||
formatter.output({ test: 'data' });
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
||||
expect(output.test).toBe('data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text mode', () => {
|
||||
it('should output formatted text for success', () => {
|
||||
const formatter = new OutputFormatter(false);
|
||||
formatter.success('Test message');
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('✓ Test message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should output formatted text for error', () => {
|
||||
const formatter = new OutputFormatter(false);
|
||||
formatter.error('Error message');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Error: Error message')
|
||||
);
|
||||
});
|
||||
|
||||
it('should output formatted text for warning', () => {
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
const formatter = new OutputFormatter(false);
|
||||
formatter.warning('Warning message');
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('⚠ Warning message')
|
||||
);
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not output info in JSON mode', () => {
|
||||
const formatter = new OutputFormatter(true);
|
||||
formatter.info('Info message');
|
||||
|
||||
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user