tui_gateway: blocking prompts wait for the human (drop _block timeouts)
This commit is contained in:
parent
ad220b9d93
commit
76a8bba15f
@ -3580,6 +3580,90 @@ def test_respond_unpacks_sid_tuple_correctly():
|
|||||||
server._answers.pop("rid-x", None)
|
server._answers.pop("rid-x", None)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Blocking prompts wait for the human (v6 north-star #5): _block with
|
||||||
|
# timeout=None must never expire — interrupt/shutdown (_clear_pending)
|
||||||
|
# are the only releases.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _run_block_in_thread(monkeypatch, sid):
|
||||||
|
"""Start _block(timeout=None) on a background thread; return
|
||||||
|
(thread, results, get_rid) where get_rid polls for the pending rid."""
|
||||||
|
monkeypatch.setattr(server, "_emit", lambda *a, **kw: None)
|
||||||
|
results: list[str] = []
|
||||||
|
|
||||||
|
def runner():
|
||||||
|
results.append(server._block("clarify.request", sid, {"question": "q"}))
|
||||||
|
|
||||||
|
t = threading.Thread(target=runner, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def get_rid():
|
||||||
|
deadline = time.time() + 5
|
||||||
|
while time.time() < deadline:
|
||||||
|
with server._prompt_lock:
|
||||||
|
for rid, (owner, _ev) in server._pending.items():
|
||||||
|
if owner == sid:
|
||||||
|
return rid
|
||||||
|
time.sleep(0.01)
|
||||||
|
raise AssertionError("pending rid never appeared for sid=%s" % sid)
|
||||||
|
|
||||||
|
return t, results, get_rid
|
||||||
|
|
||||||
|
|
||||||
|
def test_block_no_timeout_waits_for_delayed_answer(monkeypatch):
|
||||||
|
"""_block(timeout=None) must keep blocking until the answer arrives —
|
||||||
|
no premature empty return."""
|
||||||
|
t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_wait")
|
||||||
|
rid = get_rid()
|
||||||
|
|
||||||
|
# Answer after a short delay; _block must still be waiting.
|
||||||
|
time.sleep(0.3)
|
||||||
|
assert t.is_alive(), "_block returned before any answer was provided"
|
||||||
|
with server._prompt_lock:
|
||||||
|
server._answers[rid] = "green"
|
||||||
|
server._pending[rid][1].set()
|
||||||
|
|
||||||
|
t.join(timeout=5)
|
||||||
|
assert not t.is_alive()
|
||||||
|
assert results == ["green"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_pending_releases_no_timeout_block(monkeypatch):
|
||||||
|
"""_clear_pending(sid) must release a timeout=None _block with ''."""
|
||||||
|
t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_clear")
|
||||||
|
get_rid()
|
||||||
|
|
||||||
|
server._clear_pending("sid_block_clear")
|
||||||
|
t.join(timeout=5)
|
||||||
|
assert not t.is_alive()
|
||||||
|
assert results == [""]
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_pending_other_sid_does_not_release_block(monkeypatch):
|
||||||
|
"""_clear_pending on an unrelated session must NOT release a pending
|
||||||
|
timeout=None _block (session scoping)."""
|
||||||
|
t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_scoped")
|
||||||
|
rid = get_rid()
|
||||||
|
|
||||||
|
server._clear_pending("sid_some_other_session")
|
||||||
|
time.sleep(0.2)
|
||||||
|
assert t.is_alive(), (
|
||||||
|
"_clear_pending on another sid released a prompt owned by "
|
||||||
|
"sid_block_scoped — session scoping is broken"
|
||||||
|
)
|
||||||
|
assert not results
|
||||||
|
|
||||||
|
# Clean up: release properly so the thread joins.
|
||||||
|
with server._prompt_lock:
|
||||||
|
server._answers[rid] = "done"
|
||||||
|
server._pending[rid][1].set()
|
||||||
|
t.join(timeout=5)
|
||||||
|
assert not t.is_alive()
|
||||||
|
assert results == ["done"]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /model switch and other agent-mutating commands must reject while the
|
# /model switch and other agent-mutating commands must reject while the
|
||||||
# session is running. agent.switch_model() mutates self.model, self.provider,
|
# session is running. agent.switch_model() mutates self.model, self.provider,
|
||||||
|
|||||||
@ -1066,7 +1066,11 @@ def _enable_gateway_prompts() -> None:
|
|||||||
# ── Blocking prompt factory ──────────────────────────────────────────
|
# ── Blocking prompt factory ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _block(event: str, sid: str, payload: dict, timeout: int = 300) -> str:
|
def _block(event: str, sid: str, payload: dict, timeout: int | None = None) -> str:
|
||||||
|
# Blocking prompts wait for the human; interrupt/shutdown (via
|
||||||
|
# _clear_pending) are the only releases — v6 north-star #5. A
|
||||||
|
# timeout would orphan the TUI prompt and silently feed the agent
|
||||||
|
# an empty answer, so callers default to waiting forever.
|
||||||
rid = uuid.uuid4().hex[:8]
|
rid = uuid.uuid4().hex[:8]
|
||||||
ev = threading.Event()
|
ev = threading.Event()
|
||||||
with _prompt_lock:
|
with _prompt_lock:
|
||||||
@ -2328,7 +2332,7 @@ def _wire_callbacks(sid: str):
|
|||||||
from tools.terminal_tool import set_sudo_password_callback
|
from tools.terminal_tool import set_sudo_password_callback
|
||||||
from tools.skills_tool import set_secret_capture_callback
|
from tools.skills_tool import set_secret_capture_callback
|
||||||
|
|
||||||
set_sudo_password_callback(lambda: _block("sudo.request", sid, {}, timeout=120))
|
set_sudo_password_callback(lambda: _block("sudo.request", sid, {}))
|
||||||
|
|
||||||
def secret_cb(env_var, prompt, metadata=None):
|
def secret_cb(env_var, prompt, metadata=None):
|
||||||
pl = {"prompt": prompt, "env_var": env_var}
|
pl = {"prompt": prompt, "env_var": env_var}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user