mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
247 lines
9.3 KiB
TypeScript
247 lines
9.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
MAX_PROVIDER_DIAGNOSTIC_CHARS,
|
|
providerDiagnostic,
|
|
providerDiagnosticOf,
|
|
agentSessionFailureFact,
|
|
readAgentSessionFailureFact,
|
|
readProviderRetry,
|
|
readWholeAgentSessionFailureFact,
|
|
withProviderDiagnostic
|
|
} from './agent-session-failure'
|
|
import { AgentSessionFailureFactSchema } from './agent-session-failure-fact-schema'
|
|
import { AGENT_SESSION_REFUSAL_REASONS } from './agent-session-refusal-details'
|
|
|
|
describe('provider diagnostics', () => {
|
|
it('bounds the text and records nothing for an empty one', () => {
|
|
expect(providerDiagnostic(' ', 'log')).toBeUndefined()
|
|
expect(
|
|
providerDiagnostic('x'.repeat(MAX_PROVIDER_DIAGNOSTIC_CHARS + 50), 'log')?.text
|
|
).toHaveLength(MAX_PROVIDER_DIAGNOSTIC_CHARS)
|
|
})
|
|
|
|
it('is read from the error that carries it, or from what it wraps', () => {
|
|
const diagnostic = { text: 'bad request', audience: 'person' as const }
|
|
const carried = withProviderDiagnostic(
|
|
new Error('codex turn/start failed: bad request'),
|
|
diagnostic
|
|
)
|
|
expect(providerDiagnosticOf(carried)).toEqual(diagnostic)
|
|
expect(providerDiagnosticOf(new Error('wrapped', { cause: carried }))).toEqual(diagnostic)
|
|
expect(providerDiagnosticOf(new AggregateError([new Error('other'), carried], 'both'))).toEqual(
|
|
diagnostic
|
|
)
|
|
})
|
|
|
|
it('ends on an aggregate error that contains itself', () => {
|
|
const loop = new AggregateError([], 'loop')
|
|
loop.errors.push(loop)
|
|
expect(providerDiagnosticOf(loop)).toBeUndefined()
|
|
})
|
|
|
|
it('is never inferred from an error that did not carry one', () => {
|
|
// Orca's own wording, even when it quotes something that looks like a provider message.
|
|
expect(
|
|
providerDiagnosticOf(new Error('claude stream-json exited (code 1): not signed in'))
|
|
).toBe(undefined)
|
|
expect(providerDiagnosticOf('a string')).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('reading a failure fact', () => {
|
|
it('keeps what this build can place', () => {
|
|
expect(
|
|
readAgentSessionFailureFact({
|
|
kind: 'restartFailed',
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'claimConflicted' } },
|
|
detail: { text: 'x', audience: 'person' }
|
|
})
|
|
).toEqual({
|
|
kind: 'restartFailed',
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'claimConflicted' } },
|
|
detail: { text: 'x', audience: 'person' }
|
|
})
|
|
})
|
|
|
|
it('retains safe argument details through schema admission and falls back for unknown details', () => {
|
|
const fact = {
|
|
kind: 'startFailed',
|
|
argumentProblem: { agent: 'Codex', option: '--remote', problem: 'unsupportedOption' }
|
|
}
|
|
expect(readWholeAgentSessionFailureFact(AgentSessionFailureFactSchema.parse(fact))).toEqual(
|
|
fact
|
|
)
|
|
expect(readWholeAgentSessionFailureFact({ kind: 'startFailed' })).toEqual({
|
|
kind: 'startFailed'
|
|
})
|
|
for (const argumentProblem of [
|
|
{ agent: 'Future', option: '--remote', problem: 'unsupportedOption' },
|
|
{ agent: 'Codex', option: '--remote', problem: 'futureProblem' },
|
|
{ agent: 'Codex', option: '--remote=private', problem: 'unsupportedOption' }
|
|
]) {
|
|
expect(
|
|
readWholeAgentSessionFailureFact({ kind: 'startFailed', argumentProblem })
|
|
).toBeUndefined()
|
|
expect(readAgentSessionFailureFact({ kind: 'startFailed', argumentProblem })).toEqual({
|
|
kind: 'startFailed'
|
|
})
|
|
}
|
|
})
|
|
|
|
it('keeps a timed-out request through schema admission and drops one it cannot show', () => {
|
|
const fact = {
|
|
kind: 'startFailed',
|
|
requestTimeout: { method: 'thread/start', timeoutMs: 30_000 }
|
|
}
|
|
expect(readWholeAgentSessionFailureFact(AgentSessionFailureFactSchema.parse(fact))).toEqual(
|
|
fact
|
|
)
|
|
for (const requestTimeout of [
|
|
{ method: 'thread/start because of <b>', timeoutMs: 30_000 },
|
|
{ method: 'thread/start', timeoutMs: -1 },
|
|
{ method: 'thread/start' }
|
|
]) {
|
|
expect(
|
|
readWholeAgentSessionFailureFact({ kind: 'startFailed', requestTimeout })
|
|
).toBeUndefined()
|
|
expect(readAgentSessionFailureFact({ kind: 'startFailed', requestTimeout })).toEqual({
|
|
kind: 'startFailed'
|
|
})
|
|
}
|
|
})
|
|
|
|
it('keeps what a provider said it is retrying, and only that', () => {
|
|
expect(
|
|
readAgentSessionFailureFact({
|
|
kind: 'providerRetrying',
|
|
retry: { error: 'rate_limit', status: 429, retryDelayMs: 600 }
|
|
})
|
|
).toEqual({ kind: 'providerRetrying', retry: { error: 'rate_limit', status: 429 } })
|
|
expect(
|
|
readAgentSessionFailureFact({ kind: 'providerRetrying', retry: { error: '', status: 'x' } })
|
|
).toEqual({ kind: 'providerRetrying' })
|
|
// The provider's own account of what failed survives a read, bounded like any detail.
|
|
expect(
|
|
readAgentSessionFailureFact({
|
|
kind: 'providerRetrying',
|
|
retry: { cause: ` stream disconnected${' x'.repeat(400)}` }
|
|
})?.retry?.cause
|
|
).toBe(`stream disconnected${' x'.repeat(400)}`.slice(0, 512).trim())
|
|
expect(
|
|
readAgentSessionFailureFact({ kind: 'providerRetrying', retry: { cause: ' ' } })
|
|
).toEqual({ kind: 'providerRetrying' })
|
|
})
|
|
|
|
it('reads a row an unreleased build wrote with a cause as a refusal with no details', () => {
|
|
expect(
|
|
readAgentSessionFailureFact({
|
|
kind: 'restartFailed',
|
|
refusal: { code: 'agent_session_conflict', cause: 'claimConflicted' }
|
|
})
|
|
).toEqual({ kind: 'restartFailed', refusal: { code: 'agent_session_conflict' } })
|
|
})
|
|
|
|
it('drops what a newer host wrote that this build cannot place', () => {
|
|
expect(readAgentSessionFailureFact({ kind: 'futureKind' })).toBeUndefined()
|
|
expect(readAgentSessionFailureFact(undefined)).toBeUndefined()
|
|
expect(
|
|
readAgentSessionFailureFact({
|
|
kind: 'restartFailed',
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'futureReason' } },
|
|
detail: { text: 'x', audience: 'future' }
|
|
})
|
|
).toEqual({ kind: 'restartFailed', refusal: { code: 'agent_session_conflict' } })
|
|
})
|
|
|
|
it('keeps which retry it is, and its maximum only where it bounds that retry', () => {
|
|
expect(readProviderRetry({ attempt: 3, maxRetries: 10 })).toEqual({
|
|
attempt: 3,
|
|
maxRetries: 10
|
|
})
|
|
expect(readProviderRetry({ status: 502, maxRetries: 10 })).toEqual({ status: 502 })
|
|
expect(readProviderRetry({ attempt: 12, maxRetries: 10 })).toEqual({ attempt: 12 })
|
|
expect(readProviderRetry({ attempt: 0, maxRetries: 10 })).toBeUndefined()
|
|
expect(readProviderRetry({ attempt: 2.5, status: 502 })).toEqual({ status: 502 })
|
|
})
|
|
})
|
|
|
|
describe('reading all of a failure fact', () => {
|
|
it('reads every part of a fact the host built, as it arrives off the wire', () => {
|
|
for (const { fact, expected } of [
|
|
{
|
|
fact: agentSessionFailureFact('startFailed', {
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'claimConflicted' } }
|
|
}),
|
|
expected: {
|
|
kind: 'startFailed',
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'claimConflicted' } }
|
|
}
|
|
},
|
|
{
|
|
fact: agentSessionFailureFact('providerRejected', {
|
|
detail: providerDiagnostic('Image type .bmp', 'person')
|
|
}),
|
|
expected: {
|
|
kind: 'providerRejected',
|
|
detail: { text: 'Image type .bmp', audience: 'person' }
|
|
}
|
|
},
|
|
{
|
|
fact: agentSessionFailureFact('attachmentInvalid', {
|
|
attachment: { reason: 'tooLarge', limit: 5 * 1024 * 1024 }
|
|
}),
|
|
expected: {
|
|
kind: 'attachmentInvalid',
|
|
attachment: { reason: 'tooLarge', limit: 5 * 1024 * 1024 }
|
|
}
|
|
},
|
|
{
|
|
fact: agentSessionFailureFact('providerRetrying', {
|
|
retry: readProviderRetry({ error: 'rate_limit', status: 429 })
|
|
}),
|
|
expected: { kind: 'providerRetrying', retry: { error: 'rate_limit', status: 429 } }
|
|
},
|
|
{
|
|
fact: agentSessionFailureFact('providerRetrying', {
|
|
retry: readProviderRetry({
|
|
error: 'server_error',
|
|
status: 502,
|
|
attempt: 3,
|
|
maxRetries: 10
|
|
})
|
|
}),
|
|
expected: {
|
|
kind: 'providerRetrying',
|
|
retry: { error: 'server_error', status: 502, attempt: 3, maxRetries: 10 }
|
|
}
|
|
}
|
|
]) {
|
|
expect(fact).toEqual(expected)
|
|
expect(readWholeAgentSessionFailureFact(JSON.parse(JSON.stringify(fact)))).toEqual(expected)
|
|
}
|
|
// Every reason a refusal can name, on the code that names it.
|
|
for (const [code, reasons] of Object.entries(AGENT_SESSION_REFUSAL_REASONS)) {
|
|
for (const reason of reasons) {
|
|
const fact = { kind: 'startFailed', refusal: { code, details: { reason } } }
|
|
expect(readWholeAgentSessionFailureFact(fact)).toEqual(fact)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('reads nothing when this build would drop any part, however deep', () => {
|
|
for (const value of [
|
|
{ kind: 'futureKind' },
|
|
{ kind: 'startFailed', refusal: { code: 'agent_session_future_code' } },
|
|
{
|
|
kind: 'startFailed',
|
|
refusal: { code: 'agent_session_conflict', details: { reason: 'futureReason' } }
|
|
},
|
|
{ kind: 'attachmentInvalid', attachment: { reason: 'futureReason' } },
|
|
{ kind: 'providerRejected', detail: { text: 'x', audience: 'future' } },
|
|
{ kind: 'startFailed', futurePart: {} }
|
|
]) {
|
|
expect(readWholeAgentSessionFailureFact(value)).toBeUndefined()
|
|
}
|
|
})
|
|
})
|