"""Tests for the agent-system webui's actions mirror + mutate proxy. Covers the fix for the Action Queue panel always showing 0 pending: when CONTROL_PLANE_URL is set (this webui is a read-only mirror of VPS, e.g. on PIHA), current_actions() must read the materializer's mirrored world/actions.json instead of the local (always-empty) ACTIONS_DIR, and mutate_action() must proxy approve/reject to the VPS control-plane instead of writing to the local ACTIONS_DIR, which the real executor never reads. """ from __future__ import annotations import json import sys import urllib.error from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).parent.parent)) import web as web_module class _FakeResponse: def __init__(self, status): self.status = status def __enter__(self): return self def __exit__(self, *exc): return False def _setup_dirs(tmp_path, monkeypatch): world = tmp_path / "world" actions = tmp_path / "actions" world.mkdir() actions.mkdir() monkeypatch.setattr(web_module, "WORLD_DIR", world) monkeypatch.setattr(web_module, "ACTIONS_DIR", actions) return world, actions def test_current_actions_reads_mirror_when_control_plane_url_set(tmp_path, monkeypatch): world, actions = _setup_dirs(tmp_path, monkeypatch) monkeypatch.setattr(web_module, "CONTROL_PLANE_URL", "http://100.95.58.48:18180") mirrored = {"pending": [{"id": "a1", "status": "pending", "type": "redeploy"}], "approved": [], "running": [], "completed": [], "failed": [], "rejected": []} (world / "actions.json").write_text(json.dumps(mirrored)) # A stray local pending file must NOT leak into the mirrored result — VPS # is the sole source of truth once mirroring is on. local_pending = actions / "pending" local_pending.mkdir() (local_pending / "local-only.json").write_text(json.dumps({"type": "alert_only"})) result = web_module.current_actions() assert result == mirrored assert len(result["pending"]) == 1 assert result["pending"][0]["id"] == "a1" def test_current_actions_falls_back_to_local_scan_without_control_plane_url(tmp_path, monkeypatch): world, actions = _setup_dirs(tmp_path, monkeypatch) monkeypatch.setattr(web_module, "CONTROL_PLANE_URL", "") pending_dir = actions / "pending" pending_dir.mkdir() (pending_dir / "act-1.json").write_text(json.dumps({"type": "redeploy"})) result = web_module.current_actions() assert len(result["pending"]) == 1 assert result["pending"][0]["id"] == "act-1" assert result["pending"][0]["status"] == "pending" def test_mutate_action_proxies_to_control_plane_when_set(tmp_path, monkeypatch): _setup_dirs(tmp_path, monkeypatch) monkeypatch.setattr(web_module, "CONTROL_PLANE_URL", "http://100.95.58.48:18180") captured = {} def fake_urlopen(req, timeout=10): captured["url"] = req.full_url captured["body"] = json.loads(req.data.decode("utf-8")) return _FakeResponse(200) monkeypatch.setattr(web_module.urllib.request, "urlopen", fake_urlopen) success, msg = web_module.mutate_action("a1", "approved") assert success is True assert captured["url"] == "http://100.95.58.48:18180/action/mutate" assert captured["body"] == {"id": "a1", "status": "approved"} def test_mutate_action_proxy_failure_reports_error(tmp_path, monkeypatch): _setup_dirs(tmp_path, monkeypatch) monkeypatch.setattr(web_module, "CONTROL_PLANE_URL", "http://100.95.58.48:18180") def fake_urlopen(req, timeout=10): raise urllib.error.URLError("connection refused") monkeypatch.setattr(web_module.urllib.request, "urlopen", fake_urlopen) success, msg = web_module.mutate_action("a1", "approved") assert success is False assert "100.95.58.48:18180" in msg def test_mutate_action_local_when_no_control_plane_url(tmp_path, monkeypatch): world, actions = _setup_dirs(tmp_path, monkeypatch) monkeypatch.setattr(web_module, "CONTROL_PLANE_URL", "") pending_dir = actions / "pending" pending_dir.mkdir() (pending_dir / "act-1.json").write_text(json.dumps({"status": "pending"})) success, msg = web_module.mutate_action("act-1", "approved") assert success is True assert not (pending_dir / "act-1.json").exists() approved_data = json.loads((actions / "approved" / "act-1.json").read_text()) assert approved_data["status"] == "approved"