Files
magpie/tests/orchestrator/orchestrator-resilience.test.ts
Li Liu 629ed8b00e feat: add --fail-fast option to abort review/discuss on any reviewer failure
By default the orchestrator is resilient: a single reviewer (or context
gatherer) failure is logged and the round continues with the survivors,
aborting only when all reviewers fail.

The new --fail-fast flag flips to strict mode — any reviewer or
context-gathering failure re-throws immediately and terminates the
whole flow. Wired through the review and discuss commands via
OrchestratorOptions.failFast, with a regression test and README docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:30:06 -07:00

89 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { DebateOrchestrator } from '../../src/orchestrator/orchestrator.js'
import type { AIProvider } from '../../src/providers/types.js'
import type { Reviewer } from '../../src/orchestrator/types.js'
function makeProvider(name: string, response: string): AIProvider {
return {
name,
async chat() { return response },
async *chatStream() { yield response },
}
}
function makeFailingProvider(name: string): AIProvider {
return {
name,
async chat() { throw new Error(`${name} crashed`) },
async *chatStream() { throw new Error(`${name} crashed`) },
}
}
function makeReviewer(id: string, provider: AIProvider): Reviewer {
return { id, provider, systemPrompt: 'Review the code.' }
}
describe('DebateOrchestrator resilience', () => {
it('should complete review when one reviewer fails in streaming mode', async () => {
const goodProvider = makeProvider('good', 'LGTM, no issues found.')
const badProvider = makeFailingProvider('bad')
const reviewers = [
makeReviewer('good-reviewer', goodProvider),
makeReviewer('bad-reviewer', badProvider),
]
const summarizer = makeReviewer('summarizer', makeProvider('sum', 'Final conclusion.'))
const analyzer = makeReviewer('analyzer', makeProvider('analyzer', 'Analysis done.'))
const orchestrator = new DebateOrchestrator(reviewers, summarizer, analyzer, {
maxRounds: 1,
interactive: false,
checkConvergence: false,
})
const result = await orchestrator.runStreaming('test', 'Review this code')
expect(result.finalConclusion).toBeTruthy()
expect(result.messages.some(m => m.reviewerId === 'good-reviewer')).toBe(true)
})
it('should fail if ALL reviewers fail', async () => {
const reviewers = [
makeReviewer('bad-1', makeFailingProvider('bad1')),
makeReviewer('bad-2', makeFailingProvider('bad2')),
]
const summarizer = makeReviewer('summarizer', makeProvider('sum', 'Final conclusion.'))
const analyzer = makeReviewer('analyzer', makeProvider('analyzer', 'Analysis done.'))
const orchestrator = new DebateOrchestrator(reviewers, summarizer, analyzer, {
maxRounds: 1,
interactive: false,
checkConvergence: false,
})
await expect(orchestrator.runStreaming('test', 'Review this code'))
.rejects.toThrow('All reviewers failed')
})
it('should abort immediately when failFast is enabled and any reviewer fails', async () => {
const goodProvider = makeProvider('good', 'LGTM, no issues found.')
const badProvider = makeFailingProvider('bad')
const reviewers = [
makeReviewer('good-reviewer', goodProvider),
makeReviewer('bad-reviewer', badProvider),
]
const summarizer = makeReviewer('summarizer', makeProvider('sum', 'Final conclusion.'))
const analyzer = makeReviewer('analyzer', makeProvider('analyzer', 'Analysis done.'))
const orchestrator = new DebateOrchestrator(reviewers, summarizer, analyzer, {
maxRounds: 1,
interactive: false,
checkConvergence: false,
failFast: true,
})
await expect(orchestrator.runStreaming('test', 'Review this code'))
.rejects.toThrow(/bad-reviewer.*fail-fast/)
})
})