Skip to content

Commit 0bdba86

Browse files
committed
test(providers): pin that a structured provider error survives the error-body bound
1 parent 78a4a23 commit 0bdba86

1 file changed

Lines changed: 45 additions & 2 deletions

File tree

apps/sim/providers/openai/core.transport-phase.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
1111
import { executeResponsesProviderRequest } from '@/providers/openai/core'
1212
import type { ProviderRequest } from '@/providers/types'
1313

14+
const { mockSupportsReasoningEffort } = vi.hoisted(() => ({
15+
mockSupportsReasoningEffort: vi.fn(() => false),
16+
}))
17+
1418
vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 }))
1519

1620
vi.mock('@/providers/utils', () => ({
@@ -26,7 +30,7 @@ vi.mock('@/providers/utils', () => ({
2630
hasFilteredTools: false,
2731
}),
2832
trackForcedToolUsage: () => ({ hasUsedForcedTool: false, usedForcedTools: [] }),
29-
supportsReasoningEffort: () => false,
33+
supportsReasoningEffort: mockSupportsReasoningEffort,
3034
}))
3135

3236
vi.mock('@/tools', () => ({ executeTool: vi.fn() }))
@@ -51,7 +55,10 @@ const COMPLETED = {
5155
describe('OpenAI transport phase annotation', () => {
5256
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as never
5357

54-
beforeEach(() => vi.clearAllMocks())
58+
beforeEach(() => {
59+
vi.clearAllMocks()
60+
mockSupportsReasoningEffort.mockReturnValue(false)
61+
})
5562

5663
function run(fetchMock: unknown, request: Partial<ProviderRequest> = {}) {
5764
return executeResponsesProviderRequest(
@@ -136,6 +143,42 @@ describe('OpenAI transport phase annotation', () => {
136143
expect(error.message.length).toBeLessThan(700)
137144
})
138145

146+
/**
147+
* The bound applies only to non-JSON bodies. A structured provider error must survive
148+
* intact, because the reasoning-summary strip-and-retry fallback matches on its text
149+
* (`message.includes('reasoning.summary')`) — truncating it would silently disable
150+
* that recovery path for any provider whose error message runs long.
151+
*/
152+
it('does not truncate a structured provider error, so the summary fallback still matches', async () => {
153+
// Markers deliberately placed beyond the 500-char bound so that truncating a
154+
// structured error would drop them and the fallback would stop matching.
155+
const longMessage = `${'context detail. '.repeat(40)}Invalid value for reasoning.summary: your organization must be verified to use this feature.`
156+
expect(longMessage.indexOf('reasoning.summary')).toBeGreaterThan(500)
157+
158+
const completed = {
159+
ok: true,
160+
status: 200,
161+
headers: new Headers(),
162+
json: () => Promise.resolve(COMPLETED),
163+
}
164+
const verificationError = {
165+
ok: false,
166+
status: 400,
167+
headers: new Headers(),
168+
text: () => Promise.resolve(JSON.stringify({ error: { message: longMessage } })),
169+
}
170+
const fetchMock = vi
171+
.fn()
172+
.mockResolvedValueOnce(verificationError)
173+
.mockResolvedValueOnce(completed)
174+
// The fallback only applies when the payload actually carried reasoning.summary.
175+
mockSupportsReasoningEffort.mockReturnValue(true)
176+
177+
await expect(run(fetchMock, { agentEvents: true })).resolves.toMatchObject({ content: 'ok' })
178+
// Matched the verification error and retried without the summary, rather than failing.
179+
expect(fetchMock).toHaveBeenCalledTimes(2)
180+
})
181+
139182
it('leaves a healthy response entirely unaffected', async () => {
140183
const fetchMock = vi.fn().mockResolvedValue({
141184
ok: true,

0 commit comments

Comments
 (0)