"""Runtime tests for the hosted-room session adapter.""" from __future__ import annotations import threading import time from contextlib import contextmanager from pathlib import Path from types import SimpleNamespace from typing import Any import pytest from gateway import hosted_room_driver as state from gateway import hosted_rooms from tui_gateway.hosted_room_driver import ( MAX_TERMINAL_TEXT_BYTES, ROOM_SESSION_SOURCE, HostedRoomBinding, HostedRoomRuntime, room_session_title, ) from tui_gateway.hosted_room_peer_http import PeerRunsHTTPError from tui_gateway.hosted_room_peer_transport import ( PeerHostedRoomTransport, PeerMemberRoute, ) ROOM_ID = "room-1" PROFILE = "ops" BINDING = HostedRoomBinding( room_id=ROOM_ID, gateway_id="gateway-a", authority_epoch=1, ) class RecordingTurnLocks: """Record the profile lock and expose ownership to the fake RPC.""" def __init__(self) -> None: self.events: list[tuple[str, str]] = [] self.local = threading.local() @contextmanager def __call__(self, profile: str): self.events.append(("lock-enter", profile)) self.local.profile = profile try: yield finally: self.events.append(("lock-exit", profile)) self.local.profile = None def held_for(self, profile: str) -> bool: return getattr(self.local, "profile", None) == profile class FakeSessionRPC: """Normalized in-memory session adapter with no model or network.""" def __init__( self, *, auto_complete: bool = True, required_lock: RecordingTurnLocks | None = None, ) -> None: self.auto_complete = auto_complete self.required_lock = required_lock self.calls: list[tuple[str, dict[str, Any]]] = [] self.sessions: dict[tuple[str, str], dict[str, Any]] = {} self.states: dict[str, dict[str, Any]] = {} self.submitted = threading.Event() self.on_interrupt = None self.on_info = None self.history_failures = 0 self._next_id = 1 self._lock = threading.Lock() def _assert_lock(self, profile: str) -> None: if self.required_lock is not None: assert self.required_lock.held_for(profile) def add_session( self, *, profile: str = PROFILE, title: str = room_session_title(ROOM_ID), active: bool = False, task_id: str | None = None, history: list[dict[str, Any]] | None = None, ) -> str: with self._lock: session_id = f"session-{self._next_id}" self._next_id += 1 session = {"session_id": session_id, "title": title} self.sessions[(profile, title)] = session self.states[session_id] = { "active": active, "task_id": task_id, "execution_generation": None, "history": list(history or []), "on_terminal": None, "pending_approval": None, } return session_id def complete( self, task_id: str, *, content: str = "Finished once.", status: str = "settled", ) -> None: callback = None receipt = None with self._lock: for session_id, session_state in self.states.items(): if session_state["task_id"] != task_id: continue receipt = { "role": "assistant", "task_id": task_id, "execution_generation": session_state["execution_generation"], "status": status, "message_id": f"reply:{task_id}", "content": content, } session_state["history"].append(receipt) session_state["active"] = False callback = session_state.get("on_terminal") self.calls.append(("complete", {"session_id": session_id})) break if receipt is None: raise AssertionError(f"no active session for {task_id}") if callback is not None: callback({ "status": status, "settlement_id": receipt["message_id"], "message_id": receipt["message_id"], "text": content, }) def resolve_exact(self, *, profile: str, title: str, source: str): self._assert_lock(profile) params = {"profile": profile, "title": title, "source": source} self.calls.append(("resolve_exact", params)) with self._lock: session = self.sessions.get((profile, title)) return dict(session) if session is not None else None def create(self, *, profile: str, title: str, source: str): self._assert_lock(profile) params = {"profile": profile, "title": title, "source": source} self.calls.append(("create", params)) session_id = self.add_session(profile=profile, title=title) return {"session_id": session_id, "title": title} def resume(self, *, profile: str, session_id: str, source: str): self._assert_lock(profile) params = { "profile": profile, "session_id": session_id, "source": source, } self.calls.append(("resume", params)) return {"session_id": session_id} def submit( self, *, profile: str, session_id: str, prompt: str, source: str, task: state.TaskIdentity, execution_generation: int, on_terminal, ): self._assert_lock(profile) params = { "profile": profile, "session_id": session_id, "prompt": prompt, "source": source, "task": task, "execution_generation": execution_generation, "on_terminal": on_terminal, } self.calls.append(("submit", params)) with self._lock: self.states[session_id]["active"] = True self.states[session_id]["task_id"] = task.task_id self.states[session_id]["execution_generation"] = execution_generation self.states[session_id]["on_terminal"] = on_terminal self.submitted.set() if self.auto_complete: self.complete(task.task_id) return {"accepted": True} def history(self, *, profile: str, session_id: str, source: str): self._assert_lock(profile) params = { "profile": profile, "session_id": session_id, "source": source, } self.calls.append(("history", params)) if self.history_failures > 0: self.history_failures -= 1 raise RuntimeError("transient history read failed") with self._lock: return [dict(message) for message in self.states[session_id]["history"]] def info(self, *, profile: str, session_id: str, source: str): self._assert_lock(profile) params = { "profile": profile, "session_id": session_id, "source": source, } self.calls.append(("info", params)) with self._lock: session_state = self.states[session_id] result = { "active": session_state["active"], "task_id": session_state["task_id"], } if session_state.get("pending_approval"): result["status"] = "waiting_for_approval" result["pending_approval"] = dict(session_state["pending_approval"]) if self.on_info is not None: self.on_info() return result def interrupt( self, *, profile: str, session_id: str, source: str, expected_task_id: str, ): params = { "profile": profile, "session_id": session_id, "source": source, "expected_task_id": expected_task_id, } with self._lock: current = self.states[session_id] if not current["active"] or current["task_id"] != expected_task_id: self.calls.append(("interrupt_skipped", params)) return {"interrupted": False} current["active"] = False self.calls.append(("interrupt", params)) if self.on_interrupt is not None: self.on_interrupt() return {"interrupted": True} class SelectiveCompletionRPC(FakeSessionRPC): """Keep selected local profiles running while peers complete normally.""" def __init__(self, *, waiting_profiles: set[str]) -> None: super().__init__() self.waiting_profiles = waiting_profiles self._submit_mode_lock = threading.Lock() def submit(self, **kwargs): with self._submit_mode_lock: original = self.auto_complete self.auto_complete = kwargs["profile"] not in self.waiting_profiles try: return super().submit(**kwargs) finally: self.auto_complete = original class NotAdmittedThenSuccessRPC(FakeSessionRPC): def __init__(self, failures: int) -> None: super().__init__() self.failures = failures self.attempted_generations: list[int] = [] def submit(self, **kwargs): self.attempted_generations.append(kwargs["execution_generation"]) if self.failures > 0: self.failures -= 1 self.calls.append(("submit", dict(kwargs))) raise PeerRunsHTTPError( "peer refused the connection", retryable=True, not_admitted=True, ) return super().submit(**kwargs) class TerminalPeerClient: """Peer client whose terminal history would look failed if read first.""" def __init__(self, *, task_id: str, execution_generation: int) -> None: self.task_id = task_id self.execution_generation = execution_generation self.status_task_id = task_id self.status_generation = execution_generation self.status_value = "interrupted" self.history_calls = 0 def prepare(self, **_kwargs): return {"session_id": "peer-session"} def status(self, **_kwargs): return { "active": False, "status": self.status_value, "task_id": self.status_task_id, "execution_generation": self.status_generation, } def history(self, **_kwargs): self.history_calls += 1 return [{ "role": "assistant", "task_id": self.task_id, "execution_generation": self.execution_generation, "status": "failed", "message_id": "peer-interrupted", "content": "interrupted", }] def stop_receipt(self, **_kwargs): return {"status": "stopping"} def stop(self, **_kwargs): return {"status": "stopping"} def _peer_resolver(client: TerminalPeerClient): route = PeerMemberRoute( home_install_id="install-home", member_id=PROFILE, target_install_id="install-peer", target_profile=PROFILE, capability_digest="a" * 64, execution_policy_digest="b" * 64, cancellation_scope_id="cancel-peer", trace_id="trace-peer", grant="signed-room-grant", ) def resolve(binding, task): return PeerHostedRoomTransport( binding=binding, route=route, client=client, source_event_seq=int(task["payload"]["source_event_seq"]), task_id=task["identity"].task_id, execution_generation=int(task["execution_generation"]), ) return resolve @pytest.fixture def db(tmp_path: Path) -> Path: path = tmp_path / "state.db" hosted_rooms.create_room( path, room_id=ROOM_ID, name="Release room", members=[{"profile": PROFILE, "handle": PROFILE}], authority_gateway_id=BINDING.gateway_id, now=time.time(), ) return path def _identity(task_id: str = "task-1") -> state.TaskIdentity: return state.TaskIdentity( room_id=ROOM_ID, task_id=task_id, thread_id="thread-1", turn_id=f"turn-{task_id}", ) def _admit( db: Path, identity: state.TaskIdentity, *, prompt: str = "Inspect the release candidate.", ) -> None: state.admit_task( db, identity, payload={ "target_profile": PROFILE, "prompt": prompt, "source_event_seq": 1, }, clock=time.time, ) def _runtime( db: Path, rpc: FakeSessionRPC, locks: RecordingTurnLocks | None = None, **kwargs, ) -> HostedRoomRuntime: return HostedRoomRuntime( db_path=db, rooms=[BINDING], rpc=rpc, turn_lock=locks or RecordingTurnLocks(), lease_ttl_seconds=kwargs.pop("lease_ttl_seconds", 0.4), poll_interval_seconds=kwargs.pop("poll_interval_seconds", 0.01), **kwargs, ) def _wait_for(predicate, *, timeout: float = 2.0) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: if predicate(): return time.sleep(0.01) raise AssertionError("condition was not reached before timeout") def test_runtime_uses_unique_process_generation(db: Path): first = _runtime(db, FakeSessionRPC()) second = _runtime(db, FakeSessionRPC()) assert first.process_generation != second.process_generation assert len(first.process_generation) == 32 @pytest.mark.parametrize("value", [0, True]) def test_room_concurrency_bound_must_be_a_positive_integer(db: Path, value): with pytest.raises(ValueError, match="max_concurrent_rooms"): _runtime(db, FakeSessionRPC(), max_concurrent_rooms=value) def test_waiting_room_does_not_block_an_independent_local_room(tmp_path: Path): db = tmp_path / "state.db" bindings = [ HostedRoomBinding("room-waiting", "gateway-a", 1), HostedRoomBinding("room-healthy", "gateway-a", 1), ] identities = [ state.TaskIdentity("room-waiting", "task-waiting", "thread-a", "turn-a"), state.TaskIdentity("room-healthy", "task-healthy", "thread-b", "turn-b"), ] profiles = ["profile-waiting", "profile-healthy"] for binding, identity, profile in zip(bindings, identities, profiles): hosted_rooms.create_room( db, room_id=binding.room_id, name=binding.room_id, members=[{"profile": profile, "handle": profile}], authority_gateway_id=binding.gateway_id, now=time.time(), ) state.admit_task( db, identity, payload={ "target_profile": profile, "prompt": f"Run {binding.room_id}.", "source_event_seq": 1, }, clock=time.time, ) rpc = SelectiveCompletionRPC(waiting_profiles={"profile-waiting"}) runtime = HostedRoomRuntime( db_path=db, rooms=bindings, rpc=rpc, turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.01, max_concurrent_rooms=2, ) runtime.start() _wait_for(lambda: state.get_task(db, identities[1])["status"] == "settled") _wait_for(lambda: state.get_task(db, identities[0])["status"] == "running") assert state.get_task(db, identities[0])["status"] == "running" _wait_for(lambda: len(runtime.status()["current_tasks"]) == 1) assert len(runtime.status()["current_tasks"]) == 1 assert runtime.stop(timeout=5.0) def test_rotated_bounded_scheduler_eventually_runs_later_room(tmp_path: Path): db = tmp_path / "state.db" bindings = [ HostedRoomBinding(f"room-{index}", "gateway-a", 1) for index in range(1, 4) ] for binding in bindings: hosted_rooms.create_room( db, room_id=binding.room_id, name=binding.room_id, members=[{"profile": PROFILE, "handle": PROFILE}], authority_gateway_id=binding.gateway_id, now=time.time(), ) identity = state.TaskIdentity( "room-3", "task-room-3", "thread-room-3", "turn-room-3", ) state.admit_task( db, identity, payload={ "target_profile": PROFILE, "prompt": "Run the later room.", "source_event_seq": 1, }, clock=time.time, ) runtime = HostedRoomRuntime( db_path=db, rooms=bindings, rpc=FakeSessionRPC(), turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.01, max_concurrent_rooms=2, ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) def test_queued_task_routes_profile_and_credentials_without_overrides(db: Path): identity = _identity() _admit(db, identity, prompt="Use the configured profile credentials.") rpc = FakeSessionRPC() runtime = _runtime(db, rpc) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) create = next(params for method, params in rpc.calls if method == "create") submit = next(params for method, params in rpc.calls if method == "submit") assert create == { "profile": PROFILE, "title": f"Group: {ROOM_ID}", "source": ROOM_SESSION_SOURCE, } assert submit["profile"] == PROFILE assert submit["source"] == ROOM_SESSION_SOURCE assert submit["prompt"] == "Use the configured profile credentials." assert "model" not in create | submit assert "provider" not in create | submit assert state.get_task(db, identity)["result"]["text"] == "Finished once." def test_worker_settles_without_any_client_transport(db: Path): identity = _identity() _admit(db, identity) runtime = _runtime(db, FakeSessionRPC()) runtime.start() _wait_for( lambda: ( state.get_task(db, identity)["status"] == "settled" and runtime.status()["cycles"] >= 1 ) ) assert runtime.status()["running"] is True assert runtime.status()["cycles"] >= 1 assert runtime.stop(timeout=5.0) def test_policy_hooks_prepare_and_publish_terminal_idempotently(db: Path): identity = _identity() _admit(db, identity) prepared = [] published = [] runtime = _runtime( db, FakeSessionRPC(), prepare_room=lambda binding: prepared.append(binding.room_id), publish_terminal=lambda binding, task: published.append(( binding.room_id, task["identity"].task_id, task["status"], )), ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) assert prepared assert published == [(ROOM_ID, identity.task_id, "settled")] def test_transport_resolver_selects_member_transport_without_forking_state( db: Path, ): identity = _identity() _admit(db, identity) selected = FakeSessionRPC() resolutions = [] def resolve_transport(binding, task): resolutions.append((binding, task["identity"], task["payload"])) return selected runtime = HostedRoomRuntime( db_path=db, rooms=[BINDING], transport_resolver=resolve_transport, turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.01, ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) assert resolutions assert all(binding == BINDING for binding, _, _ in resolutions) assert all(task_identity == identity for _, task_identity, _ in resolutions) assert any(method == "submit" for method, _ in selected.calls) def test_not_admitted_peer_task_stays_queued_with_exponential_capped_retry( db: Path, ): now = [100.0] identity = _identity() _admit(db, identity, prompt="Keep this exact prompt queued.") rpc = NotAdmittedThenSuccessRPC(failures=3) runtime = _runtime( db, rpc, clock=lambda: now[0], lease_ttl_seconds=30, unavailable_retry_min_seconds=2, unavailable_retry_max_seconds=4, ) runtime._run_cycle() assert state.get_task(db, identity)["status"] == "queued" assert rpc.attempted_generations == [1] runtime._run_cycle() assert rpc.attempted_generations == [1] now[0] += 2 runtime._run_cycle() assert rpc.attempted_generations == [1, 2] now[0] += 3.9 runtime._run_cycle() assert rpc.attempted_generations == [1, 2] now[0] += 0.1 runtime._run_cycle() assert rpc.attempted_generations == [1, 2, 3] now[0] += 4 runtime._run_cycle() task = state.get_task(db, identity) assert task["status"] == "settled" assert task["execution_generation"] == 4 assert task["payload"]["prompt"] == "Keep this exact prompt queued." assert rpc.attempted_generations == [1, 2, 3, 4] def test_not_admitted_room_does_not_block_other_rooms(tmp_path: Path): db = tmp_path / "state.db" for room_id in ("room-1", "room-2"): hosted_rooms.create_room( db, room_id=room_id, name=room_id, members=[{"profile": PROFILE, "handle": PROFILE}], authority_gateway_id=BINDING.gateway_id, now=90, ) offline_identity = _identity("offline-task") healthy_identity = state.TaskIdentity( "room-2", "healthy-task", "thread-2", "turn-healthy" ) _admit(db, offline_identity) _admit(db, healthy_identity) offline = NotAdmittedThenSuccessRPC(failures=10) healthy = FakeSessionRPC() runtime = HostedRoomRuntime( db_path=db, rooms=[BINDING, HostedRoomBinding("room-2", "gateway-a", 1)], transport_resolver=lambda binding, _task: ( offline if binding.room_id == ROOM_ID else healthy ), turn_lock=RecordingTurnLocks(), clock=lambda: 100.0, lease_ttl_seconds=30, poll_interval_seconds=0.01, ) runtime._run_cycle() assert state.get_task(db, offline_identity)["status"] == "queued" assert state.get_task(db, healthy_identity)["status"] == "settled" def test_waiting_room_does_not_block_an_independent_room(tmp_path: Path): db = tmp_path / "state.db" bindings = [ HostedRoomBinding("room-waiting", "gateway-a", 1), HostedRoomBinding("room-healthy", "gateway-a", 1), ] identities = [ state.TaskIdentity("room-waiting", "task-waiting", "thread-a", "turn-a"), state.TaskIdentity("room-healthy", "task-healthy", "thread-b", "turn-b"), ] profiles = ["profile-waiting", "profile-healthy"] for binding, identity, profile in zip(bindings, identities, profiles): hosted_rooms.create_room( db, room_id=binding.room_id, name=binding.room_id, members=[{"profile": profile, "handle": profile}], authority_gateway_id=binding.gateway_id, now=time.time(), ) state.admit_task( db, identity, payload={ "target_profile": profile, "prompt": f"Run {binding.room_id}.", "source_event_seq": 1, }, clock=time.time, ) waiting = FakeSessionRPC(auto_complete=False) healthy = FakeSessionRPC() runtime = HostedRoomRuntime( db_path=db, rooms=bindings, transport_resolver=lambda binding, _task: ( waiting if binding.room_id == "room-waiting" else healthy ), turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.01, max_concurrent_rooms=2, ) runtime.start() assert waiting.submitted.wait(1.0) _wait_for(lambda: state.get_task(db, identities[1])["status"] == "settled") assert state.get_task(db, identities[0])["status"] == "running" assert runtime.stop(timeout=5.0) def test_bounded_scheduler_eventually_runs_later_room(tmp_path: Path): db = tmp_path / "state.db" bindings = [ HostedRoomBinding(f"room-{index}", "gateway-a", 1) for index in range(1, 4) ] for binding in bindings: hosted_rooms.create_room( db, room_id=binding.room_id, name=binding.room_id, members=[{"profile": PROFILE, "handle": PROFILE}], authority_gateway_id=binding.gateway_id, now=time.time(), ) identity = state.TaskIdentity( "room-3", "task-room-3", "thread-room-3", "turn-room-3", ) state.admit_task( db, identity, payload={ "target_profile": PROFILE, "prompt": "Run the later room.", "source_event_seq": 1, }, clock=time.time, ) runtime = HostedRoomRuntime( db_path=db, rooms=bindings, rpc=FakeSessionRPC(), turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.01, max_concurrent_rooms=2, ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) def test_existing_canonical_session_is_resumed_not_duplicated(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC() session_id = rpc.add_session() runtime = _runtime(db, rpc) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) assert not [call for call in rpc.calls if call[0] == "create"] resume = next(params for method, params in rpc.calls if method == "resume") assert resume == { "profile": PROFILE, "session_id": session_id, "source": ROOM_SESSION_SOURCE, } def test_local_crash_recovery_keeps_ambiguous_history_explicit_without_resume( db: Path, ): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=0.2, clock=clock, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session( task_id=identity.task_id, history=[ { "role": "assistant", "task_id": identity.task_id, "execution_generation": 1, "status": "settled", "message_id": "reply:recovered", "content": "Recovered durable answer.", } ], ) now[0] = 101.0 runtime = _runtime( db, rpc, clock=clock, indeterminate_defer_seconds=5, ) runtime._process_room(BINDING) recovered = state.get_task(db, identity) assert recovered["status"] == "indeterminate" assert recovered["result"] is None assert not [call for call in rpc.calls if call[0] == "history"] assert [call for call in rpc.calls if call[0] == "info"] assert not [call for call in rpc.calls if call[0] == "resume"] assert not [call for call in rpc.calls if call[0] == "submit"] def test_expired_local_attempt_defers_without_hydrating_or_resubmitting(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=0.2, clock=clock, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session( task_id=identity.task_id, history=[ { "role": "assistant", "task_id": identity.task_id, "execution_generation": 1, "status": "settled", "message_id": "reply:expired-recovered", "content": "Recovered after lease expiry.", } ], ) now[0] = 101.0 runtime = _runtime( db, rpc, clock=clock, indeterminate_defer_seconds=0.5, ) runtime._process_room(BINDING) now[0] = 102.0 runtime._process_room(BINDING) recovered = state.get_task(db, identity) assert recovered["status"] == "deferred" assert recovered["result"] == { "reason": "member_unavailable", "retryable": True, } assert not [call for call in rpc.calls if call[0] == "history"] assert not [call for call in rpc.calls if call[0] == "resume"] assert not [call for call in rpc.calls if call[0] == "submit"] def test_oversized_terminal_reply_is_bounded_without_waiting_for_deadline(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc, turn_timeout_seconds=30) runtime.start() assert rpc.submitted.wait(timeout=5.0) rpc.complete( identity.task_id, content="é" * (MAX_TERMINAL_TEXT_BYTES + 100), ) _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) result = state.get_task(db, identity)["result"] assert result["truncated"] is True assert len(result["text"].encode("utf-8")) <= MAX_TERMINAL_TEXT_BYTES assert result["text"].endswith("share the full result as a file.]") def test_peer_recovery_probe_is_bounded_by_attempt_and_stale_age(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) now[0] = 102.0 runtime = _runtime( db, FakeSessionRPC(), clock=clock, lease_ttl_seconds=30, indeterminate_defer_seconds=5, ) recovery_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation=runtime.process_generation, ttl_seconds=30, clock=clock, ) state.recover_room(db, recovery_lease, clock=clock) runtime.transport_resolver = lambda _binding, _task: object() probes = [] def inspect(_binding, task): probes.append((task["identity"].task_id, now[0])) return SimpleNamespace(terminal=None, active=False, status=None) runtime._inspect_recovery_session = inspect assert runtime._reconcile_indeterminate(BINDING, recovery_lease) is True assert runtime._reconcile_indeterminate(BINDING, recovery_lease) is True assert probes == [(identity.task_id, 102.0)] now[0] = 108.0 assert runtime._reconcile_indeterminate(BINDING, recovery_lease) is False assert probes == [(identity.task_id, 102.0), (identity.task_id, 108.0)] assert state.get_task(db, identity)["status"] == "deferred" def test_turn_deadline_stops_exact_attempt_and_publishes_durable_failure(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) published = [] runtime = _runtime( db, rpc, active_poll_interval_seconds=0.01, turn_timeout_seconds=0.05, publish_terminal=lambda _binding, task: published.append(task), ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "failed") assert runtime.stop(timeout=5.0) failed = state.get_task(db, identity) assert failed["result"] == { "error": ( "This Group Chat turn exceeded its configured time limit and was stopped." ), "reason_code": "turn_deadline_exceeded", "timeout_seconds": 0.05, } assert failed["cancel_id"] == "deadline:1" assert [call for call in rpc.calls if call[0] == "interrupt"] assert [task["status"] for task in published] == ["failed"] def test_deadline_releases_worker_capacity_for_later_room(tmp_path: Path): db = tmp_path / "state.db" bindings = [ HostedRoomBinding("room-stuck", "gateway-a", 1), HostedRoomBinding("room-healthy", "gateway-a", 1), ] identities = [ state.TaskIdentity("room-stuck", "task-stuck", "thread-a", "turn-a"), state.TaskIdentity("room-healthy", "task-healthy", "thread-b", "turn-b"), ] for binding, identity in zip(bindings, identities): hosted_rooms.create_room( db, room_id=binding.room_id, name=binding.room_id, members=[{"profile": PROFILE, "handle": PROFILE}], authority_gateway_id=binding.gateway_id, ) state.admit_task( db, identity, payload={ "target_profile": PROFILE, "prompt": f"Run {binding.room_id}.", "source_event_seq": 1, }, clock=time.time, ) class FirstRoomStallsRPC(FakeSessionRPC): def submit(self, **kwargs): result = super().submit(**kwargs) if kwargs["task"].room_id == "room-healthy": self.complete(kwargs["task"].task_id) return result rpc = FirstRoomStallsRPC(auto_complete=False) runtime = HostedRoomRuntime( db_path=db, rooms=bindings, rpc=rpc, turn_lock=RecordingTurnLocks(), lease_ttl_seconds=0.4, poll_interval_seconds=0.02, active_poll_interval_seconds=0.01, turn_timeout_seconds=0.05, max_concurrent_rooms=1, ) runtime.start() _wait_for(lambda: state.get_task(db, identities[0])["status"] == "failed") _wait_for(lambda: state.get_task(db, identities[1])["status"] == "settled") assert runtime.stop(timeout=5.0) assert state.get_task(db, identities[0])["result"]["reason_code"] == ( "turn_deadline_exceeded" ) assert state.get_task(db, identities[1])["status"] == "settled" def test_retry_ignores_late_receipt_from_prior_execution_generation(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=0.2, clock=clock, ) _admit(db, identity) old_attempt = state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) now[0] = 101.0 current_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="manual-recovery", ttl_seconds=30, clock=clock, ) state.recover_room(db, current_lease, clock=clock) state.requeue_indeterminate_task( db, identity, current_lease, expected_execution_generation=old_attempt.execution_generation, expected_cancel_generation=old_attempt.cancel_generation, clock=clock, ) state.release_lease(db, current_lease, clock=clock) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session( task_id=identity.task_id, history=[ { "role": "assistant", "task_id": identity.task_id, "execution_generation": old_attempt.execution_generation, "status": "settled", "message_id": "reply:late-old-attempt", "content": "Late old result.", } ], ) runtime = _runtime(db, rpc, clock=clock) runtime.start() assert rpc.submitted.wait(1.0) time.sleep(0.04) assert runtime.stop(timeout=5.0) task = state.get_task(db, identity) assert task["status"] == "running" assert task["execution_generation"] == old_attempt.execution_generation + 1 def test_active_recovered_turn_is_never_resubmitted(db: Path): identity = _identity() old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=10, clock=time.time, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=time.time, ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session(active=True, task_id=identity.task_id) runtime = _runtime(db, rpc) runtime.start() time.sleep(0.08) assert runtime.stop(timeout=5.0) assert state.get_task(db, identity)["status"] == "running" assert not [call for call in rpc.calls if call[0] == "submit"] def test_retry_cannot_advance_generation_while_original_attempt_is_active( db: Path, ): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) _admit(db, identity) attempt = state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) now[0] = 102.0 recovery_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="recovery-process", ttl_seconds=30, clock=clock, ) state.recover_room(db, recovery_lease, clock=clock) state.release_lease(db, recovery_lease, clock=clock) rpc = FakeSessionRPC(auto_complete=False) session_id = rpc.add_session(active=True, task_id=identity.task_id) rpc.states[session_id]["execution_generation"] = attempt.execution_generation runtime = _runtime(db, rpc, clock=clock) with pytest.raises(state.InvalidTaskTransitionError, match="still active"): runtime.retry_indeterminate(identity) task = state.get_task(db, identity) assert task["status"] == "indeterminate" assert task["execution_generation"] == attempt.execution_generation assert not [call for call in rpc.calls if call[0] == "submit"] def test_retry_uses_runtime_session_id_returned_by_resume(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) now[0] = 102.0 recovery_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="recovery-process", ttl_seconds=30, clock=clock, ) state.recover_room(db, recovery_lease, clock=clock) state.release_lease(db, recovery_lease, clock=clock) rpc = FakeSessionRPC(auto_complete=False) stored_id = rpc.add_session(active=False, task_id=identity.task_id) runtime_id = "runtime-session" rpc.states[runtime_id] = rpc.states.pop(stored_id) def resume(**kwargs): rpc.calls.append(("resume", dict(kwargs))) return {"session_id": runtime_id} observed: dict[str, str] = {} original_history = rpc.history original_info = rpc.info def history(**kwargs): observed["history"] = kwargs["session_id"] return original_history(**kwargs) def info(**kwargs): observed["info"] = kwargs["session_id"] return original_info(**kwargs) rpc.resume = resume rpc.history = history rpc.info = info runtime = _runtime(db, rpc, clock=clock) retried = runtime.retry_indeterminate(identity) assert retried["status"] == "queued" assert observed == {"history": runtime_id, "info": runtime_id} def test_retry_reconciles_terminal_remote_cancellation_without_new_generation( db: Path, ): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) _admit(db, identity) attempt = state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) now[0] = 102.0 recovery_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="recovery-process", ttl_seconds=30, clock=clock, ) state.recover_room(db, recovery_lease, clock=clock) state.release_lease(db, recovery_lease, clock=clock) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session(active=False, task_id=identity.task_id) original_info = rpc.info def cancelled_info(**kwargs): return {**original_info(**kwargs), "status": "cancelled"} rpc.info = cancelled_info runtime = _runtime(db, rpc, clock=clock) cancelled = runtime.retry_indeterminate(identity) assert cancelled["status"] == "cancelled" assert cancelled["execution_generation"] == attempt.execution_generation assert not [call for call in rpc.calls if call[0] == "submit"] def test_ambiguous_recovery_remains_indeterminate(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=0.2, clock=clock, ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session(active=False, task_id=identity.task_id) now[0] = 101.0 runtime = _runtime(db, rpc, clock=clock) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "indeterminate") assert runtime.stop(timeout=5.0) assert not [call for call in rpc.calls if call[0] == "submit"] def test_offline_member_defers_then_healthy_task_runs_and_retry_is_fenced( db: Path, ): now = [100.0] def clock(): return now[0] first = _identity("task-offline") second = state.TaskIdentity( room_id=ROOM_ID, task_id="task-healthy", thread_id="thread-1", turn_id="turn-task-healthy", ) state.admit_task( db, first, payload={ "target_profile": PROFILE, "prompt": "Try the offline member.", "source_event_seq": 1, }, clock=clock, ) old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) old_attempt = state.start_task( db, first, old_lease, expected_cancel_generation=0, clock=clock, ) state.admit_task( db, second, payload={ "target_profile": PROFILE, "prompt": "Continue with the healthy member.", "source_event_seq": 2, }, clock=clock, ) now[0] = 102.0 rpc = FakeSessionRPC() published = [] runtime = _runtime( db, rpc, clock=clock, lease_ttl_seconds=30, indeterminate_defer_seconds=5, publish_terminal=lambda _binding, task: published.append(task), ) runtime._process_room(BINDING) assert state.get_task(db, first)["status"] == "indeterminate" assert state.get_task(db, second)["status"] == "queued" now[0] = 108.0 runtime._process_room(BINDING) assert state.get_task(db, first)["status"] == "deferred" assert state.get_task(db, second)["status"] == "settled" assert [task["status"] for task in published] == ["deferred", "settled"] assert ROOM_ID not in runtime.status()["blocked_rooms"] requeued = runtime.retry_indeterminate(first) assert requeued["status"] == "queued" lease = runtime._leases[ROOM_ID] retry_attempt = state.start_task( db, first, lease, expected_cancel_generation=0, clock=clock, ) assert retry_attempt.execution_generation == old_attempt.execution_generation + 1 late_attempt = state.TaskAttempt( identity=first, lease=lease, execution_generation=old_attempt.execution_generation, cancel_generation=old_attempt.cancel_generation, ) with pytest.raises(state.StaleTaskError): state.settle_task( db, late_attempt, settlement_id="late-old-result", status="settled", result={"text": "too late"}, clock=clock, ) state.settle_task( db, retry_attempt, settlement_id="retry-result", status="settled", result={"text": "retry accepted"}, clock=clock, ) assert state.get_task(db, first)["result"]["text"] == "retry accepted" def test_post_submit_observation_failure_preserves_recoverable_outcome(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) rpc.history_failures = 1 runtime = _runtime(db, rpc) runtime.start() assert rpc.submitted.wait(1.0) _wait_for( lambda: ( "observation failed after submit" in str(runtime.status()["last_error"] or "") ) ) assert state.get_task(db, identity)["status"] == "running" rpc.complete(identity.task_id, content="Recovered after a transient read.") runtime.wakeup() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) task = state.get_task(db, identity) assert task["result"]["text"] == "Recovered after a transient read." assert not [call for call in rpc.calls if call[0] == "submit"][1:] def test_cancellation_is_persisted_before_interrupt_and_fences_late_result( db: Path, ): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) observed_status: list[str] = [] rpc.on_interrupt = lambda: observed_status.append( state.get_task(db, identity)["status"] ) runtime.start() assert rpc.submitted.wait(1.0) cancelled = runtime.cancel(identity, cancel_id="cancel-user") rpc.complete(identity.task_id, content="Too late.") runtime.wakeup() time.sleep(0.05) assert runtime.stop(timeout=5.0) assert cancelled["status"] == "cancelled" assert observed_status == ["stopping"] def test_transient_remote_stop_failure_stays_pending_and_retries(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) original_interrupt = rpc.interrupt attempts = 0 retry_allowed = threading.Event() def flaky_interrupt(**kwargs): nonlocal attempts attempts += 1 if attempts == 1: raise RuntimeError("temporary stop transport failure") assert retry_allowed.wait(1.0) return original_interrupt(**kwargs) rpc.interrupt = flaky_interrupt runtime.start() assert rpc.submitted.wait(1.0) stopping = runtime.cancel(identity, cancel_id="cancel-retry") assert stopping["status"] == "stopping" assert state.get_task(db, identity)["status"] == "stopping" retry_allowed.set() runtime.wakeup() _wait_for(lambda: state.get_task(db, identity)["status"] == "cancelled") assert attempts >= 2 assert runtime.stop(timeout=5.0) assert state.get_task(db, identity)["status"] == "cancelled" def test_provisional_stopping_response_does_not_acknowledge_cancellation(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation=runtime.process_generation, ttl_seconds=1, clock=time.time, ) runtime._leases[ROOM_ID] = lease attempt = state.start_task( db, identity, lease, expected_cancel_generation=0, clock=time.time, ) session_id = rpc.add_session(active=True, task_id=identity.task_id) rpc.states[session_id]["execution_generation"] = attempt.execution_generation terminal = False def peer_interrupt(**_kwargs): return {"status": "cancelled" if terminal else "stopping"} rpc.interrupt = peer_interrupt stopping = runtime.cancel(identity, cancel_id="cancel-peer") assert stopping["status"] == "stopping" assert state.get_task(db, identity)["status"] == "stopping" terminal = True cancelled = runtime.cancel(identity, cancel_id="cancel-peer") assert cancelled["status"] == "cancelled" def test_peer_terminal_status_acknowledges_durable_stop_on_retry(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation=runtime.process_generation, ttl_seconds=1, clock=time.time, ) runtime._leases[ROOM_ID] = lease attempt = state.start_task( db, identity, lease, expected_cancel_generation=0, clock=time.time, ) client = TerminalPeerClient( task_id=identity.task_id, execution_generation=attempt.execution_generation, ) runtime.transport_resolver = _peer_resolver(client) stopping = state.begin_task_cancel( db, identity, cancel_id="cancel-peer-terminal", expected_cancel_generation=0, clock=time.time, ) assert stopping["status"] == "stopping" runtime._retry_stopping_tasks(BINDING, lease) assert state.get_task(db, identity)["status"] == "cancelled" assert client.history_calls == 0 def test_peer_terminal_status_must_match_exact_task_attempt(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation=runtime.process_generation, ttl_seconds=1, clock=time.time, ) runtime._leases[ROOM_ID] = lease attempt = state.start_task( db, identity, lease, expected_cancel_generation=0, clock=time.time, ) client = TerminalPeerClient( task_id=identity.task_id, execution_generation=attempt.execution_generation, ) client.status_task_id = "different-task" runtime.transport_resolver = _peer_resolver(client) stopping = state.begin_task_cancel( db, identity, cancel_id="cancel-mismatch", expected_cancel_generation=0, clock=time.time, ) assert runtime._peer_stop_acknowledged(BINDING, stopping) is False client.status_task_id = identity.task_id client.status_generation = attempt.execution_generation + 1 assert runtime._peer_stop_acknowledged(BINDING, stopping) is False assert state.get_task(db, identity)["status"] == "stopping" def test_completion_wins_a_race_with_unacknowledged_stop(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) runtime.start() assert rpc.submitted.wait(1.0) def finish_only_after_stop_intent(): if state.get_task(db, identity)["status"] == "stopping": rpc.complete(identity.task_id, content="Already done.") rpc.on_info = finish_only_after_stop_intent result = runtime.cancel(identity, cancel_id="cancel-raced") assert result["status"] == "settled" assert result["result"]["text"] == "Already done." assert runtime.stop(timeout=5.0) def test_restart_harvests_completion_before_retrying_durable_stop(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] _admit(db, identity) old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) attempt = state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) stopping = state.begin_task_cancel( db, identity, cancel_id="cancel-before-restart", expected_cancel_generation=attempt.cancel_generation, clock=clock, ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session( active=False, task_id=identity.task_id, history=[ { "role": "assistant", "task_id": identity.task_id, "execution_generation": attempt.execution_generation, "status": "settled", "message_id": "reply-after-stop", "content": "Finished before Stop reached the session.", } ], ) now[0] = 102.0 runtime = _runtime( db, rpc, process_generation="new-process", clock=clock, ) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) settled = state.get_task(db, identity) assert stopping["status"] == "stopping" assert settled["result"]["text"] == "Finished before Stop reached the session." assert not [call for call in rpc.calls if call[0] == "interrupt"] def test_restart_acknowledges_inactive_local_stop_without_memory_marker(db: Path): identity = _identity() now = [100.0] def clock(): return now[0] _admit(db, identity) old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1, clock=clock, ) attempt = state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=clock, ) state.begin_task_cancel( db, identity, cancel_id="cancel-before-restart", expected_cancel_generation=attempt.cancel_generation, clock=clock, ) rpc = FakeSessionRPC(auto_complete=False) now[0] = 102.0 runtime = _runtime( db, rpc, process_generation="new-process", clock=clock, ) cancelled = runtime.cancel(identity, cancel_id="cancel-before-restart") assert cancelled["status"] == "cancelled" assert not [call for call in rpc.calls if call[0] == "interrupt"] def test_stop_resumes_persisted_session_before_reading_runtime_history(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation=runtime.process_generation, ttl_seconds=1, clock=time.time, ) runtime._leases[ROOM_ID] = lease attempt = state.start_task( db, identity, lease, expected_cancel_generation=0, clock=time.time, ) stored_id = rpc.add_session(active=False, task_id=identity.task_id) runtime_id = "runtime-session" rpc.states[runtime_id] = rpc.states.pop(stored_id) def resume(**kwargs): events.append("resume") return {"session_id": runtime_id} original_history = rpc.history def history(**kwargs): events.append("history") return original_history(**kwargs) events: list[str] = [] rpc.resume = resume rpc.history = history state.begin_task_cancel( db, identity, cancel_id="cancel-remapped", expected_cancel_generation=attempt.cancel_generation, clock=time.time, ) cancelled = runtime.cancel(identity, cancel_id="cancel-remapped") assert cancelled["status"] == "cancelled" assert events.index("resume") < events.index("history") def test_pending_local_approval_is_reported_with_safe_choices(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) actions = [] runtime = _runtime( db, rpc, pending_action=lambda room_id, member_id, action: actions.append(( room_id, member_id, action, )), ) runtime.start() assert rpc.submitted.wait(1.0) session_id = next(iter(rpc.states)) with rpc._lock: rpc.states[session_id]["pending_approval"] = { "request_id": "approval-1", "command": "pytest -q tests/focused", "choices": ["once", "session", "always", "deny"], } runtime.wakeup() _wait_for(lambda: any(action for _room, _member, action in actions)) _room, member, action = next(item for item in actions if item[2] is not None) assert member == PROFILE assert action["request_id"] == "approval-1" assert action["approval"]["choices"] == ["once", "deny"] assert runtime.stop(timeout=5.0) def test_cancel_never_interrupts_a_newer_task_in_the_same_session(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc) runtime.start() assert rpc.submitted.wait(1.0) session_id = next(iter(rpc.states)) def switch_to_newer_task() -> None: with rpc._lock: rpc.states[session_id]["active"] = True rpc.states[session_id]["task_id"] = "task-2" rpc.on_info = switch_to_newer_task cancelled = runtime.cancel(identity, cancel_id="cancel-old-task") assert cancelled["status"] == "stopping" assert not [call for call in rpc.calls if call[0] == "interrupt"] skipped = [params for method, params in rpc.calls if method == "interrupt_skipped"] assert all(params["expected_task_id"] == identity.task_id for params in skipped) assert rpc.states[session_id]["active"] is True assert rpc.states[session_id]["task_id"] == "task-2" assert runtime.stop(timeout=5.0) def test_status_reports_room_blocked_on_unresolved_indeterminate_task(db: Path): identity = _identity() now = [100.0] old_lease = state.acquire_lease( db, room_id=ROOM_ID, gateway_id=BINDING.gateway_id, authority_epoch=BINDING.authority_epoch, process_generation="old-process", ttl_seconds=1.0, clock=lambda: now[0], ) _admit(db, identity) state.start_task( db, identity, old_lease, expected_cancel_generation=0, clock=lambda: now[0], ) rpc = FakeSessionRPC(auto_complete=False) rpc.add_session(active=False, task_id=identity.task_id) now[0] += 2.0 runtime = _runtime(db, rpc, clock=lambda: now[0]) runtime.start() _wait_for(lambda: ROOM_ID in runtime.status()["blocked_rooms"]) assert runtime.stop(timeout=5.0) assert state.get_task(db, identity)["status"] == "indeterminate" def test_authority_loss_stops_terminal_commit(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) # Generous lease TTL: this test is about AUTHORITY loss. A short TTL let # a loaded CI runner expire the lease before the authority change was # observed, so last_error flipped to "driver lease is stale or expired" # (flaky main run 33455779041). runtime = _runtime(db, rpc, lease_ttl_seconds=30.0) runtime.start() assert rpc.submitted.wait(1.0) hosted_rooms.claim_authority( db, room_id=ROOM_ID, expected_gateway_id="gateway-a", expected_epoch=1, new_gateway_id="gateway-b", event_id="claim-gateway-b", now=time.time(), ) rpc.complete(identity.task_id) runtime.wakeup() _wait_for(lambda: runtime.status()["last_error"] is not None) assert runtime.stop(timeout=5.0) assert state.get_task(db, identity)["status"] == "running" assert "authority changed" in runtime.status()["last_error"] def test_profile_turn_lock_covers_resolve_submit_and_terminal_observation(db: Path): identity = _identity() _admit(db, identity) locks = RecordingTurnLocks() rpc = FakeSessionRPC(required_lock=locks) runtime = _runtime(db, rpc, locks) runtime.start() _wait_for(lambda: state.get_task(db, identity)["status"] == "settled") assert runtime.stop(timeout=5.0) assert locks.events == [("lock-enter", PROFILE), ("lock-exit", PROFILE)] methods = [method for method, _params in rpc.calls] assert methods.index("resolve_exact") < methods.index("submit") assert methods.index("submit") < methods.index("complete") assert "history" not in methods def test_stop_is_bounded_and_does_not_interrupt_active_turn(db: Path): identity = _identity() _admit(db, identity) rpc = FakeSessionRPC(auto_complete=False) runtime = _runtime(db, rpc, poll_interval_seconds=0.01) runtime.start() assert rpc.submitted.wait(1.0) started = time.monotonic() stopped = runtime.stop(timeout=0.5) assert stopped is True assert time.monotonic() - started < 0.5 assert state.get_task(db, identity)["status"] == "running" assert not [call for call in rpc.calls if call[0] == "interrupt"]