import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import { runOmpTurn } from '../src/main/agent/omp-turn' import type { OmpKernel } from '../src/main/omp' import type { ChatEvent } from '../src/shared/types' class Kernel extends EventEmitter { abortCalls = 0 disposeCalls = 0 rejectAbort = false async prompt(): Promise<{ type: string; success: boolean }> { return { type: 'response', success: true } } async abort(): Promise { this.abortCalls++; if (this.rejectAbort) throw new Error('abort unavailable') } async dispose(): Promise { this.disposeCalls++ } } function begin(kernel = new Kernel(), controller = new AbortController()) { const events: ChatEvent[] = [] const outcome = runOmpTurn({ kernel: kernel as unknown as OmpKernel, runId: 'provider-error', userText: 'fixture', signal: controller.signal, emit: event => events.push(event) }) .then(value => ({ value, error: undefined }), (error: Error) => ({ value: undefined, error })) return { kernel, controller, events, outcome } } const errorMessage = { role: 'assistant', stopReason: 'error', content: [], errorMessage: 'Unable to connect. Is the computer able to access the url?' } const emit = (kernel: Kernel, frame: Record): void => { kernel.emit('event', frame) } const delay = (): Promise => new Promise(resolve => setTimeout(resolve, 15)) { const test = begin() emit(test.kernel, { type: 'message_end', message: errorMessage }) const result = await test.outcome assert.equal(result.error?.message, errorMessage.errorMessage) assert.equal(test.kernel.abortCalls, 1) assert.equal(test.events.filter(event => event.type === 'error').length, 1) emit(test.kernel, { type: 'agent_end', yielded: false, messages: [errorMessage] }) emit(test.kernel, { type: 'agent_end', yielded: true }) assert.equal(test.events.filter(event => ['done', 'cancelled'].includes(event.type)).length, 0) assert.equal(test.kernel.listenerCount('event'), 0) assert.equal(test.kernel.listenerCount('error'), 0) } { const test = begin() emit(test.kernel, { type: 'agent_end', yielded: false, messages: [{ role: 'user' }, errorMessage] }) assert.equal((await test.outcome).error?.message, errorMessage.errorMessage) assert.equal(test.kernel.abortCalls, 1) } { const test = begin() emit(test.kernel, { type: 'message_end', message: { ...errorMessage, errorMessage: 'Failed https://account:secret@example.invalid/v1?key=url-secret API Key: field-secret Authorization: Bearer bearer-secret sk-plainsecret123' } }) const message = (await test.outcome).error!.message for (const secret of ['account', 'secret@example', 'url-secret', 'field-secret', 'bearer-secret', 'plainsecret123']) assert.equal(message.includes(secret), false) assert.match(message, /已隐藏/) } { const test = begin() test.controller.abort() emit(test.kernel, { type: 'message_end', message: errorMessage }) assert.equal((await test.outcome).value?.status, 'cancelled') assert.equal(test.events.some(event => event.type === 'error'), false) assert.equal(test.events.filter(event => event.type === 'cancelled').length, 1) assert.equal(test.kernel.abortCalls, 1) } { const test = begin() let ended = false void test.outcome.then(() => { ended = true }) // There is no first-byte deadline: long thinking and tool-level failures remain live. emit(test.kernel, { type: 'message_update', assistantMessageEvent: { type: 'thinking_delta', delta: 'thinking', contentIndex: 0 } }) emit(test.kernel, { type: 'message_end', message: { ...errorMessage, role: 'toolResult' } }) emit(test.kernel, { type: 'tool_execution_start', toolCallId: 'tool', toolName: 'shell', args: {} }) emit(test.kernel, { type: 'tool_execution_end', toolCallId: 'tool', toolName: 'shell', isError: true, result: { content: [{ type: 'text', text: 'command failed' }] } }) emit(test.kernel, { type: 'agent_end', yielded: false, messages: [{ role: 'assistant', stopReason: 'toolUse' }] }) await delay() assert.equal(ended, false) assert.equal(test.kernel.abortCalls, 0) assert.equal(test.events.some(event => event.type === 'error'), false) emit(test.kernel, { type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: 'recovered' } }) emit(test.kernel, { type: 'agent_end', yielded: true, messages: [errorMessage, { role: 'assistant', stopReason: 'stop' }] }) const result = (await test.outcome).value! assert.equal(result.status, 'done') assert.equal(result.content, 'recovered') assert.equal(result.tools[0].status, 'error') } { const test = begin() test.kernel.rejectAbort = true emit(test.kernel, { type: 'message_end', message: { role: 'assistant', stopReason: 'error' } }) assert.equal((await test.outcome).error?.message, 'OMP 模型服务请求失败') await delay() assert.equal(test.kernel.disposeCalls, 1, 'failed abort disposes the sidecar instead of leaving automatic retries running') } console.log('omp-provider-error-regression: provider error, retry stop, sanitization, cancellation, long thinking, and tool error isolation passed (protocol fixtures; no real upstream request)')