feat(api_hook_client): add wait_for_project_switch for deterministic test waits
Adds a polling helper that blocks until the project switch completes, errors out, or times out. Replaces the fragile 10x1s blind poll in test_full_live_workflow with a condition-based wait on the /api/project_switch_status endpoint. Features: - Polls /api/project_switch_status every 200ms (configurable) - Returns immediately on error (with the error in the result) - Path matching: exact match OR basename match (handles absolute vs relative) - Times out with a clear 'timeout' flag instead of a generic assertion - Optional expected_path: if None, returns on any in_progress=False - src/api_hook_client.py: new wait_for_project_switch method (37 lines) - tests/test_api_hook_client_wait_for_project_switch.py: 6 unit tests with mocked _make_request covering all paths
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""Tests for ApiHookClient.wait_for_project_switch.
|
||||
|
||||
These tests use mocked _make_request so they don't require a live_gui
|
||||
session. They verify the polling logic: success, error, timeout, and
|
||||
path-matching behavior.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
|
||||
def test_wait_for_project_switch_returns_immediately_when_idle() -> None:
|
||||
"""If the controller is already idle and path matches, return immediately."""
|
||||
client = ApiHookClient()
|
||||
with patch.object(client, "_make_request") as mock_make:
|
||||
mock_make.return_value = {"in_progress": False, "path": "C:/projects/foo.toml", "error": None}
|
||||
start = time.time()
|
||||
result = client.wait_for_project_switch(expected_path="C:/projects/foo.toml", timeout=5.0)
|
||||
elapsed = time.time() - start
|
||||
assert result["in_progress"] is False
|
||||
assert result["path"] == "C:/projects/foo.toml"
|
||||
assert result.get("error") is None
|
||||
assert "timeout" not in result
|
||||
assert elapsed < 1.0, f"should return quickly, took {elapsed}s"
|
||||
|
||||
|
||||
def test_wait_for_project_switch_surfaces_error() -> None:
|
||||
"""If the controller reports an error, return immediately with the error."""
|
||||
client = ApiHookClient()
|
||||
with patch.object(client, "_make_request") as mock_make:
|
||||
mock_make.return_value = {"in_progress": False, "path": None, "error": "load failed: file not found"}
|
||||
result = client.wait_for_project_switch(expected_path="C:/missing.toml", timeout=5.0)
|
||||
assert result["error"] == "load failed: file not found"
|
||||
assert "timeout" not in result
|
||||
|
||||
|
||||
def test_wait_for_project_switch_matches_by_basename() -> None:
|
||||
"""Path matching is loose: matches by basename to handle absolute vs relative."""
|
||||
client = ApiHookClient()
|
||||
with patch.object(client, "_make_request") as mock_make:
|
||||
mock_make.return_value = {"in_progress": False, "path": "C:/abs/path/temp_project.toml", "error": None}
|
||||
result = client.wait_for_project_switch(
|
||||
expected_path="tests/artifacts/temp_project.toml",
|
||||
timeout=5.0,
|
||||
)
|
||||
assert result["path"].endswith("temp_project.toml")
|
||||
assert "timeout" not in result
|
||||
|
||||
|
||||
def test_wait_for_project_switch_times_out_when_in_progress() -> None:
|
||||
"""If the controller stays in_progress past the timeout, return with timeout flag."""
|
||||
client = ApiHookClient()
|
||||
with patch.object(client, "_make_request") as mock_make:
|
||||
mock_make.return_value = {"in_progress": True, "path": None, "error": None}
|
||||
result = client.wait_for_project_switch(expected_path="C:/foo.toml", timeout=0.5, poll_interval=0.1)
|
||||
assert result.get("timeout") is True
|
||||
assert result["in_progress"] is True
|
||||
|
||||
|
||||
def test_wait_for_project_switch_no_expected_path() -> None:
|
||||
"""If expected_path is None, return when in_progress becomes False (any path)."""
|
||||
client = ApiHookClient()
|
||||
with patch.object(client, "_make_request") as mock_make:
|
||||
mock_make.return_value = {"in_progress": False, "path": "C:/anything.toml", "error": None}
|
||||
result = client.wait_for_project_switch(expected_path=None, timeout=5.0)
|
||||
assert result["in_progress"] is False
|
||||
assert "timeout" not in result
|
||||
|
||||
|
||||
def test_wait_for_project_switch_polls_then_completes() -> None:
|
||||
"""Simulate a switch that takes 2 polls to complete."""
|
||||
client = ApiHookClient()
|
||||
call_count = [0]
|
||||
|
||||
def fake_request(*args, **kwargs):
|
||||
call_count[0] += 1
|
||||
if call_count[0] < 3:
|
||||
return {"in_progress": True, "path": None, "error": None}
|
||||
return {"in_progress": False, "path": "C:/foo.toml", "error": None}
|
||||
|
||||
with patch.object(client, "_make_request", side_effect=fake_request):
|
||||
result = client.wait_for_project_switch(expected_path="C:/foo.toml", timeout=5.0, poll_interval=0.05)
|
||||
assert result["in_progress"] is False
|
||||
assert result["path"] == "C:/foo.toml"
|
||||
assert "timeout" not in result
|
||||
assert call_count[0] >= 3
|
||||
Reference in New Issue
Block a user