mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
3d7a2f91dd
Security (CodeQL #193-196): - command-validator: replace greedy .* with [^)]* in $() pattern - input-sanitizer: replace .* with \S+(?:\.\S+)* in UPDATE SET pattern - path-traversal-validator: split alternation into two .replace() calls - regex-safety-validator: use [^\]]* and non-greedy variants in detection patterns Flaky test indicators (5 files, 58 real setTimeout calls eliminated): - file-watcher.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - qcsd-refinement.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - qcsd-ideation-url.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync - server-client.test.ts: process.nextTick for data callbacks - stream-controller.test.ts: vi.useFakeTimers + advanceTimersByTimeAsync File size discipline (issue #374 P2): - time-crystal.ts (1,714 → 6 files, max 447 lines): time-crystal-types, phase-executor, time-crystal-persistence, time-crystal-analysis, time-crystal-scheduling, time-crystal (controller) - qe-reasoning-bank.ts (1,941 → 7 files, max 738 lines): qe-reasoning-bank-types, pretrained-patterns, agent-routing, embedding-utils, pattern-promotion, qe-reasoning-bank (class) All re-exports preserved for backward compatibility. 536 directly affected tests pass. Build clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
679 lines
25 KiB
TypeScript
679 lines
25 KiB
TypeScript
/**
|
|
* QCSD Refinement Swarm Integration Tests
|
|
*
|
|
* Verifies the complete refinement pipeline:
|
|
* 1. SFDIPOT product factor analysis (7 factors, 37 subcategories)
|
|
* 2. BDD scenario generation (Given/When/Then)
|
|
* 3. INVEST requirements validation
|
|
* 4. Contract validation (conditional - HAS_API)
|
|
* 5. Impact analysis (conditional - HAS_REFACTORING)
|
|
* 6. Dependency mapping (conditional - HAS_DEPENDENCIES)
|
|
* 7. Test idea rewriting (transformation)
|
|
* 8. Cross-phase signal consumption
|
|
* 9. Workflow orchestrator integration
|
|
* 10. Error handling
|
|
*
|
|
* These tests use mock memory to ensure deterministic behavior.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
QCSDRefinementPlugin,
|
|
SFDIPOTAssessment,
|
|
BDDScenarioSet,
|
|
RefinementReport,
|
|
} from '../../../src/domains/requirements-validation/qcsd-refinement-plugin';
|
|
import {
|
|
WorkflowOrchestrator,
|
|
WorkflowDefinition,
|
|
} from '../../../src/coordination/workflow-orchestrator';
|
|
import { InMemoryEventBus } from '../../../src/kernel/event-bus';
|
|
import { DefaultAgentCoordinator } from '../../../src/kernel/agent-coordinator';
|
|
import { MemoryBackend, StoreOptions, EventBus, AgentCoordinator } from '../../../src/kernel/interfaces';
|
|
import { DomainName } from '../../../src/shared/types';
|
|
|
|
// ============================================================================
|
|
// Mock Memory Backend
|
|
// ============================================================================
|
|
|
|
function createMockMemory(): MemoryBackend {
|
|
const store = new Map<string, unknown>();
|
|
|
|
return {
|
|
async initialize() {},
|
|
async dispose() {},
|
|
async set<T>(key: string, value: T, _options?: StoreOptions): Promise<void> {
|
|
store.set(key, value);
|
|
},
|
|
async get<T>(key: string): Promise<T | undefined> {
|
|
return store.get(key) as T | undefined;
|
|
},
|
|
async delete(key: string): Promise<boolean> {
|
|
return store.delete(key);
|
|
},
|
|
async has(key: string): Promise<boolean> {
|
|
return store.has(key);
|
|
},
|
|
async search(pattern: string, _limit?: number): Promise<string[]> {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
return Array.from(store.keys()).filter((k) => regex.test(k));
|
|
},
|
|
async vectorSearch(_embedding: number[], _k: number): Promise<[]> {
|
|
return [];
|
|
},
|
|
async storeVector(
|
|
_key: string,
|
|
_embedding: number[],
|
|
_metadata?: unknown
|
|
): Promise<void> {},
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Workflow Definitions
|
|
// ============================================================================
|
|
|
|
function createSFDIPOTTestWorkflow(): WorkflowDefinition {
|
|
return {
|
|
id: 'sfdipot-test',
|
|
name: 'SFDIPOT Analysis Test Workflow',
|
|
description: 'Test workflow for SFDIPOT product factor analysis',
|
|
version: '1.0.0',
|
|
steps: [
|
|
{
|
|
id: 'analyze-sfdipot',
|
|
name: 'Analyze SFDIPOT Factors',
|
|
domain: 'requirements-validation' as DomainName,
|
|
action: 'analyzeSFDIPOT',
|
|
inputMapping: {
|
|
targetId: 'input.targetId',
|
|
description: 'input.description',
|
|
acceptanceCriteria: 'input.acceptanceCriteria',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function createBDDTestWorkflow(): WorkflowDefinition {
|
|
return {
|
|
id: 'bdd-test',
|
|
name: 'BDD Generation Test Workflow',
|
|
description: 'Test workflow for BDD scenario generation',
|
|
version: '1.0.0',
|
|
steps: [
|
|
{
|
|
id: 'generate-bdd',
|
|
name: 'Generate BDD Scenarios',
|
|
domain: 'requirements-validation' as DomainName,
|
|
action: 'generateBDDScenarios',
|
|
inputMapping: {
|
|
targetId: 'input.targetId',
|
|
description: 'input.description',
|
|
acceptanceCriteria: 'input.acceptanceCriteria',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function createRequirementsTestWorkflow(): WorkflowDefinition {
|
|
return {
|
|
id: 'requirements-test',
|
|
name: 'Requirements Validation Test Workflow',
|
|
description: 'Test workflow for INVEST requirements validation',
|
|
version: '1.0.0',
|
|
steps: [
|
|
{
|
|
id: 'validate-requirements',
|
|
name: 'Validate Requirements',
|
|
domain: 'requirements-validation' as DomainName,
|
|
action: 'validateRefinementRequirements',
|
|
inputMapping: {
|
|
targetId: 'input.targetId',
|
|
description: 'input.description',
|
|
acceptanceCriteria: 'input.acceptanceCriteria',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function createCrossPhaseTestWorkflow(): WorkflowDefinition {
|
|
return {
|
|
id: 'cross-phase-test',
|
|
name: 'Cross-Phase Signals Test Workflow',
|
|
description: 'Test workflow for cross-phase signal consumption',
|
|
version: '1.0.0',
|
|
steps: [
|
|
{
|
|
id: 'consume-signals',
|
|
name: 'Consume Cross-Phase Signals',
|
|
domain: 'requirements-validation' as DomainName,
|
|
action: 'consumeCrossPhaseSignals',
|
|
inputMapping: {
|
|
targetId: 'input.targetId',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('QCSD Refinement Swarm Integration', () => {
|
|
let memory: MemoryBackend;
|
|
let eventBus: EventBus;
|
|
let agentCoordinator: AgentCoordinator;
|
|
let orchestrator: WorkflowOrchestrator;
|
|
let plugin: QCSDRefinementPlugin;
|
|
|
|
beforeEach(async () => {
|
|
memory = createMockMemory();
|
|
eventBus = new InMemoryEventBus();
|
|
agentCoordinator = new DefaultAgentCoordinator();
|
|
orchestrator = new WorkflowOrchestrator(eventBus, memory, agentCoordinator, {
|
|
maxConcurrentWorkflows: 10,
|
|
defaultStepTimeout: 60000,
|
|
defaultWorkflowTimeout: 600000,
|
|
enableEventTriggers: false,
|
|
persistExecutions: false,
|
|
});
|
|
|
|
plugin = new QCSDRefinementPlugin(memory);
|
|
await plugin.initialize();
|
|
await orchestrator.initialize();
|
|
plugin.registerWorkflowActions(orchestrator);
|
|
|
|
// Register test workflows
|
|
orchestrator.registerWorkflow(createSFDIPOTTestWorkflow());
|
|
orchestrator.registerWorkflow(createBDDTestWorkflow());
|
|
orchestrator.registerWorkflow(createRequirementsTestWorkflow());
|
|
orchestrator.registerWorkflow(createCrossPhaseTestWorkflow());
|
|
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.useRealTimers();
|
|
await orchestrator.dispose();
|
|
await plugin.dispose();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Action Registration Tests
|
|
// ============================================================================
|
|
|
|
describe('action registration', () => {
|
|
const expectedActions: Array<{ domain: DomainName; action: string }> = [
|
|
{ domain: 'requirements-validation' as DomainName, action: 'analyzeSFDIPOT' },
|
|
{ domain: 'requirements-validation' as DomainName, action: 'generateBDDScenarios' },
|
|
{ domain: 'requirements-validation' as DomainName, action: 'validateRefinementRequirements' },
|
|
{ domain: 'requirements-validation' as DomainName, action: 'generateRefinementReport' },
|
|
{ domain: 'requirements-validation' as DomainName, action: 'consumeCrossPhaseSignals' },
|
|
{ domain: 'code-intelligence' as DomainName, action: 'mapDependencies' },
|
|
{ domain: 'code-intelligence' as DomainName, action: 'analyzeImpact' },
|
|
{ domain: 'contract-testing' as DomainName, action: 'validateContracts' },
|
|
{ domain: 'test-generation' as DomainName, action: 'rewriteTestIdeas' },
|
|
{ domain: 'learning-optimization' as DomainName, action: 'storeRefinementLearnings' },
|
|
];
|
|
|
|
for (const { domain, action } of expectedActions) {
|
|
it(`should have ${domain}/${action} action registered`, () => {
|
|
const isRegistered = orchestrator.isActionRegistered(domain, action);
|
|
expect(isRegistered).toBe(true);
|
|
});
|
|
}
|
|
|
|
it('should list all refinement actions for requirements-validation domain', () => {
|
|
const actions = orchestrator.getRegisteredActions('requirements-validation' as DomainName);
|
|
expect(actions).toContain('analyzeSFDIPOT');
|
|
expect(actions).toContain('generateBDDScenarios');
|
|
expect(actions).toContain('validateRefinementRequirements');
|
|
expect(actions).toContain('generateRefinementReport');
|
|
expect(actions).toContain('consumeCrossPhaseSignals');
|
|
});
|
|
|
|
it('should register actions in code-intelligence domain', () => {
|
|
const actions = orchestrator.getRegisteredActions('code-intelligence' as DomainName);
|
|
expect(actions).toContain('mapDependencies');
|
|
expect(actions).toContain('analyzeImpact');
|
|
});
|
|
|
|
it('should register actions in contract-testing domain', () => {
|
|
const actions = orchestrator.getRegisteredActions('contract-testing' as DomainName);
|
|
expect(actions).toContain('validateContracts');
|
|
});
|
|
|
|
it('should register actions in test-generation domain', () => {
|
|
const actions = orchestrator.getRegisteredActions('test-generation' as DomainName);
|
|
expect(actions).toContain('rewriteTestIdeas');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// SFDIPOT Analysis Tests
|
|
// ============================================================================
|
|
|
|
describe('SFDIPOT analysis via workflow execution', () => {
|
|
it('should analyze all 7 SFDIPOT factors for a story', async () => {
|
|
const result = await orchestrator.executeWorkflow('sfdipot-test', {
|
|
targetId: 'STORY-001',
|
|
description: 'Build a REST API endpoint for user authentication with OAuth2 support. The API should integrate with existing database layer and support horizontal scaling.',
|
|
acceptanceCriteria: [
|
|
'Given a valid OAuth2 token, when the user requests /api/auth/profile, then return user profile data',
|
|
'Given an expired token, when the user makes any API request, then return 401 Unauthorized',
|
|
'Given concurrent requests, when load exceeds 1000 RPS, then response time stays under 200ms',
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('analyze-sfdipot');
|
|
expect(stepResult).toBeDefined();
|
|
expect(stepResult?.status).toBe('completed');
|
|
|
|
const assessment = stepResult?.output as SFDIPOTAssessment;
|
|
expect(assessment.factors).toBeDefined();
|
|
expect(assessment.factors.length).toBe(7);
|
|
expect(assessment.overallScore).toBeGreaterThanOrEqual(0);
|
|
expect(assessment.overallScore).toBeLessThanOrEqual(100);
|
|
|
|
// Verify all 7 factors are present
|
|
const factorNames = assessment.factors.map(f => f.factor);
|
|
expect(factorNames).toContain('structure');
|
|
expect(factorNames).toContain('function');
|
|
expect(factorNames).toContain('data');
|
|
expect(factorNames).toContain('interfaces');
|
|
expect(factorNames).toContain('platform');
|
|
expect(factorNames).toContain('operations');
|
|
expect(factorNames).toContain('time');
|
|
});
|
|
|
|
it('should detect high relevance for API-related factors', async () => {
|
|
const result = await orchestrator.executeWorkflow('sfdipot-test', {
|
|
targetId: 'STORY-002',
|
|
description: 'Create REST API endpoint with GraphQL integration and webhook notifications for downstream services',
|
|
acceptanceCriteria: ['API returns JSON responses', 'Webhooks fire within 5 seconds'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('analyze-sfdipot');
|
|
const assessment = stepResult?.output as SFDIPOTAssessment;
|
|
|
|
// Interfaces factor should have high weight due to API/GraphQL/webhook keywords
|
|
const interfacesFactor = assessment.factors.find(f => f.factor === 'interfaces');
|
|
expect(interfacesFactor).toBeDefined();
|
|
expect(interfacesFactor!.weight).toBeGreaterThanOrEqual(5);
|
|
});
|
|
|
|
it('should store SFDIPOT results in memory', async () => {
|
|
const result = await orchestrator.executeWorkflow('sfdipot-test', {
|
|
targetId: 'STORY-MEM-001',
|
|
description: 'Simple feature with data processing',
|
|
acceptanceCriteria: ['Data is processed correctly'],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
// Verify data stored in memory
|
|
const stored = await memory.get<unknown>('qcsd-refinement:sfdipot:STORY-MEM-001');
|
|
expect(stored).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// BDD Generation Tests
|
|
// ============================================================================
|
|
|
|
describe('BDD scenario generation via workflow execution', () => {
|
|
it('should generate BDD scenarios from story description', async () => {
|
|
const result = await orchestrator.executeWorkflow('bdd-test', {
|
|
targetId: 'STORY-BDD-001',
|
|
description: 'User login with email and password authentication',
|
|
acceptanceCriteria: [
|
|
'Given a registered user, when they enter valid credentials, then they are logged in',
|
|
'Given invalid credentials, when login is attempted, then show error message',
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('generate-bdd');
|
|
expect(stepResult).toBeDefined();
|
|
expect(stepResult?.status).toBe('completed');
|
|
|
|
const scenarios = stepResult?.output as BDDScenarioSet;
|
|
expect(scenarios.features).toBeDefined();
|
|
expect(scenarios.features.length).toBeGreaterThan(0);
|
|
expect(scenarios.totalScenarios).toBeGreaterThan(0);
|
|
|
|
// Verify coverage breakdown exists
|
|
expect(scenarios.coverage).toBeDefined();
|
|
expect(scenarios.coverage.happyPath).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should generate scenarios covering multiple types', async () => {
|
|
const result = await orchestrator.executeWorkflow('bdd-test', {
|
|
targetId: 'STORY-BDD-002',
|
|
description: 'Payment processing with credit card validation and error handling for expired cards and security fraud detection',
|
|
acceptanceCriteria: [
|
|
'Given a valid credit card, when payment is submitted, then charge is processed',
|
|
'Given an expired card, when payment is attempted, then show expiry error',
|
|
'Given a fraudulent transaction, when detected, then block and notify security team',
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('generate-bdd');
|
|
const scenarios = stepResult?.output as BDDScenarioSet;
|
|
|
|
// Should have scenarios of multiple types
|
|
const allScenarios = scenarios.features.flatMap(f => f.scenarios);
|
|
const types = new Set(allScenarios.map(s => s.type));
|
|
expect(types.size).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Requirements Validation Tests
|
|
// ============================================================================
|
|
|
|
describe('requirements validation via workflow execution', () => {
|
|
it('should validate requirements with INVEST criteria', async () => {
|
|
const result = await orchestrator.executeWorkflow('requirements-test', {
|
|
targetId: 'STORY-REQ-001',
|
|
description: 'As a user, I want to reset my password so that I can regain access to my account',
|
|
acceptanceCriteria: [
|
|
'Given a registered email, when reset is requested, then send reset link',
|
|
'Given a valid reset token, when new password is set, then update credentials',
|
|
'Given an expired token, when reset is attempted, then show expiry message',
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('validate-requirements');
|
|
expect(stepResult).toBeDefined();
|
|
expect(stepResult?.status).toBe('completed');
|
|
|
|
const validation = stepResult?.output as { valid: boolean; investScore: number; completeness: number; gaps: string[]; suggestions: string[] };
|
|
expect(validation.investScore).toBeGreaterThanOrEqual(0);
|
|
expect(validation.investScore).toBeLessThanOrEqual(100);
|
|
expect(validation.completeness).toBeGreaterThanOrEqual(0);
|
|
expect(validation.completeness).toBeLessThanOrEqual(100);
|
|
expect(Array.isArray(validation.gaps)).toBe(true);
|
|
});
|
|
|
|
it('should identify gaps in incomplete requirements', async () => {
|
|
const result = await orchestrator.executeWorkflow('requirements-test', {
|
|
targetId: 'STORY-REQ-002',
|
|
description: 'Make things better',
|
|
acceptanceCriteria: [],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('validate-requirements');
|
|
const validation = stepResult?.output as { valid: boolean; investScore: number; completeness: number; gaps: string[]; suggestions: string[] };
|
|
|
|
// Incomplete requirements should have gaps
|
|
expect(validation.gaps.length).toBeGreaterThan(0);
|
|
expect(validation.completeness).toBeLessThan(80);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cross-Phase Signal Tests
|
|
// ============================================================================
|
|
|
|
describe('cross-phase signal consumption', () => {
|
|
it('should consume tactical signals from memory', async () => {
|
|
// Pre-populate memory with cross-phase signals
|
|
await memory.set('cross-phase:tactical:defect-pattern-001', {
|
|
factor: 'interfaces',
|
|
pattern: 'API timeout errors',
|
|
weight: 0.8,
|
|
});
|
|
|
|
await memory.set('cross-phase:quality-criteria:testability-issue-001', {
|
|
pattern: 'Untestable async workflows',
|
|
recommendation: 'Add explicit wait conditions',
|
|
});
|
|
|
|
const result = await orchestrator.executeWorkflow('cross-phase-test', {
|
|
targetId: 'STORY-XP-001',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('consume-signals');
|
|
expect(stepResult).toBeDefined();
|
|
expect(stepResult?.status).toBe('completed');
|
|
|
|
const signals = stepResult?.output as { tacticalSignals: unknown[]; qualitySignals: unknown[]; insights: string[] };
|
|
expect(signals).toBeDefined();
|
|
expect(Array.isArray(signals.insights)).toBe(true);
|
|
});
|
|
|
|
it('should handle empty cross-phase signals gracefully', async () => {
|
|
const result = await orchestrator.executeWorkflow('cross-phase-test', {
|
|
targetId: 'STORY-XP-002',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
const stepResult = status?.stepResults?.get('consume-signals');
|
|
const signals = stepResult?.output as { tacticalSignals: unknown[]; qualitySignals: unknown[]; insights: string[] };
|
|
expect(signals.tacticalSignals).toEqual([]);
|
|
expect(signals.qualitySignals).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Error Handling Tests
|
|
// ============================================================================
|
|
|
|
describe('error handling', () => {
|
|
it('should handle missing targetId gracefully', async () => {
|
|
const result = await orchestrator.executeWorkflow('sfdipot-test', {
|
|
description: 'A story without targetId',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
// Should still complete (using fallback ID)
|
|
expect(status?.status).toBe('completed');
|
|
});
|
|
|
|
it('should handle empty description gracefully', async () => {
|
|
const result = await orchestrator.executeWorkflow('bdd-test', {
|
|
targetId: 'STORY-ERR-001',
|
|
description: '',
|
|
acceptanceCriteria: [],
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
const executionId = result.value!;
|
|
let status = orchestrator.getWorkflowStatus(executionId);
|
|
let attempts = 0;
|
|
while (status?.status === 'running' && attempts < 50) {
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
status = orchestrator.getWorkflowStatus(executionId);
|
|
attempts++;
|
|
}
|
|
|
|
expect(status?.status).toBe('completed');
|
|
|
|
// Should generate minimal scenarios even with empty input
|
|
const stepResult = status?.stepResults?.get('generate-bdd');
|
|
const scenarios = stepResult?.output as BDDScenarioSet;
|
|
expect(scenarios.features).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Plugin Lifecycle Tests
|
|
// ============================================================================
|
|
|
|
describe('QCSD Refinement Plugin Lifecycle', () => {
|
|
it('should throw if registerWorkflowActions called before initialize', async () => {
|
|
const memory = createMockMemory();
|
|
const eventBus = new InMemoryEventBus();
|
|
const agentCoordinator = new DefaultAgentCoordinator();
|
|
const orchestrator = new WorkflowOrchestrator(eventBus, memory, agentCoordinator, {
|
|
maxConcurrentWorkflows: 10,
|
|
defaultStepTimeout: 60000,
|
|
defaultWorkflowTimeout: 600000,
|
|
enableEventTriggers: false,
|
|
persistExecutions: false,
|
|
});
|
|
await orchestrator.initialize();
|
|
|
|
const plugin = new QCSDRefinementPlugin(memory);
|
|
// NOT calling plugin.initialize()
|
|
|
|
expect(() => plugin.registerWorkflowActions(orchestrator)).toThrow(
|
|
'QCSDRefinementPlugin must be initialized'
|
|
);
|
|
|
|
await orchestrator.dispose();
|
|
});
|
|
|
|
it('should allow multiple initialize calls (idempotent)', async () => {
|
|
const memory = createMockMemory();
|
|
const plugin = new QCSDRefinementPlugin(memory);
|
|
|
|
await plugin.initialize();
|
|
await plugin.initialize(); // Should not throw
|
|
await plugin.initialize(); // Should not throw
|
|
|
|
await plugin.dispose();
|
|
});
|
|
|
|
it('should clean up on dispose', async () => {
|
|
const memory = createMockMemory();
|
|
const plugin = new QCSDRefinementPlugin(memory);
|
|
|
|
await plugin.initialize();
|
|
await plugin.dispose();
|
|
|
|
// Should be able to re-initialize after dispose
|
|
await plugin.initialize();
|
|
await plugin.dispose();
|
|
});
|
|
});
|