fix: wave 1 quick wins — agent names, scroll bounce-back, chart empty states, model-pin preservation, UUID spawn normalization (#546)

* fix(panel): notifications show agent names, metrics charts get empty states

* fix(panel): stop expand/collapse scroll bounce-back; add floating scroll-jump buttons

* fix(llm): provider mode switches preserve per-agent model pins

* fix(api): normalize agent UUID to slug at the orchestrator route boundary

* fix(panel,docs): align routing-card copy and map docs with preserved-pin mode switches

* fix(panel): drop dead unfiltered scroll hook, re-observe on Suspense swap, name system sender

* docs(map): reflect preserved-pin mode switches, UUID-slug normalization, panel wave-1 deltas

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-18 00:42:13 +02:00
committed by GitHub
co-authored by Renn F
parent 4c8a9fc008
commit 9b4ce6b9c8
24 changed files with 415 additions and 160 deletions
+49 -3
View File
@@ -223,6 +223,28 @@ async def test_apply_mode_anthropic_clears_all(llm_setup: dict) -> None:
assert await svc.list_assignments() == []
@pytest.mark.asyncio
async def test_apply_mode_anthropic_preserves_agent_pin(llm_setup: dict) -> None:
"""A mode switch must not wipe per-agent pins — only role/global rows."""
svc = llm_setup["svc"]
ollama_model = _first_model_for_type(ModelProvider.OLLAMA_CLOUD)
anthropic_model = _first_model_for_type(ModelProvider.ANTHROPIC)
await svc.upsert_assignment(
scope=AssignmentScope.AGENT_SLUG,
scope_value="be-dev-1",
model_name=ollama_model,
)
await svc.upsert_assignment(
scope=AssignmentScope.GLOBAL, scope_value=None, model_name=anthropic_model
)
await svc.apply_mode(mode="anthropic")
assignments = await svc.list_assignments()
assert len(assignments) == 1 # GLOBAL row cleared, AGENT_SLUG pin survives.
assert assignments[0].scope == AssignmentScope.AGENT_SLUG
assert assignments[0].scope_value == "be-dev-1"
assert assignments[0].model_name == ollama_model
@pytest.mark.asyncio
async def test_apply_mode_ollama_sets_global(llm_setup: dict) -> None:
svc = llm_setup["svc"]
@@ -233,6 +255,26 @@ async def test_apply_mode_ollama_sets_global(llm_setup: dict) -> None:
assert assignments[0].scope == AssignmentScope.GLOBAL
@pytest.mark.asyncio
async def test_apply_mode_ollama_preserves_agent_pin(llm_setup: dict) -> None:
"""The Ollama mode-switch button must not wipe per-agent model pins."""
svc = llm_setup["svc"]
anthropic_model = _first_model_for_type(ModelProvider.ANTHROPIC)
ollama_model = _first_model_for_type(ModelProvider.OLLAMA_CLOUD)
await svc.upsert_assignment(
scope=AssignmentScope.AGENT_SLUG,
scope_value="be-dev-1",
model_name=anthropic_model,
)
await svc.apply_mode(mode="ollama", default_model=ollama_model)
assignments = await svc.list_assignments()
assert len(assignments) == 2 # noqa: PLR2004 AGENT_SLUG pin kept + new GLOBAL row.
by_scope = {a.scope: a for a in assignments}
assert by_scope[AssignmentScope.AGENT_SLUG].scope_value == "be-dev-1"
assert by_scope[AssignmentScope.AGENT_SLUG].model_name == anthropic_model
assert by_scope[AssignmentScope.GLOBAL].model_name == ollama_model
@pytest.mark.asyncio
async def test_apply_mode_grok_sets_global(llm_setup: dict) -> None:
svc = llm_setup["svc"]
@@ -479,7 +521,8 @@ async def test_apply_mode_self_hosted_requires_default_model(
async def test_apply_mode_self_hosted_clears_prior_assignments(
llm_setup_with_local: dict,
) -> None:
"""apply_mode('self_hosted') clears ALL prior assignments."""
"""apply_mode('self_hosted') clears role/global assignments but preserves
AGENT_SLUG pins (mixed-provider routing is a supported state)."""
svc = llm_setup_with_local["svc"]
anthropic_model = _first_model_for_type(ModelProvider.ANTHROPIC)
await svc.upsert_assignment(
@@ -495,8 +538,11 @@ async def test_apply_mode_self_hosted_clears_prior_assignments(
assert len(await svc.list_assignments()) == 2 # noqa: PLR2004
await svc.apply_mode(mode="self_hosted", default_model="gemma2:9b")
assignments = await svc.list_assignments()
assert len(assignments) == 1 # Only the new GLOBAL row.
assert assignments[0].provider.type == ModelProvider.LOCAL
assert len(assignments) == 2 # noqa: PLR2004 AGENT_SLUG pin kept + new GLOBAL row.
by_scope = {a.scope: a for a in assignments}
assert by_scope[AssignmentScope.AGENT_SLUG].scope_value == "be-dev-1"
assert by_scope[AssignmentScope.AGENT_SLUG].model_name == anthropic_model
assert by_scope[AssignmentScope.GLOBAL].provider.type == ModelProvider.LOCAL
@pytest.mark.asyncio
@@ -20,12 +20,14 @@ from uuid import uuid4
import pytest
import pytest_asyncio
import roboco.api.routes.orchestrator as orch_route
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from httpx import ASGITransport, AsyncClient
from roboco.agents_config import AGENT_UUIDS
from roboco.api.deps import _ServiceHolder, set_orchestrator
from roboco.api.routes.orchestrator import (
_build_manual_spawn_prompt,
_resolve_manual_spawn_prompt,
_validated_agent_id,
)
from roboco.api.routes.orchestrator import (
router as orch_router,
@@ -275,3 +277,76 @@ async def test_spawn_offline_agent_not_flagged_already_running(
)
assert response.status_code == HTTPStatus.CREATED
assert response.json()["already_running"] is False
# ---------------------------------------------------------------------------
# _validated_agent_id — UUID -> slug normalization (root fix: a caller that
# addresses a runtime container/instance by an agent's DB UUID instead of its
# slug, e.g. the panel spawn button, must resolve to the same canonical slug
# the orchestrator's instance registry and container names use).
# ---------------------------------------------------------------------------
def test_validated_agent_id_resolves_known_uuid_to_slug() -> None:
uuid_str = AGENT_UUIDS["head-marketing"]
assert _validated_agent_id(uuid_str) == "head-marketing"
def test_validated_agent_id_passes_through_slug_unchanged() -> None:
assert _validated_agent_id("head-marketing") == "head-marketing"
def test_validated_agent_id_passes_through_unknown_uuid_unchanged() -> None:
# A uuid4 is never a seeded agent UUID (the seeds are deterministic,
# low-cardinality values) — genuinely absent from the UUID -> slug map.
unknown_uuid = str(uuid4())
assert unknown_uuid not in AGENT_UUIDS.values()
assert _validated_agent_id(unknown_uuid) == unknown_uuid
def test_validated_agent_id_still_rejects_traversal() -> None:
with pytest.raises(HTTPException) as exc_info:
_validated_agent_id("../etc/passwd")
assert exc_info.value.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
@pytest.mark.asyncio
async def test_spawn_by_uuid_reaches_orchestrator_by_slug(
orch_client: tuple[AsyncClient, MagicMock],
) -> None:
"""The panel (or any caller) posting the agent's DB UUID as the path
param must not produce a container/instance keyed by that UUID — the
orchestrator only ever sees the canonical slug."""
client, orch = orch_client
orch.get_instance = MagicMock(return_value=None)
instance = SimpleNamespace(
id=uuid4(),
agent_id="head-marketing",
state=AgentState.STARTING,
current_task_id=None,
error_count=0,
started_at=datetime.now(UTC),
)
orch.spawn_agent = AsyncMock(return_value=instance)
uuid_str = AGENT_UUIDS["head-marketing"]
response = await client.post(
f"/api/orchestrator/agents/{uuid_str}/spawn", headers=_HDR
)
assert response.status_code == HTTPStatus.CREATED
orch.spawn_agent.assert_awaited_once()
assert orch.spawn_agent.await_args.kwargs["agent_id"] == "head-marketing"
@pytest.mark.asyncio
async def test_stop_by_uuid_reaches_orchestrator_by_slug(
orch_client: tuple[AsyncClient, MagicMock],
) -> None:
client, orch = orch_client
orch.stop_agent = AsyncMock(return_value=None)
uuid_str = AGENT_UUIDS["be-dev-1"]
response = await client.post(
f"/api/orchestrator/agents/{uuid_str}/stop", headers=_HDR
)
assert response.status_code == HTTPStatus.NO_CONTENT
orch.stop_agent.assert_awaited_once()
assert orch.stop_agent.await_args.args[0] == "be-dev-1"