91 lines
2.9 KiB
Python
91 lines
2.9 KiB
Python
"""Regression coverage for timed-out delegation teardown."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from types import SimpleNamespace
|
|
|
|
from tools import delegate_tool
|
|
|
|
|
|
class _SlowUnwindingChild:
|
|
def __init__(self) -> None:
|
|
self.tool_progress_callback = None
|
|
self._credential_pool = None
|
|
self._delegate_saved_tool_names = []
|
|
self._delegate_role = "leaf"
|
|
self._delegate_depth = 1
|
|
self._subagent_id = None
|
|
self.model = "test-model"
|
|
self.session_prompt_tokens = 0
|
|
self.session_completion_tokens = 0
|
|
self.session_estimated_cost_usd = 0.0
|
|
self.session_cost_status = "unknown"
|
|
self.started = threading.Event()
|
|
self.interrupted = threading.Event()
|
|
self.unwinding = threading.Event()
|
|
self.allow_finish = threading.Event()
|
|
self.finished = threading.Event()
|
|
self.closed = threading.Event()
|
|
self.close_while_running = False
|
|
|
|
def run_conversation(self, **_kwargs):
|
|
self.started.set()
|
|
assert self.interrupted.wait(timeout=1)
|
|
# Model the real child turn's finally path: it still performs session
|
|
# activity/SQLite cleanup after the parent requests interruption.
|
|
self.unwinding.set()
|
|
assert self.allow_finish.wait(timeout=2)
|
|
self.finished.set()
|
|
return {
|
|
"final_response": "",
|
|
"completed": False,
|
|
"interrupted": True,
|
|
"api_calls": 1,
|
|
"messages": [],
|
|
}
|
|
|
|
def hard_interrupt(self, _reason=None):
|
|
self.interrupted.set()
|
|
|
|
def get_activity_summary(self):
|
|
return {"api_call_count": 1}
|
|
|
|
def close(self):
|
|
if not self.finished.is_set():
|
|
self.close_while_running = True
|
|
self.closed.set()
|
|
|
|
|
|
def test_timeout_does_not_close_child_while_worker_is_unwinding(monkeypatch):
|
|
child = _SlowUnwindingChild()
|
|
parent = SimpleNamespace(
|
|
session_id="parent-timeout-test",
|
|
_current_task_id=None,
|
|
_active_children=[child],
|
|
_active_children_lock=threading.Lock(),
|
|
)
|
|
monkeypatch.setattr(delegate_tool, "_get_child_timeout", lambda: 0.5)
|
|
monkeypatch.setattr(delegate_tool, "_get_worktree_isolation", lambda: False)
|
|
|
|
result = delegate_tool._run_single_child(
|
|
task_index=0,
|
|
goal="exercise timeout teardown",
|
|
child=child,
|
|
parent_agent=parent,
|
|
)
|
|
|
|
assert result["status"] == "timeout"
|
|
assert child.unwinding.wait(timeout=1)
|
|
try:
|
|
assert not child.closed.is_set(), (
|
|
"timed-out child.close() ran before its conversation thread unwound"
|
|
)
|
|
finally:
|
|
child.allow_finish.set()
|
|
assert child.finished.wait(timeout=1)
|
|
assert child.closed.wait(timeout=1)
|
|
assert not child.close_while_running, (
|
|
"timed-out child.close() raced its still-running conversation thread"
|
|
)
|