@@ -11384,6 +11384,237 @@ def test_unknown_steps_skipped(self):
1138411384 assert len (events ) == 2
1138511385
1138611386
11387+ class TestIsSandboxOnlyTurn :
11388+ """Tests for _is_sandbox_only_turn."""
11389+
11390+ def _make_event (self , * , author , part ):
11391+ return agentplatform_genai_types .evals .AgentEvent (
11392+ author = author ,
11393+ content = genai_types .Content (role = "model" , parts = [part ]),
11394+ )
11395+
11396+ def test_provision_sandbox_is_sandbox_only (self ):
11397+ events = [
11398+ self ._make_event (
11399+ author = "agent" ,
11400+ part = genai_types .Part (
11401+ function_call = genai_types .FunctionCall (
11402+ name = "provision_sandbox" , args = {}
11403+ )
11404+ ),
11405+ ),
11406+ self ._make_event (
11407+ author = "user" ,
11408+ part = genai_types .Part (
11409+ function_response = genai_types .FunctionResponse (
11410+ name = "provision_sandbox" , response = {"status" : "ready" }
11411+ )
11412+ ),
11413+ ),
11414+ ]
11415+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is True
11416+
11417+ def test_load_sandbox_is_sandbox_only (self ):
11418+ events = [
11419+ self ._make_event (
11420+ author = "agent" ,
11421+ part = genai_types .Part (
11422+ function_call = genai_types .FunctionCall (
11423+ name = "load_sandbox" , args = {}
11424+ )
11425+ ),
11426+ ),
11427+ ]
11428+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is True
11429+
11430+ def test_non_sandbox_tool_is_not_sandbox_only (self ):
11431+ events = [
11432+ self ._make_event (
11433+ author = "agent" ,
11434+ part = genai_types .Part (
11435+ function_call = genai_types .FunctionCall (
11436+ name = "run_command" , args = {}
11437+ )
11438+ ),
11439+ ),
11440+ ]
11441+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is False
11442+
11443+ def test_text_event_is_not_sandbox_only (self ):
11444+ events = [
11445+ self ._make_event (
11446+ author = "user" ,
11447+ part = genai_types .Part (text = "Hello" ),
11448+ ),
11449+ ]
11450+ assert _evals_builtin_tools .is_sandbox_only_turn (events ) is False
11451+
11452+ def test_empty_events_is_sandbox_only (self ):
11453+ assert _evals_builtin_tools .is_sandbox_only_turn ([]) is True
11454+
11455+
11456+ class TestSandboxTurnMergingInAgentData :
11457+ """Tests that sandbox-only turns are merged in _interaction_dict_to_agent_data."""
11458+
11459+ def test_sandbox_prefix_merged_into_single_turn (self ):
11460+ """Sandbox + 1 real interaction produces 1 turn, not 2."""
11461+ interaction_dict = {
11462+ "status" : "completed" ,
11463+ "steps" : [
11464+ # Sandbox provisioning (no user_input, creates Turn 0)
11465+ {
11466+ "type" : "function_call" ,
11467+ "name" : "provision_sandbox" ,
11468+ "arguments" : {"display_name" : "sb" },
11469+ "id" : "call_sb" ,
11470+ },
11471+ {
11472+ "type" : "function_result" ,
11473+ "name" : "provision_sandbox" ,
11474+ "call_id" : "call_sb" ,
11475+ "result" : {"status" : "ready" },
11476+ },
11477+ # Real user interaction (UserInputStep creates Turn 1)
11478+ {
11479+ "type" : "user_input" ,
11480+ "content" : [{"type" : "text" , "text" : "What is 2+2?" }],
11481+ },
11482+ {
11483+ "type" : "model_output" ,
11484+ "content" : [{"type" : "text" , "text" : "4" }],
11485+ },
11486+ ],
11487+ }
11488+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11489+ # Should produce 1 turn, not 2.
11490+ assert len (result .turns ) == 1
11491+ assert result .turns [0 ].turn_index == 0
11492+ # The turn should contain all events: sandbox + real.
11493+ events = result .turns [0 ].events
11494+ assert len (events ) == 4
11495+ # First two events are sandbox tool call/response.
11496+ assert events [0 ].content .parts [0 ].function_call .name == "provision_sandbox"
11497+ assert events [1 ].content .parts [0 ].function_response .name == "provision_sandbox"
11498+ # Last two are the real conversation.
11499+ assert events [2 ].content .parts [0 ].text == "What is 2+2?"
11500+ assert events [3 ].content .parts [0 ].text == "4"
11501+
11502+ def test_sandbox_prefix_with_multi_turn (self ):
11503+ """Sandbox + 2 real turns produces 2 turns, not 3."""
11504+ interaction_dict = {
11505+ "status" : "completed" ,
11506+ "steps" : [
11507+ {
11508+ "type" : "function_call" ,
11509+ "name" : "provision_sandbox" ,
11510+ "arguments" : {},
11511+ "id" : "call_sb" ,
11512+ },
11513+ {
11514+ "type" : "function_result" ,
11515+ "name" : "provision_sandbox" ,
11516+ "call_id" : "call_sb" ,
11517+ "result" : {"status" : "ready" },
11518+ },
11519+ {
11520+ "type" : "user_input" ,
11521+ "content" : [{"type" : "text" , "text" : "Turn 1" }],
11522+ },
11523+ {
11524+ "type" : "model_output" ,
11525+ "content" : [{"type" : "text" , "text" : "Reply 1" }],
11526+ },
11527+ {
11528+ "type" : "user_input" ,
11529+ "content" : [{"type" : "text" , "text" : "Turn 2" }],
11530+ },
11531+ {
11532+ "type" : "model_output" ,
11533+ "content" : [{"type" : "text" , "text" : "Reply 2" }],
11534+ },
11535+ ],
11536+ }
11537+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11538+ assert len (result .turns ) == 2
11539+ assert result .turns [0 ].events [0 ].content .parts [0 ].function_call .name == "provision_sandbox"
11540+ assert result .turns [0 ].events [- 1 ].content .parts [0 ].text == "Reply 1"
11541+ assert result .turns [1 ].events [0 ].content .parts [0 ].text == "Turn 2"
11542+
11543+ def test_no_sandbox_prefix_unchanged (self ):
11544+ """Interaction without sandbox steps is unaffected."""
11545+ interaction_dict = {
11546+ "status" : "completed" ,
11547+ "steps" : [
11548+ {
11549+ "type" : "user_input" ,
11550+ "content" : [{"type" : "text" , "text" : "Hello" }],
11551+ },
11552+ {
11553+ "type" : "model_output" ,
11554+ "content" : [{"type" : "text" , "text" : "Hi" }],
11555+ },
11556+ ],
11557+ }
11558+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11559+ assert len (result .turns ) == 1
11560+ assert len (result .turns [0 ].events ) == 2
11561+
11562+ def test_all_sandbox_only_not_discarded (self ):
11563+ """If every step is sandbox-only, it remains as 1 turn (not discarded)."""
11564+ interaction_dict = {
11565+ "status" : "completed" ,
11566+ "steps" : [
11567+ {
11568+ "type" : "function_call" ,
11569+ "name" : "provision_sandbox" ,
11570+ "arguments" : {},
11571+ "id" : "call_sb" ,
11572+ },
11573+ {
11574+ "type" : "function_result" ,
11575+ "name" : "provision_sandbox" ,
11576+ "call_id" : "call_sb" ,
11577+ "result" : {"status" : "ready" },
11578+ },
11579+ ],
11580+ }
11581+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11582+ # Only 1 group, and it's sandbox-only — but there's no second group
11583+ # to merge into, so it stays as-is.
11584+ assert len (result .turns ) == 1
11585+
11586+ def test_non_sandbox_tool_before_user_input_not_merged (self ):
11587+ """A non-sandbox tool call before user_input is NOT merged."""
11588+ interaction_dict = {
11589+ "status" : "completed" ,
11590+ "steps" : [
11591+ {
11592+ "type" : "function_call" ,
11593+ "name" : "run_command" ,
11594+ "arguments" : {"cmd" : "ls" },
11595+ "id" : "call_1" ,
11596+ },
11597+ {
11598+ "type" : "function_result" ,
11599+ "name" : "run_command" ,
11600+ "call_id" : "call_1" ,
11601+ "result" : {"output" : "file.txt" },
11602+ },
11603+ {
11604+ "type" : "user_input" ,
11605+ "content" : [{"type" : "text" , "text" : "Hello" }],
11606+ },
11607+ {
11608+ "type" : "model_output" ,
11609+ "content" : [{"type" : "text" , "text" : "Hi" }],
11610+ },
11611+ ],
11612+ }
11613+ result = _evals_common ._interaction_dict_to_agent_data (interaction_dict )
11614+ # run_command is NOT a sandbox tool, so Turn 0 stays separate.
11615+ assert len (result .turns ) == 2
11616+
11617+
1138711618class TestMergeTextPartsInAgentData :
1138811619 """Tests for _merge_text_parts_in_agent_data."""
1138911620
0 commit comments