@@ -276,14 +276,47 @@ def test_gitignore_path_anchored_not_overpruned(tmp_path):
276276
277277
278278# ── path localization (abs→rel avoids the ~50s sandbox slow path) ─────────
279- def test_localize_absolute_path_inside_cwd (tmp_path ):
279+ @pytest .mark .parametrize ("workspace_mode" , ["unset" , "same" , "symlink" ])
280+ def test_localize_absolute_path_inside_cwd (tmp_path , monkeypatch , workspace_mode ):
280281 cwd = str (tmp_path )
282+ # Localization is valid only when the runtime resolves paths in cwd.
283+ # Do not inherit a workspace configured by another test or the caller.
284+ monkeypatch .delenv ("FRONTIER_AGENT_INPUTS_DIR" , raising = False )
285+ monkeypatch .delenv ("FRONTIER_AGENT_WORKSPACE_DIR" , raising = False )
286+ if workspace_mode == "same" :
287+ monkeypatch .setenv ("FRONTIER_AGENT_WORKSPACE_DIR" , cwd )
288+ elif workspace_mode == "symlink" :
289+ workspace = tmp_path / "workspace-link"
290+ workspace .symlink_to (tmp_path , target_is_directory = True )
291+ monkeypatch .setenv ("FRONTIER_AGENT_WORKSPACE_DIR" , str (workspace ))
281292 (tmp_path / "sub" ).mkdir ()
282293 abspath = str (tmp_path / "sub" / "f.py" )
283294 out = localize_path_args ("read_file" , {"path" : abspath }, cwd )
284295 assert out is not None and out ["path" ] == "sub/f.py"
285296
286297
298+ def test_read_file_keeps_project_absolute_path_with_split_runtime_workspace (
299+ tmp_path , monkeypatch ,
300+ ):
301+ project = tmp_path / "project"
302+ runtime_workspace = tmp_path / "private-workspace"
303+ project .mkdir ()
304+ runtime_workspace .mkdir ()
305+ target = project / "README.md"
306+ target .write_text ("# project\n " )
307+
308+ monkeypatch .delenv ("FRONTIER_AGENT_INPUTS_DIR" , raising = False )
309+ monkeypatch .setenv (
310+ "FRONTIER_AGENT_WORKSPACE_DIR" , str (runtime_workspace ),
311+ )
312+
313+ out = localize_path_args (
314+ "read_file" , {"path" : str (target )}, str (project ),
315+ )
316+
317+ assert out is None
318+
319+
287320def test_localize_preserves_absolute_task_input_path (tmp_path , monkeypatch ):
288321 inputs = tmp_path / ".apodex" / "inputs" / "run"
289322 inputs .mkdir (parents = True )
@@ -931,6 +964,8 @@ def test_native_workflow_no_tool_stop_is_not_reported_as_delivery(
931964 assert "no_tool" in out
932965 assert "partial output was not saved as a final report" in out
933966 assert "Final report" not in out
967+ assert "turns=12" in out
968+ assert "tools=0" in out
934969
935970
936971def test_generic_loop_no_tool_stop_is_a_normal_finish ():
@@ -942,6 +977,33 @@ def test_generic_loop_no_tool_stop_is_a_normal_finish():
942977 assert _is_complete_run ("max_turns" , no_tool_is_complete = True ) is False
943978
944979
980+ def test_workflow_complete_agent_no_tool_is_a_normal_finish ():
981+ """A workflow-certified agent answer may terminate via a tool-free turn."""
982+ from apodex .task_runner import _is_complete_run
983+
984+ assert _is_complete_run (
985+ "no_tool" ,
986+ answer_status = "complete" ,
987+ answer_source = "agent" ,
988+ ) is True
989+
990+ # Do not broaden workflow no_tool into an unconditional success signal.
991+ assert _is_complete_run (
992+ "no_tool" ,
993+ answer_status = "best_effort" ,
994+ answer_source = "agent" ,
995+ ) is False
996+ assert _is_complete_run (
997+ "no_tool" ,
998+ answer_source = "agent" ,
999+ ) is False
1000+ assert _is_complete_run (
1001+ "max_turns" ,
1002+ answer_status = "complete" ,
1003+ answer_source = "agent" ,
1004+ ) is False
1005+
1006+
9451007async def _drive_workflow (session , profile , follow_up ):
9461008 """Run one native workflow with ``run_task`` stubbed to record follow-ups."""
9471009 session .run_task = follow_up # type: ignore[method-assign]
@@ -1528,3 +1590,20 @@ def test_download_file_target_is_the_resolved_destination(monkeypatch, tmp_path)
15281590 assert named .startswith (str (tmp_path / "downloads" / "p.pdf" ))
15291591 assert "/elsewhere/" not in named # the requested directory is ignored
15301592 assert "renamed" in named # collisions rename it
1593+
1594+
1595+ def test_native_workflow_uses_authoritative_loop_telemetry (
1596+ tmp_path , monkeypatch , capsys ,
1597+ ):
1598+ _run_workflow_returning ({
1599+ "final_answer" : "done" ,
1600+ "answer_status" : "complete" ,
1601+ "final_answer_source" : "agent" ,
1602+ "stopped_by" : "no_tool" ,
1603+ "react_steps" : [{}] * 7 ,
1604+ "turns_used" : 8 ,
1605+ "tool_calls_count" : 7 ,
1606+ }, tmp_path , monkeypatch )
1607+
1608+ out = capsys .readouterr ().out
1609+ assert "turns=8 · tools=7 · no_tool" in out
0 commit comments