AITURK IDE 1.0.0-beta.18: gorunmeyen onay beklemesi yerine aninda "onay gerekli"
A one-shot run (hermes chat --query-file ... -Q, stdin closed, no window)
still installs the prompt_toolkit approval callback but never starts the
prompt UI. The protected instruction-file gate (AGENTS.md / CLAUDE.md /
SOUL.md ...) took the registered callback for a reachable human and waited
out approvals.timeout on a panel painted nowhere. With the profile value of
300000 s the stage job hung for good.
- Gate: in a one-shot run, or when only an automatic approver
(subagent_auto_approve) is attached, return BLOCKED / APPROVAL REQUIRED
immediately instead of prompting. Human consent is still required every
time; yolo and approvals.single_query_mode still do not open this gate.
- One-shot exit prints an "approval_required: {json}" line on stderr
(session_id, tool_call_id, request_id, targets, reason, applied=false)
so the launcher can tell "blocked, needs a human" from "still working".
- execute_code: an approval that arrives after the calling script stopped
waiting (300 s reply timeout, script run timeout, retired kernel cell)
is not applied.
- Same invisible wait removed from the computer_use and memory-write
prompts in a one-shot run. The one-shot marker is no longer inherited by
child processes.
- BLOCKED text no longer breaks on a target path containing braces.
Long-running tools are untouched: no general tool timeout was shortened.
Kart: turkservis-20261007-1421-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
This commit is contained in:
1 parent
58dbb4523b
commit
7e4cc0915c
13 files changed
+886
-16
No files matched your search
@@ -0,0 +1,578 @@
|
||||
"""Protected instruction-file gate when no human can be reached.
|
||||
|
||||
A one-shot run (``chat -q`` / ``--query-file``) installs the CLI approval
|
||||
callback but never starts the prompt UI. The gate used to treat the
|
||||
registered callback as a reachable human and wait out ``approvals.timeout``
|
||||
on a panel nobody could see. These tests pin the fixed contract:
|
||||
|
||||
* no reachable human -> BLOCKED at once, nothing written, the outcome is
|
||||
recorded for the launching process;
|
||||
* the gate itself is unchanged: still asks a real human every time, still
|
||||
ignores yolo and ``approvals.single_query_mode``;
|
||||
* an approval that arrives after the calling execute_code script stopped
|
||||
waiting is not applied.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _gate_on(monkeypatch):
|
||||
import tools.approval as A
|
||||
import tools.file_tools as ft
|
||||
|
||||
monkeypatch.setattr(ft, "_protected_instruction_config", lambda: (True, []))
|
||||
monkeypatch.delenv("HERMES_SINGLE_QUERY_SESSION", raising=False)
|
||||
A.drain_human_approval_required()
|
||||
A.set_approval_reply_deadline(None)
|
||||
yield
|
||||
A.drain_human_approval_required()
|
||||
A.set_approval_reply_deadline(None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def approvals():
|
||||
"""CLI approval callback that records calls and gives a scripted answer."""
|
||||
from tools.terminal_tool import set_approval_callback
|
||||
|
||||
state = {"calls": [], "answer": "deny"}
|
||||
|
||||
def cb(command, description, **kwargs):
|
||||
state["calls"].append({"command": command, **kwargs})
|
||||
answer = state["answer"]
|
||||
if callable(answer):
|
||||
return answer()
|
||||
return answer
|
||||
|
||||
set_approval_callback(cb)
|
||||
yield state
|
||||
set_approval_callback(None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def one_shot(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_SINGLE_QUERY_SESSION", "1")
|
||||
|
||||
|
||||
def _write(path, content="injected"):
|
||||
from tools.file_tools import write_file_tool
|
||||
|
||||
return json.loads(write_file_tool(str(path), content))
|
||||
|
||||
|
||||
def _records():
|
||||
import tools.approval as A
|
||||
|
||||
return A.drain_human_approval_required()
|
||||
|
||||
|
||||
# ---- one-shot run: callback registered, nobody to answer ------------------
|
||||
|
||||
|
||||
class TestOneShotRun:
|
||||
def test_blocks_at_once_without_prompting(self, tmp_path, approvals, one_shot):
|
||||
target = tmp_path / "AGENTS.md"
|
||||
target.write_text("rules\n", encoding="utf-8")
|
||||
approvals["answer"] = "once" # would be honored if anyone were asked
|
||||
|
||||
started = time.monotonic()
|
||||
res = _write(target)
|
||||
elapsed = time.monotonic() - started
|
||||
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert "APPROVAL REQUIRED" in res["error"]
|
||||
assert target.read_text(encoding="utf-8") == "rules\n"
|
||||
assert approvals["calls"] == [], "prompted on a surface nobody can see"
|
||||
assert elapsed < 5
|
||||
|
||||
def test_outcome_is_recorded_for_the_launcher(self, tmp_path, approvals, one_shot):
|
||||
import tools.approval as A
|
||||
|
||||
target = tmp_path / "CLAUDE.md"
|
||||
sid = A._approval_session_id.set("sess-123")
|
||||
tcid = A._approval_tool_call_id.set("call-9")
|
||||
try:
|
||||
_write(target)
|
||||
finally:
|
||||
A._approval_tool_call_id.reset(tcid)
|
||||
A._approval_session_id.reset(sid)
|
||||
|
||||
records = _records()
|
||||
assert len(records) == 1
|
||||
rec = records[0]
|
||||
assert rec["kind"] == "protected_instruction_file"
|
||||
assert rec["reason"] == "no_interactive_user"
|
||||
assert rec["applied"] is False
|
||||
assert rec["session_id"] == "sess-123"
|
||||
assert rec["tool_call_id"] == "call-9"
|
||||
assert rec["request_id"]
|
||||
assert any("CLAUDE.md" in t for t in rec["targets"])
|
||||
assert _records() == [], "drain must clear"
|
||||
|
||||
def test_yolo_does_not_open_the_gate(self, tmp_path, approvals, one_shot, monkeypatch):
|
||||
import tools.approval as A
|
||||
|
||||
monkeypatch.setattr(A, "_YOLO_MODE_FROZEN", True)
|
||||
target = tmp_path / "AGENTS.md"
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
|
||||
def test_single_query_mode_approve_does_not_open_the_gate(
|
||||
self, tmp_path, approvals, one_shot, monkeypatch
|
||||
):
|
||||
import tools.approval as A
|
||||
|
||||
real_cfg_get = A.cfg_get
|
||||
|
||||
def fake_cfg_get(config, *keys, default=None):
|
||||
if keys == ("approvals", "single_query_mode"):
|
||||
return "approve"
|
||||
return real_cfg_get(config, *keys, default=default)
|
||||
|
||||
monkeypatch.setattr(A, "cfg_get", fake_cfg_get)
|
||||
target = tmp_path / "SOUL.md"
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
|
||||
def test_regular_file_still_writes(self, tmp_path, approvals, one_shot):
|
||||
target = tmp_path / "notes.md"
|
||||
res = _write(target, "hello")
|
||||
assert not res.get("error"), res
|
||||
assert target.read_text(encoding="utf-8") == "hello"
|
||||
assert _records() == []
|
||||
|
||||
def test_multi_file_patch_applies_nothing(self, tmp_path, approvals, one_shot):
|
||||
from tools.file_tools import patch_tool
|
||||
|
||||
agents = tmp_path / "AGENTS.md"
|
||||
agents.write_text("rules\n", encoding="utf-8")
|
||||
plain = tmp_path / "plain.txt"
|
||||
plain.write_text("hello\n", encoding="utf-8")
|
||||
patch = (
|
||||
"*** Begin Patch\n"
|
||||
f"*** Update File: {plain}\n"
|
||||
"@@\n"
|
||||
"-hello\n"
|
||||
"+world\n"
|
||||
f"*** Update File: {agents}\n"
|
||||
"@@\n"
|
||||
"-rules\n"
|
||||
"+injected\n"
|
||||
"*** End Patch"
|
||||
)
|
||||
res = json.loads(patch_tool(mode="patch", patch=patch))
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert plain.read_text(encoding="utf-8") == "hello\n"
|
||||
assert agents.read_text(encoding="utf-8") == "rules\n"
|
||||
assert approvals["calls"] == []
|
||||
|
||||
def test_retry_in_same_run_blocks_again(self, tmp_path, approvals, one_shot):
|
||||
target = tmp_path / "AGENTS.md"
|
||||
_write(target)
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
assert len(_records()) == 2
|
||||
|
||||
|
||||
# ---- no channel at all ----------------------------------------------------
|
||||
|
||||
|
||||
def test_no_channel_fails_closed_and_is_recorded(tmp_path):
|
||||
target = tmp_path / "AGENTS.md"
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
records = _records()
|
||||
assert [r["reason"] for r in records] == ["no_approval_channel"]
|
||||
|
||||
|
||||
def test_undeliverable_gateway_request_is_recorded(tmp_path):
|
||||
import tools.approval as A
|
||||
|
||||
session_key = "protected-unattended-undelivered"
|
||||
token = A.set_current_session_key(session_key)
|
||||
try:
|
||||
def notify(approval_data):
|
||||
raise RuntimeError("client went away")
|
||||
|
||||
A.register_gateway_notify(session_key, notify)
|
||||
try:
|
||||
target = tmp_path / "AGENTS.md"
|
||||
res = _write(target)
|
||||
finally:
|
||||
A.unregister_gateway_notify(session_key)
|
||||
finally:
|
||||
A.reset_current_session_key(token)
|
||||
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
assert [r["reason"] for r in _records()] == ["approval_not_delivered"]
|
||||
|
||||
|
||||
# ---- a reachable human: the gate behaves exactly as before -----------------
|
||||
|
||||
|
||||
class TestReachableHuman:
|
||||
def test_allow_writes(self, tmp_path, approvals):
|
||||
target = tmp_path / "AGENTS.md"
|
||||
approvals["answer"] = "once"
|
||||
res = _write(target, "approved")
|
||||
assert not res.get("error"), res
|
||||
assert target.read_text(encoding="utf-8") == "approved"
|
||||
assert len(approvals["calls"]) == 1
|
||||
assert _records() == []
|
||||
|
||||
@pytest.mark.parametrize("answer", ["deny", "timeout"])
|
||||
def test_deny_and_silence_block(self, tmp_path, approvals, answer):
|
||||
target = tmp_path / "AGENTS.md"
|
||||
approvals["answer"] = answer
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
assert len(approvals["calls"]) == 1
|
||||
# A human was asked and did not consent: not an "unattended" outcome.
|
||||
assert _records() == []
|
||||
|
||||
def test_prompt_that_dies_blocks(self, tmp_path, approvals):
|
||||
"""Cancel / lost surface: the prompt raising is not consent."""
|
||||
def boom():
|
||||
raise RuntimeError("prompt surface lost")
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
approvals["answer"] = boom
|
||||
res = _write(target)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
|
||||
|
||||
# ---- approval that arrives after the caller stopped waiting ----------------
|
||||
|
||||
|
||||
class TestLateApproval:
|
||||
def test_late_cli_approval_is_not_applied(self, tmp_path, approvals):
|
||||
import tools.approval as A
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
target.write_text("rules\n", encoding="utf-8")
|
||||
|
||||
def answer_after_deadline():
|
||||
# The human says yes, but only once the execute_code script that
|
||||
# asked for this write has already timed out.
|
||||
A.set_approval_reply_deadline(time.monotonic() - 1)
|
||||
return "once"
|
||||
|
||||
A.set_approval_reply_deadline(time.monotonic() + 300)
|
||||
approvals["answer"] = answer_after_deadline
|
||||
res = _write(target)
|
||||
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert "NOT applied" in res["error"]
|
||||
assert target.read_text(encoding="utf-8") == "rules\n"
|
||||
|
||||
def test_late_approval_does_not_carry_to_the_next_request(self, tmp_path, approvals):
|
||||
import tools.approval as A
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
A.set_approval_reply_deadline(time.monotonic() - 1)
|
||||
approvals["answer"] = "once"
|
||||
assert "BLOCKED" in _write(target).get("error", "")
|
||||
assert not target.exists()
|
||||
|
||||
# A fresh call (new deadline) asks again; the old yes is not reused.
|
||||
A.set_approval_reply_deadline(time.monotonic() + 300)
|
||||
approvals["answer"] = "deny"
|
||||
assert "BLOCKED" in _write(target).get("error", "")
|
||||
assert not target.exists()
|
||||
assert len(approvals["calls"]) == 2
|
||||
|
||||
def test_in_time_approval_is_applied(self, tmp_path, approvals):
|
||||
import tools.approval as A
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
A.set_approval_reply_deadline(time.monotonic() + 300)
|
||||
approvals["answer"] = "once"
|
||||
res = _write(target, "approved")
|
||||
assert not res.get("error"), res
|
||||
assert target.read_text(encoding="utf-8") == "approved"
|
||||
|
||||
def test_late_gateway_approval_is_not_applied(self, tmp_path):
|
||||
import tools.approval as A
|
||||
|
||||
session_key = "protected-unattended-late"
|
||||
token = A.set_current_session_key(session_key)
|
||||
try:
|
||||
def notify(approval_data):
|
||||
A.resolve_gateway_approval(session_key, "once")
|
||||
|
||||
A.register_gateway_notify(session_key, notify)
|
||||
try:
|
||||
A.set_approval_reply_deadline(time.monotonic() - 1)
|
||||
target = tmp_path / "AGENTS.md"
|
||||
res = _write(target)
|
||||
finally:
|
||||
A.unregister_gateway_notify(session_key)
|
||||
finally:
|
||||
A.reset_current_session_key(token)
|
||||
|
||||
assert res.get("error") and "NOT applied" in res["error"]
|
||||
assert not target.exists()
|
||||
|
||||
def test_no_deadline_outside_execute_code(self):
|
||||
import tools.approval as A
|
||||
|
||||
assert A.approval_reply_window_remaining() is None
|
||||
assert A.approval_arrived_too_late() is False
|
||||
|
||||
def test_rpc_dispatch_binds_and_restores_the_deadline(self):
|
||||
import tools.approval as A
|
||||
import tools.code_execution_tool as cet
|
||||
|
||||
start = time.monotonic()
|
||||
previous = cet._bind_rpc_reply_deadline(start)
|
||||
try:
|
||||
remaining = A.approval_reply_window_remaining()
|
||||
assert remaining is not None
|
||||
assert 0 < remaining <= cet._RPC_REPLY_TIMEOUT_SECONDS
|
||||
finally:
|
||||
cet._restore_rpc_reply_deadline(previous)
|
||||
assert A.approval_reply_window_remaining() is None
|
||||
|
||||
def test_deadline_is_per_thread(self):
|
||||
import threading
|
||||
|
||||
import tools.approval as A
|
||||
|
||||
A.set_approval_reply_deadline(time.monotonic() - 1)
|
||||
seen = {}
|
||||
|
||||
def other():
|
||||
seen["late"] = A.approval_arrived_too_late()
|
||||
|
||||
worker = threading.Thread(target=other)
|
||||
worker.start()
|
||||
worker.join()
|
||||
assert seen["late"] is False
|
||||
assert A.approval_arrived_too_late() is True
|
||||
|
||||
|
||||
# ---- what the launching process sees ---------------------------------------
|
||||
|
||||
|
||||
def test_recorded_outcomes_are_bounded():
|
||||
import tools.approval as A
|
||||
|
||||
for i in range(A._HUMAN_APPROVAL_REQUIRED_MAX + 20):
|
||||
A.note_human_approval_required("protected_instruction_file", [f"f{i}"], "x")
|
||||
records = A.drain_human_approval_required()
|
||||
assert len(records) == A._HUMAN_APPROVAL_REQUIRED_MAX
|
||||
assert records[-1]["targets"] == [f"f{A._HUMAN_APPROVAL_REQUIRED_MAX + 19}"]
|
||||
|
||||
|
||||
def test_one_shot_exit_reports_approval_required_on_stderr(capsys):
|
||||
import cli as cli_module
|
||||
import tools.approval as A
|
||||
|
||||
A.note_human_approval_required(
|
||||
"protected_instruction_file", ["AGENTS.md"], "no_interactive_user"
|
||||
)
|
||||
cli_module._emit_approval_required("sess-42")
|
||||
|
||||
err = capsys.readouterr().err
|
||||
lines = [l for l in err.splitlines() if l.startswith("approval_required: ")]
|
||||
assert len(lines) == 1
|
||||
payload = json.loads(lines[0].split(": ", 1)[1])
|
||||
assert payload["session_id"] == "sess-42"
|
||||
assert payload["applied"] is False
|
||||
assert payload["reason"] == "no_interactive_user"
|
||||
assert payload["resume"] == "interactive"
|
||||
assert payload["targets"] == ["AGENTS.md"]
|
||||
|
||||
cli_module._emit_approval_required("sess-42")
|
||||
assert "approval_required" not in capsys.readouterr().err
|
||||
|
||||
|
||||
# ---- execute_code RPC: the dispatcher really binds the window ---------------
|
||||
|
||||
|
||||
def _drive_rpc_once(dispatch, stop_event=None):
|
||||
"""Send one tool call through the real RPC server loop; return its reply."""
|
||||
import socket
|
||||
import threading
|
||||
|
||||
import tools.code_execution_tool as cet
|
||||
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.bind(("127.0.0.1", 0))
|
||||
server.listen(1)
|
||||
stop_event = stop_event or threading.Event()
|
||||
worker = threading.Thread(
|
||||
target=cet._rpc_server_loop,
|
||||
args=(server, "task", [], [0], 5, frozenset({"patch"}), stop_event, "tok", dispatch),
|
||||
daemon=True,
|
||||
)
|
||||
worker.start()
|
||||
client = socket.create_connection(server.getsockname(), timeout=10)
|
||||
try:
|
||||
client.sendall(
|
||||
(json.dumps({"tool": "patch", "args": {}, "token": "tok"}) + "\n").encode()
|
||||
)
|
||||
buf = b""
|
||||
while not buf.endswith(b"\n"):
|
||||
chunk = client.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
finally:
|
||||
client.close()
|
||||
stop_event.set()
|
||||
worker.join(timeout=10)
|
||||
server.close()
|
||||
return buf.decode().strip()
|
||||
|
||||
|
||||
def test_rpc_server_loop_binds_reply_window_around_dispatch():
|
||||
import tools.approval as A
|
||||
import tools.code_execution_tool as cet
|
||||
|
||||
seen = {}
|
||||
|
||||
def dispatch(tool_name, tool_args):
|
||||
seen["remaining"] = A.approval_reply_window_remaining()
|
||||
seen["late"] = A.approval_arrived_too_late()
|
||||
return json.dumps({"ok": True})
|
||||
|
||||
reply = _drive_rpc_once(dispatch)
|
||||
assert json.loads(reply) == {"ok": True}
|
||||
assert seen["late"] is False
|
||||
assert 0 < seen["remaining"] <= cet._RPC_REPLY_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
def test_rpc_approval_after_script_ended_is_late():
|
||||
"""Script killed by its own run timeout while the prompt is still open."""
|
||||
import threading
|
||||
|
||||
import tools.approval as A
|
||||
|
||||
stop_event = threading.Event()
|
||||
seen = {}
|
||||
|
||||
def dispatch(tool_name, tool_args):
|
||||
seen["before"] = A.approval_arrived_too_late()
|
||||
stop_event.set() # the script ended while a human was deciding
|
||||
seen["after"] = A.approval_arrived_too_late()
|
||||
return json.dumps({"ok": True})
|
||||
|
||||
_drive_rpc_once(dispatch, stop_event)
|
||||
assert seen == {"before": False, "after": True}
|
||||
|
||||
|
||||
def test_protected_write_through_rpc_is_dropped_when_script_ended(tmp_path, approvals):
|
||||
"""End to end through the RPC loop: late yes -> BLOCKED, file untouched."""
|
||||
import threading
|
||||
|
||||
from tools.file_tools import write_file_tool
|
||||
from tools.terminal_tool import _get_approval_callback, set_approval_callback
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
target.write_text("rules\n", encoding="utf-8")
|
||||
stop_event = threading.Event()
|
||||
callback = _get_approval_callback()
|
||||
|
||||
def answer_after_script_died():
|
||||
stop_event.set()
|
||||
return "once"
|
||||
|
||||
approvals["answer"] = answer_after_script_died
|
||||
|
||||
def dispatch(tool_name, tool_args):
|
||||
set_approval_callback(callback) # the RPC thread carries the cell's callback
|
||||
try:
|
||||
return write_file_tool(str(target), "injected")
|
||||
finally:
|
||||
set_approval_callback(None)
|
||||
|
||||
reply = json.loads(_drive_rpc_once(dispatch, stop_event))
|
||||
assert "NOT applied" in reply.get("error", "")
|
||||
assert target.read_text(encoding="utf-8") == "rules\n"
|
||||
assert len(approvals["calls"]) == 1
|
||||
|
||||
|
||||
def test_retired_kernel_cell_counts_as_gone():
|
||||
import tools.approval as A
|
||||
|
||||
cell = {"active": True}
|
||||
previous = A.also_gone_when(lambda: not cell["active"])
|
||||
try:
|
||||
assert A.approval_arrived_too_late() is False
|
||||
cell["active"] = False
|
||||
assert A.approval_arrived_too_late() is True
|
||||
finally:
|
||||
A.restore_approval_reply_deadline(previous)
|
||||
assert A.approval_arrived_too_late() is False
|
||||
|
||||
|
||||
# ---- an automatic approver is not a person ----------------------------------
|
||||
|
||||
|
||||
def test_subagent_auto_approve_cannot_open_the_gate(tmp_path):
|
||||
from tools.delegate_tool import _subagent_auto_approve
|
||||
from tools.terminal_tool import set_approval_callback
|
||||
|
||||
target = tmp_path / "AGENTS.md"
|
||||
set_approval_callback(_subagent_auto_approve)
|
||||
try:
|
||||
res = _write(target)
|
||||
finally:
|
||||
set_approval_callback(None)
|
||||
assert res.get("error") and "BLOCKED" in res["error"]
|
||||
assert not target.exists()
|
||||
assert [r["reason"] for r in _records()] == ["no_interactive_user"]
|
||||
|
||||
|
||||
def test_blocked_message_survives_braces_in_target(tmp_path):
|
||||
import tools.file_tools as ft
|
||||
|
||||
res = ft._request_protected_instruction_approval(["{be}/AGENTS.md"])
|
||||
assert res.startswith("BLOCKED") and "{be}/AGENTS.md" in res
|
||||
|
||||
|
||||
# ---- sibling gates in a one-shot run ----------------------------------------
|
||||
|
||||
|
||||
def test_computer_use_does_not_wait_in_one_shot_run(one_shot, monkeypatch):
|
||||
import tools.computer_use.tool as cu
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(cu, "_approval_callback", lambda *a, **k: calls.append(a) or "approve_once")
|
||||
res = cu._request_approval("click", {}, session_id="unattended-test")
|
||||
assert res is not None and "one-shot run" in json.loads(res)["error"]
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_computer_use_still_asks_when_interactive(monkeypatch):
|
||||
import tools.computer_use.tool as cu
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(cu, "_approval_callback", lambda *a, **k: calls.append(a) or "approve_once")
|
||||
assert cu._request_approval("click", {}, session_id="attended-test") is None
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_memory_write_prompt_unavailable_in_one_shot_run(approvals, monkeypatch):
|
||||
import tools.write_approval as wa
|
||||
|
||||
assert wa._interactive_approval_available() is True
|
||||
monkeypatch.setenv("HERMES_SINGLE_QUERY_SESSION", "1")
|
||||
assert wa._interactive_approval_available() is False
|
||||
|
||||
|
||||
def test_one_shot_marker_is_not_inherited_by_child_processes(one_shot):
|
||||
from tools.environments.local import _make_run_env
|
||||
|
||||
assert "HERMES_SINGLE_QUERY_SESSION" not in _make_run_env({})
|
||||
Reference in new issue
Block a user