Files
Profa fd2f37eaa0 fix(v3): update README claims and init hooks to use correct package name
Changes:
- Fix README.md agent counts (48 QE agents = 41 main + 7 subagents)
- Fix README.md skill counts (15 QE-specific skills)
- Fix v3/README.md agent counts (47 → 48)
- Fix init-wizard.ts hooks from 'npx @agentic-qe/v3' to 'npx agentic-qe'
- Complete v2 → v2/ folder reorganization (54 items moved)
- Update root package.json to point to v3 as main version

Verified claims:
- 48 QE agents (41 main + 7 TDD subagents)
- 15 QE-specific skills
- 12 DDD bounded contexts
- ~5,600+ tests
- HNSW, ReasoningBank, Queen Coordinator features exist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 18:03:40 +00:00

630 lines
22 KiB
TypeScript

/**
* Workflow CLI Commands Test Suite
*
* Tests for workflow list, pause, and cancel commands
* Following TDD approach with comprehensive coverage
*/
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
// Mock Logger to prevent undefined errors in Database
jest.mock('@utils/Logger', () => ({
Logger: {
getInstance: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn()
}))
}
}));
// Mock workflow data for testing
const mockWorkflows = [
{
id: 'wf-001',
name: 'API Test Suite',
status: 'running',
progress: 0.65,
startedAt: new Date('2025-01-06T10:00:00Z'),
steps: 10,
completedSteps: 6
},
{
id: 'wf-002',
name: 'E2E Tests',
status: 'paused',
progress: 0.4,
startedAt: new Date('2025-01-06T09:30:00Z'),
steps: 5,
completedSteps: 2
},
{
id: 'wf-003',
name: 'Performance Tests',
status: 'completed',
progress: 1.0,
startedAt: new Date('2025-01-06T08:00:00Z'),
completedAt: new Date('2025-01-06T09:00:00Z'),
steps: 8,
completedSteps: 8
}
];
describe('Workflow List Command', () => {
beforeEach(() => {
// Mock process.exit to prevent test interruption
jest.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined): never => {
throw new Error(`Process.exit called with code ${code}`);
});
});
describe('Basic Functionality', () => {
it('should list all workflows', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({});
expect(result).toBeDefined();
expect(Array.isArray(result.workflows)).toBe(true);
});
it('should display workflow ID, name, and status', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({});
if (result.workflows.length > 0) {
const workflow = result.workflows[0];
expect(workflow).toHaveProperty('id');
expect(workflow).toHaveProperty('name');
expect(workflow).toHaveProperty('status');
}
});
it('should show progress for running workflows', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({});
const runningWorkflow = result.workflows.find((w: any) => w.status === 'running');
if (runningWorkflow) {
expect(runningWorkflow).toHaveProperty('progress');
expect(typeof runningWorkflow.progress).toBe('number');
}
});
it('should display start times', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({});
if (result.workflows.length > 0) {
expect(result.workflows[0]).toHaveProperty('startedAt');
}
});
it('should handle empty workflow list', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
// Mock empty response
jest.mock('@cli/commands/workflow/list', () => ({
listWorkflows: jest.fn().mockResolvedValue({ workflows: [] })
}));
const result = await listWorkflows({});
expect(result.workflows).toEqual([]);
});
});
describe('Filtering Options', () => {
it('should filter by status', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ status: 'running' });
result.workflows.forEach((w: any) => {
expect(w.status).toBe('running');
});
});
it('should filter by name pattern', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ name: 'test' });
result.workflows.forEach((w: any) => {
expect(w.name.toLowerCase()).toContain('test');
});
});
it('should support multiple status filters', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ status: ['running', 'paused'] });
result.workflows.forEach((w: any) => {
expect(['running', 'paused']).toContain(w.status);
});
});
it('should limit results with --limit option', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ limit: 2 });
expect(result.workflows.length).toBeLessThanOrEqual(2);
});
it('should sort workflows by start time', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ sort: 'startTime' });
for (let i = 1; i < result.workflows.length; i++) {
const prev = new Date(result.workflows[i - 1].startedAt);
const curr = new Date(result.workflows[i].startedAt);
expect(prev.getTime()).toBeGreaterThanOrEqual(curr.getTime());
}
});
});
describe('Output Formats', () => {
it('should support JSON output', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ format: 'json' });
expect(() => JSON.stringify(result)).not.toThrow();
});
it('should support table output', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ format: 'table' });
expect(result).toHaveProperty('formatted');
});
it('should include detailed info with --detailed flag', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
const result = await listWorkflows({ detailed: true });
if (result.workflows.length > 0) {
expect(result.workflows[0]).toHaveProperty('steps');
expect(result.workflows[0]).toHaveProperty('completedSteps');
}
});
});
describe('Error Handling', () => {
it('should handle connection errors gracefully', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
// Mock network error
jest.mock('@cli/commands/workflow/list', () => ({
listWorkflows: jest.fn().mockRejectedValue(new Error('Connection failed'))
}));
await expect(listWorkflows({})).rejects.toThrow('Connection failed');
});
it('should validate invalid status filters', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
await expect(listWorkflows({ status: 'invalid' }))
.rejects.toThrow(/invalid status/i);
});
});
describe('Memory Integration', () => {
it('should store list results in memory', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
await listWorkflows({});
// Verify memory key exists
const memoryKey = 'aqe/swarm/workflow-cli-commands/list-cache';
// Memory assertion would go here
});
});
});
describe('Workflow Pause Command', () => {
describe('Basic Functionality', () => {
it('should pause a running workflow', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({ workflowId: 'wf-001' });
expect(result.success).toBe(true);
expect(result.status).toBe('paused');
});
it('should require workflow ID', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await expect(pauseWorkflow({ workflowId: '' }))
.rejects.toThrow(/workflow id required/i);
});
it('should validate workflow exists', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await expect(pauseWorkflow({ workflowId: 'non-existent' }))
.rejects.toThrow(/workflow not found/i);
});
it('should only pause running workflows', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await expect(pauseWorkflow({ workflowId: 'wf-003' })) // completed workflow
.rejects.toThrow(/cannot pause.*workflow/i);
});
it('should return paused workflow state', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({ workflowId: 'wf-001' });
expect(result).toHaveProperty('workflow');
expect(result.workflow.status).toBe('paused');
expect(result.workflow).toHaveProperty('pausedAt');
});
});
describe('Pause Options', () => {
it('should support graceful pause', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({
workflowId: 'wf-001',
graceful: true
});
expect(result.pauseMode).toBe('graceful');
});
it('should support immediate pause', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({
workflowId: 'wf-001',
immediate: true
});
expect(result.pauseMode).toBe('immediate');
});
it('should allow adding pause reason', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({
workflowId: 'wf-001',
reason: 'Manual intervention required'
});
expect(result.workflow.pauseReason).toBe('Manual intervention required');
});
it('should support timeout for graceful pause', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({
workflowId: 'wf-001',
graceful: true,
timeout: 30000
});
expect(result.success).toBe(true);
});
});
describe('State Management', () => {
it('should save workflow state before pausing', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({ workflowId: 'wf-001' });
expect(result.workflow).toHaveProperty('savedState');
expect(result.workflow.savedState).toHaveProperty('completedSteps');
});
it('should notify agents of pause', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const result = await pauseWorkflow({ workflowId: 'wf-001' });
expect(result).toHaveProperty('notifiedAgents');
expect(Array.isArray(result.notifiedAgents)).toBe(true);
});
it('should store pause event in audit log', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await pauseWorkflow({ workflowId: 'wf-001', reason: 'Testing pause' });
// Verify audit log entry
// Audit log verification would go here
});
});
describe('Error Handling', () => {
it('should handle already paused workflows', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await expect(pauseWorkflow({ workflowId: 'wf-002' })) // already paused
.rejects.toThrow(/already paused/i);
});
it('should rollback on pause failure', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
// Mock pause failure
jest.mock('@cli/commands/workflow/pause', () => ({
pauseWorkflow: jest.fn().mockRejectedValue(new Error('Pause failed'))
}));
await expect(pauseWorkflow({ workflowId: 'wf-001' }))
.rejects.toThrow('Pause failed');
});
});
describe('Memory Integration', () => {
it('should update workflow status in memory', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await pauseWorkflow({ workflowId: 'wf-001' });
// Verify memory update
const memoryKey = 'aqe/swarm/workflow-cli-commands/workflow-wf-001-status';
// Memory assertion would go here
});
it('should store progress checkpoint', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
await pauseWorkflow({ workflowId: 'wf-001' });
const checkpointKey = 'aqe/swarm/workflow-cli-commands/checkpoint-wf-001';
// Memory assertion would go here
});
});
});
describe('Workflow Cancel Command', () => {
describe('Basic Functionality', () => {
it('should cancel a running workflow', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({ workflowId: 'wf-001' });
expect(result.success).toBe(true);
expect(result.status).toBe('cancelled');
});
it('should require workflow ID', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await expect(cancelWorkflow({ workflowId: '' }))
.rejects.toThrow(/workflow id required/i);
});
it('should validate workflow exists', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await expect(cancelWorkflow({ workflowId: 'non-existent' }))
.rejects.toThrow(/workflow not found/i);
});
it('should cancel running and paused workflows', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result1 = await cancelWorkflow({ workflowId: 'wf-001' }); // running
const result2 = await cancelWorkflow({ workflowId: 'wf-002' }); // paused
expect(result1.success).toBe(true);
expect(result2.success).toBe(true);
});
it('should not cancel completed workflows', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await expect(cancelWorkflow({ workflowId: 'wf-003' })) // completed
.rejects.toThrow(/cannot cancel.*workflow/i);
});
});
describe('Cancellation Options', () => {
it('should support graceful cancellation', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
graceful: true
});
expect(result.cancellationMode).toBe('graceful');
});
it('should support forced cancellation', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
force: true
});
expect(result.cancellationMode).toBe('forced');
});
it('should require confirmation for force cancel', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await expect(cancelWorkflow({
workflowId: 'wf-001',
force: true,
confirm: false
})).rejects.toThrow(/confirmation required/i);
});
it('should allow adding cancellation reason', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
reason: 'Requirements changed'
});
expect(result.workflow.cancelReason).toBe('Requirements changed');
});
it('should support cleanup flag', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
cleanup: true
});
expect(result.cleanupPerformed).toBe(true);
});
});
describe('Cleanup Operations', () => {
it('should stop all running agents', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({ workflowId: 'wf-001' });
expect(result).toHaveProperty('stoppedAgents');
expect(Array.isArray(result.stoppedAgents)).toBe(true);
});
it('should clean up temporary resources', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
cleanup: true
});
expect(result).toHaveProperty('cleanedResources');
});
it('should notify dependent workflows', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({ workflowId: 'wf-001' });
expect(result).toHaveProperty('notifiedWorkflows');
});
it('should preserve partial results option', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
preserveResults: true
});
expect(result.workflow).toHaveProperty('partialResults');
});
});
describe('State Management', () => {
it('should save final state before cancelling', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({ workflowId: 'wf-001' });
expect(result.workflow).toHaveProperty('finalState');
expect(result.workflow.finalState).toHaveProperty('completedSteps');
});
it('should create cancellation checkpoint', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({ workflowId: 'wf-001' });
expect(result).toHaveProperty('checkpointId');
});
it('should store cancellation event in audit log', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await cancelWorkflow({
workflowId: 'wf-001',
reason: 'Testing cancel'
});
// Verify audit log entry
// Audit log verification would go here
});
});
describe('Error Handling', () => {
it('should handle cancellation failures', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
// Mock cancellation failure
jest.mock('@cli/commands/workflow/cancel', () => ({
cancelWorkflow: jest.fn().mockRejectedValue(new Error('Cancel failed'))
}));
await expect(cancelWorkflow({ workflowId: 'wf-001' }))
.rejects.toThrow('Cancel failed');
});
it('should retry agent stop on failure', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
retryOnFailure: true
});
expect(result).toHaveProperty('retryAttempts');
});
it('should handle partial cleanup failures', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const result = await cancelWorkflow({
workflowId: 'wf-001',
cleanup: true
});
expect(result).toHaveProperty('cleanupErrors');
});
});
describe('Memory Integration', () => {
it('should update workflow status to cancelled', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await cancelWorkflow({ workflowId: 'wf-001' });
// Verify memory update
const memoryKey = 'aqe/swarm/workflow-cli-commands/workflow-wf-001-status';
// Memory assertion would go here
});
it('should store cancellation metadata', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await cancelWorkflow({
workflowId: 'wf-001',
reason: 'User requested'
});
const metadataKey = 'aqe/swarm/workflow-cli-commands/cancel-metadata-wf-001';
// Memory assertion would go here
});
it('should clean up workflow memory on complete cancel', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
await cancelWorkflow({
workflowId: 'wf-001',
cleanup: true,
cleanMemory: true
});
// Verify memory cleanup
// Memory cleanup verification would go here
});
});
});
describe('Workflow Commands Integration', () => {
it('should list workflows after pause', async () => {
const { pauseWorkflow } = await import('../../src/cli/commands/workflow/pause.js');
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
await pauseWorkflow({ workflowId: 'wf-001' });
const result = await listWorkflows({ status: 'paused' });
expect(result.workflows.some((w: any) => w.id === 'wf-001')).toBe(true);
});
it('should not list cancelled workflows by default', async () => {
const { cancelWorkflow } = await import('../../src/cli/commands/workflow/cancel.js');
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
await cancelWorkflow({ workflowId: 'wf-001' });
const result = await listWorkflows({});
expect(result.workflows.every((w: any) => w.status !== 'cancelled')).toBe(true);
});
it('should track command execution in memory', async () => {
const { listWorkflows } = await import('../../src/cli/commands/workflow/list.js');
await listWorkflows({});
// Verify progress tracking
const progressKey = 'aqe/swarm/workflow-cli-commands/progress';
// Progress tracking verification would go here
});
});