import { describe, expect, it } from 'vitest'; import { classifyRuntimeDiagnostic, selectRuntimeDiagnosticClassification, } from '../../../../src/main/services/team/runtime/RuntimeDiagnosticClassifier'; describe('RuntimeDiagnosticClassifier', () => { it('selects disk-full errors over aborted and empty OpenCode noise', () => { const selected = selectRuntimeDiagnosticClassification([ 'Latest assistant message msg_1 failed with MessageAbortedError - Aborted', 'empty_assistant_turn', "OpenCode message bridge failed: ENOSPC: no space left on device, open '/tmp/.auth.json.tmp'", ]); expect(selected).toMatchObject({ reasonCode: 'filesystem_error', normalizedMessage: 'Local disk is full (ENOSPC). Free disk space and retry OpenCode delivery.', actionRequired: true, generic: false, }); }); it('selects quota errors over empty assistant turns', () => { const selected = selectRuntimeDiagnosticClassification([ 'empty_assistant_turn', 'Latest assistant message msg_2 failed with APIError - Insufficient credits. Add more using https://openrouter.ai/settings/credits', ]); expect(selected).toMatchObject({ reasonCode: 'quota_exhausted', normalizedMessage: 'Insufficient credits. Add more using https://openrouter.ai/settings/credits', actionRequired: true, }); }); it('classifies OpenCode free usage retry status as quota exhausted', () => { const selected = selectRuntimeDiagnosticClassification([ 'empty_assistant_turn', 'OpenCode session status retry - attempt=1 - Free usage exceeded, subscribe to Go https://opencode.ai/go - next=2026-05-18T00:00:00.267Z', ]); expect(selected).toMatchObject({ reasonCode: 'quota_exhausted', normalizedMessage: 'OpenCode session status retry - attempt=1 - Free usage exceeded, subscribe to Go https://opencode.ai/go - next=2026-05-18T00:00:00.267Z', actionRequired: true, }); }); it('selects auth errors over bridge timeouts', () => { const selected = selectRuntimeDiagnosticClassification([ 'OpenCode bridge command timed out', 'authentication_failed: invalid API key', ]); expect(selected).toMatchObject({ reasonCode: 'auth_error', normalizedMessage: 'authentication_failed: invalid API key', actionRequired: true, }); }); it('classifies OpenCode bridge outcome timeouts as backend delivery state', () => { expect( classifyRuntimeDiagnostic('opencode_prompt_acceptance_unknown_after_bridge_timeout') ).toMatchObject({ reasonCode: 'backend_error', normalizedMessage: 'OpenCode bridge outcome unknown after timeout, retrying/observing.', generic: true, actionRequired: false, }); }); it('keeps pure empty assistant turns as generic backend fallback', () => { expect(classifyRuntimeDiagnostic('empty_assistant_turn')).toMatchObject({ reasonCode: 'backend_error', normalizedMessage: 'empty_assistant_turn', generic: true, actionRequired: false, }); }); it('keeps protocol proof failures above generic runtime noise', () => { const selected = selectRuntimeDiagnosticClassification([ 'OpenCode bridge command timed out', 'visible_reply_missing_task_refs', ]); expect(selected).toMatchObject({ reasonCode: 'protocol_proof_missing', normalizedMessage: 'visible_reply_missing_task_refs', generic: true, actionRequired: false, }); }); it('does not classify message_send Not connected as protocol proof missing', () => { expect( classifyRuntimeDiagnostic( 'agent-teams_message_send returned Not connected while sending a visible reply' ) ).toMatchObject({ reasonCode: 'backend_error', actionRequired: false, }); }); it('keeps explicit proof-missing diagnostics narrow', () => { expect( classifyRuntimeDiagnostic( 'OpenCode used tools, but did not create a visible reply or task progress proof.' ) ).toMatchObject({ reasonCode: 'protocol_proof_missing', generic: true, }); }); it('keeps quota and auth diagnostics above proof-missing substrings in the same message', () => { expect( classifyRuntimeDiagnostic( 'Insufficient credits: OpenCode used tools, but did not create a visible reply or task progress proof.' ) ).toMatchObject({ reasonCode: 'quota_exhausted', actionRequired: true, }); expect( classifyRuntimeDiagnostic( 'authentication_failed: visible_reply_missing_task_refs because API key is invalid' ) ).toMatchObject({ reasonCode: 'auth_error', actionRequired: true, }); }); it('keeps OpenCode bridge command timeout as backend state despite timeout tokens', () => { expect(classifyRuntimeDiagnostic('OpenCode bridge command timed out')).toMatchObject({ reasonCode: 'backend_error', generic: true, }); }); });