@@ -11518,6 +11518,237 @@ def test_unknown_steps_skipped(self):
1151811518 assert len (events ) == 2
1151911519
1152011520
11521+ class TestIsSandboxOnlyTurn :
11522+ """Tests for _is_sandbox_only_turn."""
11523+
11524+ def _make_event (self , * , author , part ):
11525+ return agentplatform_genai_types .evals .AgentEvent (
11526+ author = author ,
11527+ content = genai_types .Content (role = "model" , parts = [part ]),
11528+ )
11529+
11530+ def test_provision_sandbox_is_sandbox_only (self ):
11531+ events = [
11532+ self ._make_event (
11533+ author = "agent" ,
11534+ part = genai_types .Part (
11535+ function_call = genai_types .FunctionCall (
11536+ name = "provision_sandbox" , args = {}
11537+ )
11538+ ),
11539+ ),
11540+ self ._make_event (
11541+ author = "user" ,
11542+ part = genai_types .Part (
11543+ function_response = genai_types .FunctionResponse (
11544+ name = "provision_sandbox" , response = {"status" : "ready" }
11545+ )
11546+ ),
11547+ ),
11548+ ]
11549+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is True
11550+
11551+ def test_load_sandbox_is_sandbox_only (self ):
11552+ events = [
11553+ self ._make_event (
11554+ author = "agent" ,
11555+ part = genai_types .Part (
11556+ function_call = genai_types .FunctionCall (
11557+ name = "load_sandbox" , args = {}
11558+ )
11559+ ),
11560+ ),
11561+ ]
11562+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is True
11563+
11564+ def test_non_sandbox_tool_is_not_sandbox_only (self ):
11565+ events = [
11566+ self ._make_event (
11567+ author = "agent" ,
11568+ part = genai_types .Part (
11569+ function_call = genai_types .FunctionCall (
11570+ name = "run_command" , args = {}
11571+ )
11572+ ),
11573+ ),
11574+ ]
11575+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is False
11576+
11577+ def test_text_event_is_not_sandbox_only (self ):
11578+ events = [
11579+ self ._make_event (
11580+ author = "user" ,
11581+ part = genai_types .Part (text = "Hello" ),
11582+ ),
11583+ ]
11584+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is False
11585+
11586+ def test_empty_events_is_sandbox_only (self ):
11587+ assert _evals_builtin_tools .is_sandbox_only_turn ([]) is True
11588+
11589+
11590+ class TestSandboxTurnMergingInAgentData :
11591+ """Tests that sandbox-only turns are merged in _interaction_dict_to_agent_data."""
11592+
11593+ def test_sandbox_prefix_merged_into_single_turn (self ):
11594+ """Sandbox + 1 real interaction produces 1 turn, not 2."""
11595+ interaction_dict = {
11596+ "status" : "completed" ,
11597+ "steps" : [
11598+ # Sandbox provisioning (no user_input, creates Turn 0)
11599+ {
11600+ "type" : "function_call" ,
11601+ "name" : "provision_sandbox" ,
11602+ "arguments" : {"display_name" : "sb" },
11603+ "id" : "call_sb" ,
11604+ },
11605+ {
11606+ "type" : "function_result" ,
11607+ "name" : "provision_sandbox" ,
11608+ "call_id" : "call_sb" ,
11609+ "result" : {"status" : "ready" },
11610+ },
11611+ # Real user interaction (UserInputStep creates Turn 1)
11612+ {
11613+ "type" : "user_input" ,
11614+ "content" : [{"type" : "text" , "text" : "What is 2+2?" }],
11615+ },
11616+ {
11617+ "type" : "model_output" ,
11618+ "content" : [{"type" : "text" , "text" : "4" }],
11619+ },
11620+ ],
11621+ }
11622+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11623+ # Should produce 1 turn, not 2.
11624+ assert len (result .turns ) == 1
11625+ assert result .turns [0 ].turn_index == 0
11626+ # The turn should contain all events: sandbox + real.
11627+ events = result .turns [0 ].events
11628+ assert len (events ) == 4
11629+ # First two events are sandbox tool call/response.
11630+ assert events [0 ].content .parts [0 ].function_call .name == "provision_sandbox"
11631+ assert events [1 ].content .parts [0 ].function_response .name == "provision_sandbox"
11632+ # Last two are the real conversation.
11633+ assert events [2 ].content .parts [0 ].text == "What is 2+2?"
11634+ assert events [3 ].content .parts [0 ].text == "4"
11635+
11636+ def test_sandbox_prefix_with_multi_turn (self ):
11637+ """Sandbox + 2 real turns produces 2 turns, not 3."""
11638+ interaction_dict = {
11639+ "status" : "completed" ,
11640+ "steps" : [
11641+ {
11642+ "type" : "function_call" ,
11643+ "name" : "provision_sandbox" ,
11644+ "arguments" : {},
11645+ "id" : "call_sb" ,
11646+ },
11647+ {
11648+ "type" : "function_result" ,
11649+ "name" : "provision_sandbox" ,
11650+ "call_id" : "call_sb" ,
11651+ "result" : {"status" : "ready" },
11652+ },
11653+ {
11654+ "type" : "user_input" ,
11655+ "content" : [{"type" : "text" , "text" : "Turn 1" }],
11656+ },
11657+ {
11658+ "type" : "model_output" ,
11659+ "content" : [{"type" : "text" , "text" : "Reply 1" }],
11660+ },
11661+ {
11662+ "type" : "user_input" ,
11663+ "content" : [{"type" : "text" , "text" : "Turn 2" }],
11664+ },
11665+ {
11666+ "type" : "model_output" ,
11667+ "content" : [{"type" : "text" , "text" : "Reply 2" }],
11668+ },
11669+ ],
11670+ }
11671+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11672+ assert len (result .turns ) == 2
11673+ assert result .turns [0 ].events [0 ].content .parts [0 ].function_call .name == "provision_sandbox"
11674+ assert result .turns [0 ].events [- 1 ].content .parts [0 ].text == "Reply 1"
11675+ assert result .turns [1 ].events [0 ].content .parts [0 ].text == "Turn 2"
11676+
11677+ def test_no_sandbox_prefix_unchanged (self ):
11678+ """Interaction without sandbox steps is unaffected."""
11679+ interaction_dict = {
11680+ "status" : "completed" ,
11681+ "steps" : [
11682+ {
11683+ "type" : "user_input" ,
11684+ "content" : [{"type" : "text" , "text" : "Hello" }],
11685+ },
11686+ {
11687+ "type" : "model_output" ,
11688+ "content" : [{"type" : "text" , "text" : "Hi" }],
11689+ },
11690+ ],
11691+ }
11692+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11693+ assert len (result .turns ) == 1
11694+ assert len (result .turns [0 ].events ) == 2
11695+
11696+ def test_all_sandbox_only_not_discarded (self ):
11697+ """If every step is sandbox-only, it remains as 1 turn (not discarded)."""
11698+ interaction_dict = {
11699+ "status" : "completed" ,
11700+ "steps" : [
11701+ {
11702+ "type" : "function_call" ,
11703+ "name" : "provision_sandbox" ,
11704+ "arguments" : {},
11705+ "id" : "call_sb" ,
11706+ },
11707+ {
11708+ "type" : "function_result" ,
11709+ "name" : "provision_sandbox" ,
11710+ "call_id" : "call_sb" ,
11711+ "result" : {"status" : "ready" },
11712+ },
11713+ ],
11714+ }
11715+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11716+ # Only 1 group, and it's sandbox-only — but there's no second group
11717+ # to merge into, so it stays as-is.
11718+ assert len (result .turns ) == 1
11719+
11720+ def test_non_sandbox_tool_before_user_input_not_merged (self ):
11721+ """A non-sandbox tool call before user_input is NOT merged."""
11722+ interaction_dict = {
11723+ "status" : "completed" ,
11724+ "steps" : [
11725+ {
11726+ "type" : "function_call" ,
11727+ "name" : "run_command" ,
11728+ "arguments" : {"cmd" : "ls" },
11729+ "id" : "call_1" ,
11730+ },
11731+ {
11732+ "type" : "function_result" ,
11733+ "name" : "run_command" ,
11734+ "call_id" : "call_1" ,
11735+ "result" : {"output" : "file.txt" },
11736+ },
11737+ {
11738+ "type" : "user_input" ,
11739+ "content" : [{"type" : "text" , "text" : "Hello" }],
11740+ },
11741+ {
11742+ "type" : "model_output" ,
11743+ "content" : [{"type" : "text" , "text" : "Hi" }],
11744+ },
11745+ ],
11746+ }
11747+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11748+ # run_command is NOT a sandbox tool, so Turn 0 stays separate.
11749+ assert len (result .turns ) == 2
11750+
11751+
1152111752class TestMergeTextPartsInAgentData :
1152211753 """Tests for _merge_text_parts_in_agent_data."""
1152311754
0 commit comments