Files
sure/test/models/provider/openai/pdf_processor_test.rb
T
Ellion BlessanandClaude Sonnet 5 e69894adb9 fix(providers): capture API error response body in PDF processor span output (#2937)
* fix(providers): capture API error response body in PDF processor span output

Anthropic and OpenAI PDF processing errors only logged the exception
message, dropping the parsed response body that usually explains the
failure. Add safe_error_body to both providers' UsageRecorder concerns
and include it in the langfuse span output on failure, with tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SYp89xEYfTkrxw8HcCUQ8

* fix(providers): allowlist PDF processor error fields sent to Langfuse

safe_error_body forwarded the entire upstream error body into the
langfuse span output. For custom OpenAI-compatible providers/proxies
(and the analogous Anthropic path), that body can echo request
content from the financial document being processed. Replace it with
safe_error_detail, which extracts only type/message/code/request_id
instead of the raw body.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SYp89xEYfTkrxw8HcCUQ8

* test(openai): cover request_id extraction in PDF processor error_detail

The safe_error_detail request_id path (error.response_headers) had no
test coverage. Stub response_headers with x-request-id and assert it
appears in error_detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SYp89xEYfTkrxw8HcCUQ8

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 14:59:08 +02:00

86 lines
2.3 KiB
Ruby

require "test_helper"
class Provider::Openai::PdfProcessorTest < ActiveSupport::TestCase
setup do
@pdf_content = "%PDF-1.4 fake bytes".b
end
test "extracts only allowlisted error fields into span output when the API call fails" do
error = StandardError.new("boom")
def error.response_body
{
"error" => { "type" => "invalid_request_error", "message" => "invalid request", "code" => "bad_pdf" },
"request" => { "messages" => "statement text that should never leak" }
}
end
def error.response_headers
{ "x-request-id" => "req_abc123" }
end
captured_output = nil
trace = stub_trace { |output| captured_output = output }
assert_raises(StandardError) do
build_processor(error, trace).process
end
assert_equal(
{ type: "invalid_request_error", message: "invalid request", code: "bad_pdf", request_id: "req_abc123" },
captured_output[:error_detail]
)
end
test "error_detail is nil in span output when the error exposes no response_body" do
error = StandardError.new("boom")
captured_output = nil
trace = stub_trace { |output| captured_output = output }
assert_raises(StandardError) do
build_processor(error, trace).process
end
assert_nil captured_output[:error_detail]
end
test "error_detail falls back to a placeholder when reading response_body itself raises" do
error = StandardError.new("boom")
def error.response_body
raise "response_body accessor exploded"
end
captured_output = nil
trace = stub_trace { |output| captured_output = output }
assert_raises(StandardError) do
build_processor(error, trace).process
end
assert_match(/detail unavailable/i, captured_output[:error_detail])
end
private
def build_processor(error, trace)
client = mock
client.expects(:chat).raises(error)
processor = Provider::Openai::PdfProcessor.new(
client,
model: "gpt-4.1",
pdf_content: @pdf_content,
langfuse_trace: trace,
max_response_tokens: 1000
)
processor.stubs(:extract_text_from_pdf).returns("Statement text")
processor
end
def stub_trace
span = mock
span.expects(:end).with { |args| yield(args[:output]); true }
trace = mock
trace.stubs(:span).returns(span)
trace
end
end