tenseleyflow/loader / eda0bc4

Browse files

Add direct tests for turn iteration control

Authored by espadonne
SHA
eda0bc4c97418e30ac57b8b793dc9cccec4923a7
Parents
cdb33c6
Tree
7d879aa

1 changed file

StatusFile+-
A tests/test_turn_iteration.py 140 0
tests/test_turn_iteration.pyadded
@@ -0,0 +1,140 @@
1
+"""Tests for assistant-cycle iteration orchestration."""
2
+
3
+from __future__ import annotations
4
+
5
+from pathlib import Path
6
+
7
+import pytest
8
+
9
+from loader.agent.loop import Agent, AgentConfig
10
+from loader.llm.base import CompletionResponse, ToolCall
11
+from loader.runtime.conversation import ConversationRuntime
12
+from loader.runtime.turn_iteration import TurnIterationAction
13
+from tests.helpers.runtime_harness import ScriptedBackend
14
+
15
+
16
+def non_streaming_config() -> AgentConfig:
17
+    """Shared config for direct turn-iteration tests."""
18
+
19
+    return AgentConfig(auto_context=False, stream=False, max_iterations=8)
20
+
21
+
22
+async def _run_iteration(
23
+    runtime: ConversationRuntime,
24
+    *,
25
+    task: str,
26
+    requested_mode: str = "execute",
27
+    original_task: str | None = None,
28
+) -> tuple:
29
+    events = []
30
+
31
+    async def capture(event) -> None:
32
+        events.append(event)
33
+
34
+    prepared = await runtime.turn_preparation.prepare(
35
+        task=task,
36
+        emit=capture,
37
+        requested_mode=requested_mode,
38
+        original_task=original_task,
39
+        on_user_question=None,
40
+    )
41
+    decision = await runtime.turn_iteration.run_iteration(
42
+        task=prepared.task,
43
+        effective_task=prepared.effective_task,
44
+        original_task=original_task,
45
+        effective_max_tokens=prepared.effective_max_tokens,
46
+        iterations=1,
47
+        max_iterations=runtime.agent.config.max_iterations,
48
+        actions_taken=[],
49
+        continuation_count=0,
50
+        empty_retry_count=0,
51
+        max_empty_retries=5,
52
+        extracted_iterations=0,
53
+        max_extracted_iterations=3,
54
+        consecutive_errors=0,
55
+        dod=prepared.definition_of_done,
56
+        emit=capture,
57
+        summary=prepared.summary,
58
+        executor=prepared.executor,
59
+        rollback_plan=prepared.rollback_plan,
60
+        on_confirmation=None,
61
+        on_user_question=None,
62
+        emit_confirmation=runtime._emit_confirmation(capture),
63
+    )
64
+    return prepared, decision, events
65
+
66
+
67
+@pytest.mark.asyncio
68
+async def test_turn_iteration_completes_react_final_answer(
69
+    temp_dir: Path,
70
+) -> None:
71
+    backend = ScriptedBackend(
72
+        completions=[
73
+            CompletionResponse(
74
+                content="Thought: I have enough context.\nFinal Answer: All set.",
75
+            )
76
+        ],
77
+        supports_native_tools=False,
78
+    )
79
+    agent = Agent(
80
+        backend=backend,
81
+        config=non_streaming_config(),
82
+        project_root=temp_dir,
83
+    )
84
+    runtime = ConversationRuntime(agent)
85
+
86
+    prepared, decision, events = await _run_iteration(
87
+        runtime,
88
+        task="Explain whether the turn iteration controller works.",
89
+    )
90
+
91
+    assert agent.use_react
92
+    assert decision.action == TurnIterationAction.COMPLETE
93
+    assert prepared.summary.final_response == "All set."
94
+    assert prepared.summary.assistant_messages[-1].content.endswith("Final Answer: All set.")
95
+    assert any(event.type == "response" and event.content == "All set." for event in events)
96
+
97
+
98
+@pytest.mark.asyncio
99
+async def test_turn_iteration_executes_native_tool_batch_and_continues(
100
+    temp_dir: Path,
101
+) -> None:
102
+    readme = temp_dir / "README.md"
103
+    readme.write_text("Loader runtime notes\n")
104
+
105
+    backend = ScriptedBackend(
106
+        completions=[
107
+            CompletionResponse(
108
+                content="I'll inspect the README first.",
109
+                tool_calls=[
110
+                    ToolCall(
111
+                        id="read-1",
112
+                        name="read",
113
+                        arguments={"file_path": "README.md"},
114
+                    )
115
+                ],
116
+            )
117
+        ]
118
+    )
119
+    agent = Agent(
120
+        backend=backend,
121
+        config=non_streaming_config(),
122
+        project_root=temp_dir,
123
+    )
124
+    runtime = ConversationRuntime(agent)
125
+
126
+    prepared, decision, events = await _run_iteration(
127
+        runtime,
128
+        task="Inspect the README before continuing.",
129
+    )
130
+
131
+    assert decision.action == TurnIterationAction.CONTINUE
132
+    assert decision.new_actions_taken == ["read: {'file_path': 'README.md'}"]
133
+    assert prepared.summary.assistant_messages[-1].tool_calls[0].name == "read"
134
+    assert len(prepared.summary.tool_result_messages) == 1
135
+    assert "Loader runtime notes" in prepared.summary.tool_result_messages[0].content
136
+    assert any(event.type == "tool_call" and event.tool_name == "read" for event in events)
137
+    assert any(
138
+        event.type == "tool_result" and "Loader runtime notes" in event.content
139
+        for event in events
140
+    )