Files
hermes-agent/apps/desktop/src/lib/error-surface.test.ts
T
Teknium 334bcbac93 fix(desktop): error card honors the classifier's retry verdict + failing-session identity (review feedback)
Addresses @helix4u's review on #91493:
- conversation_loop now stamps failure_retryable (the real ClassifiedError
  verdict) next to failure_reason; error_surface prefers it and only falls
  back to the reason set for older results. Fallback set corrected to match
  classify_api_error (auth, format_error, billing_unverified now
  non-retryable).
- The descriptor carries the failing session's provider/model captured at
  classification time; Copy error details prefers them over the foreground
  composer atoms.
- Open logs is labeled 'Open Desktop logs' on remote/cloud connections —
  the local folder holds transport logs, not the remote runtime's.
- API-exception module allowlist widened to botocore/boto3/google/grpc/
  requests/aiohttp so other adapter SDKs don't misclassify as gateway.
2026-08-21 15:24:03 -07:00

87 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { formatErrorDiagnostics, parseErrorSurface } from './error-surface'
describe('parseErrorSurface', () => {
it('accepts a valid descriptor', () => {
expect(parseErrorSurface({ layer: 'streaming', code: 'stream_drop', retryable: true })).toEqual({
layer: 'streaming',
code: 'stream_drop',
retryable: true
})
})
it('accepts every documented layer', () => {
for (const layer of ['provider', 'endpoint', 'streaming', 'auth', 'billing', 'gateway', 'runtime', 'disk']) {
expect(parseErrorSurface({ layer, code: 'x', retryable: false })?.layer).toBe(layer)
}
})
it('rejects unknown layers and non-objects', () => {
expect(parseErrorSurface({ layer: 'blockchain', code: 'x', retryable: true })).toBeNull()
expect(parseErrorSurface('provider')).toBeNull()
expect(parseErrorSurface(null)).toBeNull()
expect(parseErrorSurface(undefined)).toBeNull()
expect(parseErrorSurface(7)).toBeNull()
})
it('defaults code and retryable when missing', () => {
expect(parseErrorSurface({ layer: 'gateway' })).toEqual({ layer: 'gateway', code: 'unknown', retryable: true })
})
it('honors retryable=false', () => {
expect(parseErrorSurface({ layer: 'auth', code: 'auth_permanent', retryable: false })?.retryable).toBe(false)
})
it('carries the failing session identity when present', () => {
const surface = parseErrorSurface({
layer: 'provider',
code: 'rate_limit',
retryable: true,
provider: 'openrouter',
model: 'test/m1'
})
expect(surface?.provider).toBe('openrouter')
expect(surface?.model).toBe('test/m1')
// Absent identity yields no keys, not empty strings.
expect(parseErrorSurface({ layer: 'provider', code: 'x', retryable: true })?.provider).toBeUndefined()
})
})
describe('formatErrorDiagnostics', () => {
it('includes layer, code, model and error', () => {
const text = formatErrorDiagnostics({
errorText: 'boom',
model: 'anthropic/claude-opus-4.6',
surface: { layer: 'provider', code: 'rate_limit', retryable: true }
})
expect(text).toContain('layer: provider')
expect(text).toContain('code: rate_limit')
expect(text).toContain('model: anthropic/claude-opus-4.6')
expect(text).toContain('error: boom')
})
it('prefers the descriptor identity over the caller fallback', () => {
const text = formatErrorDiagnostics({
errorText: 'boom',
// Foreground composer atom — potentially stale by click time.
model: 'some/other-model',
surface: { layer: 'provider', code: 'rate_limit', retryable: true, provider: 'openrouter', model: 'failed/model' }
})
expect(text).toContain('provider: openrouter')
expect(text).toContain('model: failed/model')
expect(text).not.toContain('some/other-model')
})
it('omits absent fields without leaving blank lines', () => {
const text = formatErrorDiagnostics({ errorText: 'boom' })
expect(text).not.toContain('layer:')
expect(text).not.toContain('model:')
expect(text.split('\n').every(line => line.trim().length > 0)).toBe(true)
})
})