mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
650 lines
20 KiB
TypeScript
650 lines
20 KiB
TypeScript
/**
|
|
* Integration Tests: Parallel Eval Runner
|
|
* ADR-056 Phase 5: Worker pool-based parallel evaluation
|
|
*
|
|
* These tests verify that:
|
|
* 1. Parallel execution is faster than sequential
|
|
* 2. Test cases are distributed evenly across workers
|
|
* 3. Worker failures are handled gracefully
|
|
* 4. Failed tests are retried when configured
|
|
* 5. Outcomes are recorded to SkillValidationLearner
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as yaml from 'yaml';
|
|
import {
|
|
ParallelEvalRunner,
|
|
createParallelEvalRunner,
|
|
ParallelEvalConfig,
|
|
EvalSuite,
|
|
LLMExecutor,
|
|
} from '../../../src/validation/parallel-eval-runner.js';
|
|
import {
|
|
SkillValidationLearner,
|
|
createSkillValidationLearner,
|
|
} from '../../../src/learning/skill-validation-learner.js';
|
|
import {
|
|
RealQEReasoningBank,
|
|
createRealQEReasoningBank,
|
|
} from '../../../src/learning/real-qe-reasoning-bank.js';
|
|
|
|
// ============================================================================
|
|
// Test Fixtures
|
|
// ============================================================================
|
|
|
|
const TEST_EVAL_SUITE: EvalSuite = {
|
|
skill: 'test-skill',
|
|
version: '1.0.0',
|
|
description: 'Test eval suite for integration testing',
|
|
models_to_test: ['claude-3.5-sonnet', 'claude-3-haiku'],
|
|
mcp_integration: {
|
|
enabled: true,
|
|
namespace: 'test-validation',
|
|
track_outcomes: true,
|
|
},
|
|
learning: {
|
|
store_success_patterns: true,
|
|
cross_model_comparison: true,
|
|
},
|
|
test_cases: [
|
|
{
|
|
id: 'tc001',
|
|
description: 'Test alt text detection',
|
|
category: 'perceivable',
|
|
priority: 'critical',
|
|
input: {
|
|
code: '<img src="test.jpg">',
|
|
context: { language: 'html', wcagLevel: 'AA' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['alt', '1.1.1', 'perceivable'],
|
|
must_not_contain: ['no issues'],
|
|
},
|
|
validation: {
|
|
keyword_match_threshold: 0.8,
|
|
reasoning_quality_min: 0.5,
|
|
},
|
|
},
|
|
{
|
|
id: 'tc002',
|
|
description: 'Test contrast detection',
|
|
category: 'perceivable',
|
|
priority: 'high',
|
|
input: {
|
|
code: '<p style="color: #777">Gray text</p>',
|
|
context: { language: 'html', wcagLevel: 'AA' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['contrast', '1.4.3'],
|
|
},
|
|
validation: {
|
|
keyword_match_threshold: 0.8,
|
|
},
|
|
},
|
|
{
|
|
id: 'tc003',
|
|
description: 'Test keyboard accessibility',
|
|
category: 'operable',
|
|
priority: 'critical',
|
|
input: {
|
|
code: '<div onclick="click()">Button</div>',
|
|
context: { language: 'html', wcagLevel: 'A' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['keyboard', '2.1.1', 'operable', 'button'],
|
|
},
|
|
validation: {
|
|
keyword_match_threshold: 0.9,
|
|
reasoning_quality_min: 0.6,
|
|
},
|
|
},
|
|
{
|
|
id: 'tc004',
|
|
description: 'Test form labels',
|
|
category: 'understandable',
|
|
priority: 'high',
|
|
input: {
|
|
code: '<input type="text" placeholder="Name">',
|
|
context: { language: 'html', wcagLevel: 'A' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['label', '3.3.2', 'understandable'],
|
|
},
|
|
},
|
|
{
|
|
id: 'tc005',
|
|
description: 'Test ARIA validation',
|
|
category: 'robust',
|
|
priority: 'medium',
|
|
input: {
|
|
code: '<button aria-label="">Submit</button>',
|
|
context: { language: 'html', wcagLevel: 'A' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['ARIA', '4.1.2', 'robust'],
|
|
},
|
|
},
|
|
{
|
|
id: 'tc006',
|
|
description: 'Test focus visibility',
|
|
category: 'operable',
|
|
priority: 'high',
|
|
input: {
|
|
code: 'button:focus { outline: none; }',
|
|
context: { language: 'css', wcagLevel: 'AA' },
|
|
},
|
|
expected_output: {
|
|
must_contain: ['focus', '2.4.7', 'outline'],
|
|
},
|
|
},
|
|
],
|
|
success_criteria: {
|
|
pass_rate: 0.8,
|
|
critical_pass_rate: 1.0,
|
|
avg_reasoning_quality: 0.5,
|
|
max_execution_time_ms: 60000,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Mock LLM executor that tracks execution times
|
|
*/
|
|
class TimedMockExecutor implements LLMExecutor {
|
|
public executionTimes: number[] = [];
|
|
public callCount = 0;
|
|
private delayMs: number;
|
|
|
|
constructor(delayMs = 100) {
|
|
this.delayMs = delayMs;
|
|
}
|
|
|
|
async execute(
|
|
prompt: string,
|
|
_model: string,
|
|
_options?: { timeout?: number }
|
|
): Promise<{ output: string; tokensUsed: number; durationMs: number }> {
|
|
const start = Date.now();
|
|
this.callCount++;
|
|
|
|
// Simulate LLM response time
|
|
await new Promise((resolve) => setTimeout(resolve, this.delayMs));
|
|
|
|
const duration = Date.now() - start;
|
|
this.executionTimes.push(duration);
|
|
|
|
// Generate response with keywords from prompt
|
|
const output = this.generateResponse(prompt);
|
|
|
|
return {
|
|
output,
|
|
tokensUsed: 500,
|
|
durationMs: duration,
|
|
};
|
|
}
|
|
|
|
private generateResponse(prompt: string): string {
|
|
const keywords: string[] = [];
|
|
|
|
if (prompt.includes('alt') || prompt.includes('img'))
|
|
keywords.push('alt', '1.1.1', 'perceivable');
|
|
if (prompt.includes('contrast') || prompt.includes('color'))
|
|
keywords.push('contrast', '1.4.3', '4.5:1');
|
|
if (prompt.includes('onclick') || prompt.includes('keyboard'))
|
|
keywords.push('keyboard', '2.1.1', 'operable', 'button');
|
|
if (prompt.includes('label') || prompt.includes('input'))
|
|
keywords.push('label', '3.3.2', 'understandable');
|
|
if (prompt.includes('aria') || prompt.includes('ARIA'))
|
|
keywords.push('ARIA', '4.1.2', 'robust');
|
|
if (prompt.includes('focus') || prompt.includes('outline'))
|
|
keywords.push('focus', '2.4.7', 'outline');
|
|
|
|
return `WCAG Analysis: Found issues related to ${keywords.join(', ')}.
|
|
Severity: serious
|
|
Remediation: Fix the ${keywords[0]} attribute.
|
|
This affects accessibility for users with disabilities.`;
|
|
}
|
|
|
|
reset(): void {
|
|
this.executionTimes = [];
|
|
this.callCount = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock LLM executor that fails on specific test cases
|
|
*/
|
|
class FailingMockExecutor implements LLMExecutor {
|
|
public failedCalls = 0;
|
|
public retryCalls = 0;
|
|
public totalCalls = 0;
|
|
private failOnFirst: boolean;
|
|
private hasFailed = false;
|
|
|
|
constructor(failOnFirst = true) {
|
|
this.failOnFirst = failOnFirst;
|
|
}
|
|
|
|
async execute(
|
|
prompt: string,
|
|
_model: string,
|
|
_options?: { timeout?: number }
|
|
): Promise<{ output: string; tokensUsed: number; durationMs: number }> {
|
|
this.totalCalls++;
|
|
|
|
// Fail only on first call if configured
|
|
if (this.failOnFirst && !this.hasFailed) {
|
|
this.hasFailed = true;
|
|
this.failedCalls++;
|
|
throw new Error('Simulated failure for first test case');
|
|
}
|
|
|
|
// Track retries
|
|
if (this.hasFailed && this.totalCalls === 2) {
|
|
this.retryCalls++;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
|
|
return {
|
|
output: `Analysis complete. WCAG issues found: alt, 1.1.1, perceivable, contrast, 1.4.3, keyboard, 2.1.1, operable, button, label, 3.3.2, understandable, ARIA, 4.1.2, robust, focus, 2.4.7, outline`,
|
|
tokensUsed: 500,
|
|
durationMs: 30,
|
|
};
|
|
}
|
|
|
|
reset(): void {
|
|
this.failedCalls = 0;
|
|
this.retryCalls = 0;
|
|
this.totalCalls = 0;
|
|
this.hasFailed = false;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Setup
|
|
// ============================================================================
|
|
|
|
describe('ParallelEvalRunner Integration Tests', () => {
|
|
let reasoningBank: RealQEReasoningBank;
|
|
let learner: SkillValidationLearner;
|
|
let testDir: string;
|
|
|
|
beforeEach(async () => {
|
|
// Create temp directory for test files
|
|
testDir = path.join(process.cwd(), '.test-evals-' + Date.now());
|
|
fs.mkdirSync(testDir, { recursive: true });
|
|
|
|
// Write test eval suite
|
|
const skillDir = path.join(testDir, 'test-skill', 'evals');
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(skillDir, 'test-skill.yaml'),
|
|
yaml.stringify(TEST_EVAL_SUITE)
|
|
);
|
|
|
|
// Initialize reasoning bank with test database
|
|
reasoningBank = await createRealQEReasoningBank({
|
|
persistencePath: path.join(testDir, 'test-memory.db'),
|
|
hnswConfig: {
|
|
m: 8,
|
|
efConstruction: 50,
|
|
efSearch: 25,
|
|
},
|
|
});
|
|
await reasoningBank.initialize();
|
|
|
|
// Create learner
|
|
learner = createSkillValidationLearner(reasoningBank);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Close reasoning bank
|
|
if (reasoningBank) {
|
|
await reasoningBank.dispose();
|
|
}
|
|
|
|
// Clean up test directory
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Test: Parallel execution is faster than sequential
|
|
// ==========================================================================
|
|
|
|
it('should run eval suite faster than sequential execution', async () => {
|
|
const executor = new TimedMockExecutor(50); // 50ms per test
|
|
|
|
// Run sequentially (1 worker)
|
|
const sequentialConfig: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 1,
|
|
batchSize: 1,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const sequentialRunner = createParallelEvalRunner(
|
|
learner,
|
|
sequentialConfig,
|
|
executor
|
|
);
|
|
|
|
const sequentialStart = Date.now();
|
|
const sequentialResult = await sequentialRunner.runEvalParallel(
|
|
'test-skill',
|
|
'claude-3.5-sonnet'
|
|
);
|
|
const sequentialDuration = Date.now() - sequentialStart;
|
|
|
|
// Reset executor
|
|
executor.reset();
|
|
|
|
// Run in parallel (3 workers)
|
|
const parallelConfig: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 3,
|
|
batchSize: 2,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const parallelRunner = createParallelEvalRunner(
|
|
learner,
|
|
parallelConfig,
|
|
executor
|
|
);
|
|
|
|
const parallelStart = Date.now();
|
|
const parallelResult = await parallelRunner.runEvalParallel(
|
|
'test-skill',
|
|
'claude-3.5-sonnet'
|
|
);
|
|
const parallelDuration = Date.now() - parallelStart;
|
|
|
|
// Both should have same number of tests
|
|
expect(parallelResult.totalTests).toBe(sequentialResult.totalTests);
|
|
expect(parallelResult.totalTests).toBe(6);
|
|
|
|
// Parallel should show some speedup factor
|
|
expect(parallelResult.parallelSpeedup).toBeGreaterThan(0);
|
|
|
|
// Results should be valid
|
|
expect(parallelResult.testResults.length).toBe(6);
|
|
expect(sequentialResult.testResults.length).toBe(6);
|
|
}, 30000);
|
|
|
|
// ==========================================================================
|
|
// Test: Test cases are distributed across workers
|
|
// ==========================================================================
|
|
|
|
it('should distribute test cases across workers', async () => {
|
|
const executor = new TimedMockExecutor(30);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 3,
|
|
batchSize: 2,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
const result = await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Should complete all tests
|
|
expect(result.totalTests).toBe(6);
|
|
expect(result.testResults.length).toBe(6);
|
|
|
|
// Workers used should be reflected
|
|
expect(result.workersUsed).toBe(3);
|
|
|
|
// Check worker status
|
|
const workerStatus = runner.getWorkerStatus();
|
|
expect(workerStatus.length).toBe(3);
|
|
}, 15000);
|
|
|
|
// ==========================================================================
|
|
// Test: Worker failures are handled gracefully
|
|
// ==========================================================================
|
|
|
|
it('should handle worker failures gracefully', async () => {
|
|
// Create executor that fails on first test
|
|
const executor = new FailingMockExecutor(true);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 1,
|
|
batchSize: 1,
|
|
retryFailedTests: false, // Disable retry
|
|
timeout: 5000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
const result = await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Should complete all tests even with failure
|
|
expect(result.totalTests).toBe(6);
|
|
|
|
// Should have recorded at least one failure
|
|
expect(result.failedTests).toBeGreaterThanOrEqual(1);
|
|
|
|
// Executor should have tracked the failure
|
|
expect(executor.failedCalls).toBeGreaterThanOrEqual(1);
|
|
}, 15000);
|
|
|
|
// ==========================================================================
|
|
// Test: Failed tests are retried when configured
|
|
// ==========================================================================
|
|
|
|
it('should retry failed tests when configured', async () => {
|
|
// Create executor that fails first time
|
|
const executor = new FailingMockExecutor(true);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 1,
|
|
batchSize: 1,
|
|
retryFailedTests: true, // Enable retry
|
|
timeout: 5000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
const result = await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Should have called executor more times due to retry (6 tests + at least 1 retry)
|
|
expect(executor.totalCalls).toBeGreaterThan(6);
|
|
|
|
// The first test may fail even with retry (depending on mock logic)
|
|
// but subsequent tests should pass
|
|
// With 6 tests total and first one potentially failing, we expect at least 3 passed
|
|
expect(result.passedTests).toBeGreaterThanOrEqual(3);
|
|
|
|
// Verify retry was triggered
|
|
expect(executor.failedCalls).toBeGreaterThanOrEqual(1);
|
|
}, 15000);
|
|
|
|
// ==========================================================================
|
|
// Test: Outcomes are recorded to SkillValidationLearner
|
|
// ==========================================================================
|
|
|
|
it('should record outcomes to SkillValidationLearner', async () => {
|
|
const executor = new TimedMockExecutor(30);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 2,
|
|
batchSize: 3,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
|
|
// Run eval
|
|
await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Check that confidence was recorded
|
|
const confidence = await learner.getSkillConfidence('test-skill');
|
|
|
|
expect(confidence).not.toBeNull();
|
|
expect(confidence!.skillName).toBe('test-skill');
|
|
expect(confidence!.outcomes.length).toBeGreaterThan(0);
|
|
expect(confidence!.avgScore).toBeGreaterThanOrEqual(0);
|
|
expect(confidence!.avgScore).toBeLessThanOrEqual(1);
|
|
}, 30000);
|
|
|
|
// ==========================================================================
|
|
// Test: Progress callback is called during execution
|
|
// ==========================================================================
|
|
|
|
it('should report progress during execution', async () => {
|
|
const executor = new TimedMockExecutor(100);
|
|
const progressReports: any[] = [];
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 2,
|
|
batchSize: 2,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
progressIntervalMs: 50, // Report every 50ms
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
|
|
runner.onProgress((progress) => {
|
|
progressReports.push({ ...progress });
|
|
});
|
|
|
|
await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Should have received progress reports
|
|
expect(progressReports.length).toBeGreaterThan(0);
|
|
|
|
// Progress should include expected fields
|
|
const lastProgress = progressReports[progressReports.length - 1];
|
|
expect(lastProgress.skill).toBe('test-skill');
|
|
expect(lastProgress.totalTasks).toBe(6);
|
|
expect(lastProgress.elapsedMs).toBeGreaterThan(0);
|
|
}, 15000);
|
|
|
|
// ==========================================================================
|
|
// Test: Multiple skill evals in parallel
|
|
// ==========================================================================
|
|
|
|
it('should run multiple skill evals in parallel', async () => {
|
|
// Create another test skill
|
|
const skill2Dir = path.join(testDir, 'test-skill-2', 'evals');
|
|
fs.mkdirSync(skill2Dir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(skill2Dir, 'test-skill-2.yaml'),
|
|
yaml.stringify({
|
|
...TEST_EVAL_SUITE,
|
|
skill: 'test-skill-2',
|
|
test_cases: TEST_EVAL_SUITE.test_cases.slice(0, 3), // Only 3 tests
|
|
})
|
|
);
|
|
|
|
const executor = new TimedMockExecutor(30);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 3,
|
|
batchSize: 2,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
|
|
const results = await runner.runMultipleEvalsParallel(
|
|
['test-skill', 'test-skill-2'],
|
|
['claude-3.5-sonnet']
|
|
);
|
|
|
|
// Should have results for both skills
|
|
expect(results.size).toBe(2);
|
|
expect(results.has('test-skill')).toBe(true);
|
|
expect(results.has('test-skill-2')).toBe(true);
|
|
|
|
// Each skill should have results
|
|
const skill1Results = results.get('test-skill')!;
|
|
expect(skill1Results.length).toBe(1);
|
|
expect(skill1Results[0].totalTests).toBe(6);
|
|
|
|
const skill2Results = results.get('test-skill-2')!;
|
|
expect(skill2Results.length).toBe(1);
|
|
expect(skill2Results[0].totalTests).toBe(3);
|
|
}, 30000);
|
|
|
|
// ==========================================================================
|
|
// Test: Measures and reports speedup correctly
|
|
// ==========================================================================
|
|
|
|
it('should measure and report parallel speedup correctly', async () => {
|
|
const executor = new TimedMockExecutor(50);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 3,
|
|
batchSize: 2,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
const result = await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
|
|
// Speedup should be positive
|
|
expect(result.parallelSpeedup).toBeGreaterThan(0);
|
|
|
|
// Duration should be recorded
|
|
expect(result.totalDurationMs).toBeGreaterThan(0);
|
|
|
|
// Results should be complete
|
|
expect(result.testResults.length).toBe(6);
|
|
}, 15000);
|
|
|
|
// ==========================================================================
|
|
// Test: Worker reset clears statistics
|
|
// ==========================================================================
|
|
|
|
it('should reset worker statistics between runs', async () => {
|
|
const executor = new TimedMockExecutor(30);
|
|
|
|
const config: Partial<ParallelEvalConfig> = {
|
|
maxWorkers: 2,
|
|
batchSize: 3,
|
|
retryFailedTests: false,
|
|
timeout: 10000,
|
|
skillsDir: testDir,
|
|
};
|
|
|
|
const runner = createParallelEvalRunner(learner, config, executor);
|
|
|
|
// First run
|
|
await runner.runEvalParallel('test-skill', 'claude-3.5-sonnet');
|
|
let status = runner.getWorkerStatus();
|
|
const firstRunTotal = status.reduce((sum, w) => sum + w.tasksCompleted, 0);
|
|
|
|
// Reset workers
|
|
runner.resetWorkers();
|
|
status = runner.getWorkerStatus();
|
|
const afterResetTotal = status.reduce((sum, w) => sum + w.tasksCompleted, 0);
|
|
|
|
expect(firstRunTotal).toBeGreaterThan(0);
|
|
expect(afterResetTotal).toBe(0);
|
|
|
|
// All workers should be idle
|
|
for (const worker of status) {
|
|
expect(worker.status).toBe('idle');
|
|
expect(worker.tasksCompleted).toBe(0);
|
|
expect(worker.tasksFailed).toBe(0);
|
|
}
|
|
}, 15000);
|
|
});
|