This commit is contained in:
@@ -495,6 +495,128 @@ def test_observation_loop_blocks_repeated_broad_reobservation(tmp_path: Path, mo
|
||||
assert blocked["window_summary"] == "Save as [#32770]"
|
||||
|
||||
|
||||
def test_observation_loop_counts_sleep_as_non_progress(tmp_path: Path, monkeypatch) -> None:
|
||||
agent = _build_agent(tmp_path, monkeypatch)
|
||||
agent.step_history = [
|
||||
{
|
||||
"step": 40,
|
||||
"tool_names": ["click"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": False,
|
||||
"contains_action": True,
|
||||
},
|
||||
{
|
||||
"step": 41,
|
||||
"tool_names": ["see_screen"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": True,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 42,
|
||||
"tool_names": ["sleep"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": False,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 43,
|
||||
"tool_names": ["get_active_window"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": False,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 44,
|
||||
"tool_names": ["sleep"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": False,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 45,
|
||||
"tool_names": ["see_screen"],
|
||||
"window_signature": "123|#32770|Save as",
|
||||
"window_summary": "Save as [#32770]",
|
||||
"had_visual": True,
|
||||
"contains_action": False,
|
||||
},
|
||||
]
|
||||
|
||||
blocked = agent._dispatch_tool("see_screen", {})
|
||||
|
||||
assert blocked["ok"] is False
|
||||
assert blocked["blocked"] is True
|
||||
assert blocked["blocked_reason"] == "observation_loop"
|
||||
assert blocked["repeated_steps"] == 3
|
||||
|
||||
|
||||
def test_observation_loop_treats_focus_window_as_action_progress(tmp_path: Path, monkeypatch) -> None:
|
||||
agent = _build_agent(tmp_path, monkeypatch)
|
||||
|
||||
agent._record_step_history(["focus_window"], {"hwnd": 123, "class_name": "#32770", "title": "Save as"})
|
||||
|
||||
entry = agent.step_history[-1]
|
||||
assert entry["contains_action"] is True
|
||||
|
||||
|
||||
def test_observation_loop_requires_non_empty_window_signature(tmp_path: Path, monkeypatch) -> None:
|
||||
agent = _build_agent(tmp_path, monkeypatch)
|
||||
agent.step_history = [
|
||||
{
|
||||
"step": 50,
|
||||
"tool_names": ["see_screen"],
|
||||
"window_signature": "",
|
||||
"window_summary": "",
|
||||
"had_visual": True,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 51,
|
||||
"tool_names": ["get_active_window"],
|
||||
"window_signature": "",
|
||||
"window_summary": "",
|
||||
"had_visual": False,
|
||||
"contains_action": False,
|
||||
},
|
||||
{
|
||||
"step": 52,
|
||||
"tool_names": ["see_screen"],
|
||||
"window_signature": "",
|
||||
"window_summary": "",
|
||||
"had_visual": True,
|
||||
"contains_action": False,
|
||||
},
|
||||
]
|
||||
|
||||
assert agent._stable_observation_loop() is None
|
||||
|
||||
|
||||
def test_record_step_history_reuses_last_observed_window_for_visual_only_steps(tmp_path: Path, monkeypatch) -> None:
|
||||
agent = _build_agent(tmp_path, monkeypatch)
|
||||
agent.last_observed_window = {
|
||||
"available": True,
|
||||
"hwnd": 321,
|
||||
"class_name": "ApplicationFrameWindow",
|
||||
"title": "Settings",
|
||||
}
|
||||
|
||||
agent._record_step_history(["see_screen"], None, "sig-a")
|
||||
agent._record_step_history(["get_active_window"], None)
|
||||
agent._record_step_history(["detect_dialog"], None)
|
||||
|
||||
stable = agent._stable_observation_loop()
|
||||
|
||||
assert stable is not None
|
||||
assert stable["window_summary"] == "Settings [ApplicationFrameWindow]"
|
||||
assert stable["repeated_steps"] == 3
|
||||
|
||||
|
||||
def test_repeated_ambiguous_action_requires_verification_and_then_blocks(tmp_path: Path, monkeypatch) -> None:
|
||||
agent = _build_agent(tmp_path, monkeypatch)
|
||||
type_args = {"text": "repeat me"}
|
||||
|
||||
@@ -9,6 +9,14 @@ from src.config import AppConfig
|
||||
from src.models import AgentResult, RunArtifacts, UsageSummary
|
||||
|
||||
|
||||
class _OverlayRecorder:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
def show_completion(self, **kwargs: Any) -> None:
|
||||
self.calls.append(kwargs)
|
||||
|
||||
|
||||
def test_cli_emits_structured_return_and_data(monkeypatch: Any, capsys, tmp_path: Path) -> None:
|
||||
config = AppConfig(
|
||||
openai_api_key="test_key",
|
||||
@@ -56,16 +64,28 @@ def test_cli_emits_structured_return_and_data(monkeypatch: Any, capsys, tmp_path
|
||||
)
|
||||
return result, artifacts
|
||||
|
||||
overlay = _OverlayRecorder()
|
||||
|
||||
monkeypatch.setattr(cli_module, "load_app_config", fake_load_app_config)
|
||||
monkeypatch.setattr(cli_module, "assess_task_safety", fake_assess_task_safety)
|
||||
monkeypatch.setattr(cli_module, "run_job", fake_run_job)
|
||||
monkeypatch.setattr(cli_module, "create_openai_client", lambda *_args, **_kwargs: object())
|
||||
monkeypatch.setattr(cli_module, "get_desktop_overlay_manager", lambda: overlay)
|
||||
|
||||
code = cli_module.main(["Open amazon.de"])
|
||||
assert code == 0
|
||||
|
||||
out = capsys.readouterr().out
|
||||
payload = json.loads(out)
|
||||
assert overlay.calls == [
|
||||
{
|
||||
"job_id": "20260527_000001",
|
||||
"objective": "Open amazon.de",
|
||||
"return_message": "Task completed successfully",
|
||||
"steps": 3,
|
||||
"elapsed_seconds": 2.5,
|
||||
}
|
||||
]
|
||||
assert payload["response"]["return"] == "Task completed successfully"
|
||||
assert payload["response"]["data"] == "file1.txt\nfile2.txt"
|
||||
assert payload["return"] == "Task completed successfully"
|
||||
|
||||
@@ -4,6 +4,7 @@ import types
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
import src.desktop_overlay as desktop_overlay_module
|
||||
from src.desktop_overlay import CompletionOverlayPayload, DesktopOverlayManager
|
||||
|
||||
|
||||
@@ -127,6 +128,25 @@ class _FakeTkModule(types.SimpleNamespace):
|
||||
return _FakeButton(root._root, command=command)
|
||||
|
||||
|
||||
def test_show_completion_plays_sound_even_without_overlay_thread(monkeypatch: Any) -> None:
|
||||
manager = DesktopOverlayManager()
|
||||
calls: list[str] = []
|
||||
|
||||
monkeypatch.setattr(desktop_overlay_module.os, "name", "nt", raising=False)
|
||||
monkeypatch.setattr(manager, "_play_completion_sound", lambda: calls.append("sound"))
|
||||
monkeypatch.setattr(manager, "_ensure_thread", lambda: False)
|
||||
|
||||
manager.show_completion(
|
||||
job_id="job-123",
|
||||
objective="Write a report",
|
||||
return_message="Finished",
|
||||
steps=5,
|
||||
elapsed_seconds=12.4,
|
||||
)
|
||||
|
||||
assert calls == ["sound"]
|
||||
|
||||
|
||||
def test_completion_overlay_auto_dismisses(monkeypatch: Any) -> None:
|
||||
root = _FakeTk()
|
||||
fake_tk = _FakeTkModule(root)
|
||||
@@ -147,3 +167,15 @@ def test_completion_overlay_auto_dismisses(monkeypatch: Any) -> None:
|
||||
|
||||
assert any(delay == 10 for delay in root.scheduled_delays)
|
||||
assert root.cards[0]._exists is False
|
||||
|
||||
|
||||
def test_play_completion_sound_uses_winsound_message_beep(monkeypatch: Any) -> None:
|
||||
calls: list[int] = []
|
||||
fake_winsound = types.SimpleNamespace(MB_ICONASTERISK=64, MessageBeep=lambda value: calls.append(value))
|
||||
|
||||
monkeypatch.setattr(desktop_overlay_module.os, "name", "nt", raising=False)
|
||||
monkeypatch.setattr(desktop_overlay_module, "winsound", fake_winsound)
|
||||
|
||||
DesktopOverlayManager()._play_completion_sound()
|
||||
|
||||
assert calls == [64]
|
||||
|
||||
Reference in New Issue
Block a user