mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(panel): V6 review gaps — honest errors, safe secretary start, real tests
CEO A2A mutations now invalidate the Mine-list query key so the list refreshes without the socket; the tg Metrics tab renders explicit error notes instead of confident zero stats when a section's fetch fails; the tg Secretary chat checks a new registry-backed /secretary/live/active route before auto-starting, showing a Take Over button instead of silently killing a session live on another device; the AI-routing card surfaces roster fetch errors instead of an empty grid; the shared acceptance-criteria editor caps at the backend's 7-item limit; the board-tab comment no longer calls the task sheet read-only. Tests: task-sheet approve/reject interactions (not just visibility), a non-demo metrics error-state test, secretary take-over branches, and dashboard-router auth-gate coverage (the e2e harness now mounts the dashboard router so the gate is actually exercised).
This commit is contained in:
@@ -321,6 +321,7 @@ def _make_admin_clone(root: Path, origin: Path) -> Path:
|
||||
|
||||
def _build_app(gh: _FakeGitHub) -> FastAPI:
|
||||
from roboco.api.middleware import setup_middleware
|
||||
from roboco.api.routes.dashboard import router as dashboard_router
|
||||
from roboco.api.routes.health import router as health_router
|
||||
from roboco.api.routes.notifications import router as notifications_router
|
||||
from roboco.api.routes.orchestrator import router as orchestrator_router
|
||||
@@ -353,6 +354,7 @@ def _build_app(gh: _FakeGitHub) -> FastAPI:
|
||||
# require_panel_token dep paths on these routers.
|
||||
app.include_router(orchestrator_router, prefix="/api/orchestrator")
|
||||
app.include_router(settings_router, prefix="/api/settings")
|
||||
app.include_router(dashboard_router, prefix="/api/dashboard")
|
||||
app.include_router(_fake_github_router(gh))
|
||||
return app
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ the live uvicorn + middleware + dependency stack — no stubbed deps:
|
||||
(c) NO credential → ``GET /api/settings`` 401 (``require_panel_token``).
|
||||
(d) a REAL CEO session cookie (minted via the auth backend's JWT strategy
|
||||
over a seeded CEO user row) → ``GET /api/settings`` 200.
|
||||
(e) NO credential → ``GET /api/dashboard/ceo`` 401 (the dashboard router's
|
||||
OWN ``require_panel_token`` gate — a distinct router from (c)/(d), added
|
||||
to close a prior unauthenticated metrics/scorecard exposure).
|
||||
(f) the same REAL CEO session cookie → ``GET /api/dashboard/ceo`` 200.
|
||||
|
||||
The running uvicorn app reads the same ``roboco.config.settings`` singleton
|
||||
per-request, so monkeypatching it live takes effect without a restart.
|
||||
@@ -138,6 +142,33 @@ def test_settings_real_ceo_cookie_passes(
|
||||
)
|
||||
|
||||
|
||||
def test_dashboard_no_credential_rejected(
|
||||
e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_arm_cloud_auth(monkeypatch)
|
||||
resp = httpx.get(f"{e2e_stack.base_url}/api/dashboard/ceo", timeout=10)
|
||||
assert resp.status_code == HTTPStatus.UNAUTHORIZED, (
|
||||
f"dashboard no-credential: expected 401, got "
|
||||
f"{resp.status_code} {resp.text[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def test_dashboard_real_ceo_cookie_passes(
|
||||
e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_arm_cloud_auth(monkeypatch)
|
||||
user: UserTable = e2e_stack.run_db(_seed_ceo_user)
|
||||
cookie = _mint_ceo_session_cookie(user)
|
||||
resp = httpx.get(
|
||||
f"{e2e_stack.base_url}/api/dashboard/ceo",
|
||||
cookies={"roboco_session": cookie},
|
||||
timeout=10,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK, (
|
||||
f"dashboard real cookie: expected 200, got {resp.status_code} {resp.text[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def _ceo_agent_id() -> str:
|
||||
from roboco.agents_config import CEO_AGENT_ID
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ async def test_stream_accepts_valid_panel_token(
|
||||
("GET", "/api/secretary/live/unknown/status", None),
|
||||
("POST", "/api/secretary/live/unknown/messages", {"text": "hi"}),
|
||||
("POST", "/api/secretary/live/sess/stop", None),
|
||||
("GET", "/api/secretary/live/active", None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
@@ -176,6 +177,22 @@ async def test_status_send_stop_reject_missing_token_when_required(
|
||||
assert r.status_code == _HTTP_401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_active_accepts_valid_panel_token_and_reflects_the_registry(
|
||||
auth_client: AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_strict(monkeypatch)
|
||||
headers = {"X-Agent-Token": issue_panel_token()}
|
||||
|
||||
r = await auth_client.get("/api/secretary/live/active", headers=headers)
|
||||
assert r.status_code == HTTPStatus.OK
|
||||
assert r.json() == {"active": False}
|
||||
|
||||
prompter_live.get_live_registry().open("some-device", "secretary-1")
|
||||
r = await auth_client.get("/api/secretary/live/active", headers=headers)
|
||||
assert r.json() == {"active": True}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_events_ungated_in_strict_mode(
|
||||
auth_client: AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
@@ -221,5 +221,23 @@ async def test_deliver_to_unknown_or_failing_returns_false() -> None:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
def test_has_live_agent_tracks_any_session_for_that_agent() -> None:
|
||||
"""Backs the "is the Secretary live under ANY session id" check — distinct
|
||||
from is_alive, which needs the caller's own session id."""
|
||||
reg = PrompterLiveRegistry()
|
||||
assert reg.has_live_agent("secretary-1") is False # nothing open yet
|
||||
|
||||
reg.open("device-a", "secretary-1")
|
||||
assert reg.has_live_agent("secretary-1") is True
|
||||
assert reg.has_live_agent("intake-1") is False # different agent, untouched
|
||||
|
||||
reg.close("device-a")
|
||||
assert reg.has_live_agent("secretary-1") is False # closed -> gone
|
||||
|
||||
# A second session id for the SAME agent still counts as live.
|
||||
reg.open("device-b", "secretary-1")
|
||||
assert reg.has_live_agent("secretary-1") is True
|
||||
|
||||
|
||||
def test_registry_singleton() -> None:
|
||||
assert get_live_registry() is get_live_registry()
|
||||
|
||||
Reference in New Issue
Block a user