mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user