From b26306d807e02c91ca0fe229f5a81bbc9b671882 Mon Sep 17 00:00:00 2001 From: Guillem Arias Date: Tue, 26 May 2026 10:39:15 +0200 Subject: [PATCH] test(ai): add Anthropic tool_use round-trip + multi-tool turn coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses @jjmata's "worth confirming" note on PR #1983: tool-use turns from prior assistant messages must round-trip correctly when retrieved from the database. - New `ChatParser → ToolCall::Function → MessageFormatter` test walks the full path: Anthropic response with a tool_use block → ChatFunctionRequest → ToolCall::Function.from_function_request → persisted on the AssistantMessage → MessageFormatter rebuild on the next turn. Asserts the original `tool_use.id` is preserved end-to-end as both `tool_use.id` and the paired `tool_result.tool_use_id`, and that the original `input` hash and serialized result content survive. - New multi-tool assistant turn test confirms two tool_use blocks on a single assistant message render as two tool_use blocks followed by two paired tool_result blocks in a single user-role follow-up, matching Anthropic's required alternation. Both tests exercise the existing PR1 code without behavior changes. --- .../anthropic/message_formatter_test.rb | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/models/provider/anthropic/message_formatter_test.rb b/test/models/provider/anthropic/message_formatter_test.rb index 9b4b8914d..73a7f9b42 100644 --- a/test/models/provider/anthropic/message_formatter_test.rb +++ b/test/models/provider/anthropic/message_formatter_test.rb @@ -85,6 +85,74 @@ class Provider::Anthropic::MessageFormatterTest < ActiveSupport::TestCase assert_includes messages[2][:content].first[:content], "99" end + # Confirms the round-trip flagged in PR #1983 review: an Anthropic tool_use + # block returned by the model → ChatFunctionRequest → ToolCall::Function + # persisted on the AssistantMessage → MessageFormatter rebuild on the next + # turn produces an Anthropic-compatible history where tool_use_id pairs back + # to the original block. + test "ChatParser → ToolCall::Function → MessageFormatter round-trips tool_use_id" do + anthropic_response = OpenStruct.new( + id: "msg_abc", + model: "claude-sonnet-4-6", + content: [ + OpenStruct.new(type: :tool_use, id: "toolu_round_trip", name: "get_net_worth", input: { "currency" => "USD" }) + ] + ) + + parsed = Provider::Anthropic::ChatParser.new(anthropic_response).parsed + function_request = parsed.function_requests.first + + persisted_tool_call = ToolCall::Function.from_function_request( + function_request, + { "amount" => 12345, "currency" => "USD" } + ) + + assistant = stub_assistant_message("Your net worth is $12,345.", tool_calls: [ persisted_tool_call ]) + history = [ stub_user_message("net worth?"), assistant ] + + rebuilt = Provider::Anthropic::MessageFormatter.new(prompt: "follow-up", conversation_history: history).build + + tool_use_block = rebuilt[1][:content].find { |b| b[:type] == "tool_use" } + tool_result_block = rebuilt[2][:content].first + + assert_equal "toolu_round_trip", tool_use_block[:id] + assert_equal "toolu_round_trip", tool_result_block[:tool_use_id] + assert_equal({ "currency" => "USD" }, tool_use_block[:input]) + assert_equal({ "amount" => 12345, "currency" => "USD" }.to_json, tool_result_block[:content]) + end + + test "renders multi-tool assistant turn with all pairings preserved" do + tool_a = stub_tool_call( + id: "toolu_a", + name: "get_accounts", + arguments: {}, + result: [ { "id" => 1, "name" => "Checking" } ] + ) + tool_b = stub_tool_call( + id: "toolu_b", + name: "get_holdings", + arguments: {}, + result: [ { "ticker" => "VTI", "qty" => 10 } ] + ) + assistant = stub_assistant_message("Looked up your accounts and holdings.", tool_calls: [ tool_a, tool_b ]) + + messages = Provider::Anthropic::MessageFormatter.new( + prompt: "follow-up", + conversation_history: [ stub_user_message("accounts and holdings?"), assistant ] + ).build + + tool_uses = messages[1][:content].select { |b| b[:type] == "tool_use" } + tool_results = messages[2][:content] + + assert_equal 2, tool_uses.size + assert_equal 2, tool_results.size + assert_equal [ "toolu_a", "toolu_b" ], tool_uses.map { |b| b[:id] } + assert_equal [ "toolu_a", "toolu_b" ], tool_results.map { |b| b[:tool_use_id] } + # Anthropic requires the user turn to follow the assistant turn that used tools + assert_equal "assistant", messages[1][:role] + assert_equal "user", messages[2][:role] + end + test "parses string arguments and nil outputs gracefully" do formatter = Provider::Anthropic::MessageFormatter.new( prompt: "go",