feat. Finishing sound & agent loop fixes
CI / test (push) Failing after 33s

This commit is contained in:
2026-06-04 15:16:31 +02:00
parent 97641b354e
commit 643320a9de
6 changed files with 271 additions and 42 deletions
+122
View File
@@ -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"}
+20
View File
@@ -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"
+32
View File
@@ -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]