feat(0131): data_table_cpp_viz migration + botones acción + 7 nuevos tests e2e
Issue 0131 (agents v0.2) — frontend agents_dashboard: * Migra tabla Agents de ImGui::BeginTable a render_grid_stage0 (data_table_cpp_viz). 11 columnas: Status(Badge), ID, Name, Uptime, Msg/24h, Start/Stop/Restart/Clear Memory/Del Cache/Logs (Button renderers). * Lee campos reales uptime_seconds + messages_24h del backend (antes leía 'instances' como proxy para msg_24h; ahora lee el campo correcto). * Confirmation modal (ImGui::BeginPopupModal) para acciones destructivas clear_memory y delete_cache. * Link fn_module_data_table en CMakeLists (HAS_DATA_TABLE activado). * Actualiza app.md: data_table_cpp_viz en uses_functions, version 0.2.0. * Añade 7 tests pytest nuevos (total 24): test_uptime_field_present, test_msg_24h_field_present, test_clear_memory_requires_apikey, test_delete_cache_requires_apikey, test_control_roundtrip, test_unified_stop_does_not_kill_launcher, test_clear_memory_response_shape. SEGURIDAD: solo test-bot se para/arranca en e2e, nunca agentes reales. Build verificado: Linux + Windows cross-compile (cmake --build cpp/build/windows). Co-Authored-By: fn-orquestador (issue 0131) <noreply@fn-registry>
This commit is contained in:
@@ -306,3 +306,196 @@ def test_get_single_agent_returns_logs():
|
||||
assert body.get("id") == "assistant-bot"
|
||||
assert "logs" in body
|
||||
assert isinstance(body["logs"], list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v0.2 — per-agent control + new fields (issue 0131)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_uptime_field_present():
|
||||
"""GET /agents response must include uptime_seconds field (integer >= 0)."""
|
||||
apikey = _apikey()
|
||||
r = _curl([
|
||||
"-fsS",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{_url()}/agents",
|
||||
])
|
||||
assert r.returncode == 0, r.stderr
|
||||
agents = json.loads(r.stdout)
|
||||
assert len(agents) > 0, "no agents returned"
|
||||
for a in agents:
|
||||
assert "uptime_seconds" in a, f"agent {a.get('id')} missing uptime_seconds"
|
||||
assert isinstance(a["uptime_seconds"], int) and a["uptime_seconds"] >= 0, \
|
||||
f"agent {a.get('id')} uptime_seconds={a['uptime_seconds']!r} not a non-negative int"
|
||||
|
||||
|
||||
def test_msg_24h_field_present():
|
||||
"""GET /agents response must include messages_24h field (integer >= 0)."""
|
||||
apikey = _apikey()
|
||||
r = _curl([
|
||||
"-fsS",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{_url()}/agents",
|
||||
])
|
||||
assert r.returncode == 0, r.stderr
|
||||
agents = json.loads(r.stdout)
|
||||
assert len(agents) > 0, "no agents returned"
|
||||
for a in agents:
|
||||
assert "messages_24h" in a, f"agent {a.get('id')} missing messages_24h"
|
||||
assert isinstance(a["messages_24h"], int) and a["messages_24h"] >= 0, \
|
||||
f"agent {a.get('id')} messages_24h={a['messages_24h']!r} not a non-negative int"
|
||||
|
||||
|
||||
def test_clear_memory_requires_apikey():
|
||||
"""POST /agents/{id}/clear_memory must return 401 without Bearer token."""
|
||||
r = _curl([
|
||||
"-s", "-o", "/dev/null", "-w", "%{http_code}",
|
||||
"-X", "POST",
|
||||
f"{_url()}/agents/test-bot/clear_memory",
|
||||
])
|
||||
assert r.stdout == "401", f"expected 401 got {r.stdout!r}"
|
||||
|
||||
|
||||
def test_delete_cache_requires_apikey():
|
||||
"""POST /agents/{id}/delete_cache must return 401 without Bearer token."""
|
||||
r = _curl([
|
||||
"-s", "-o", "/dev/null", "-w", "%{http_code}",
|
||||
"-X", "POST",
|
||||
f"{_url()}/agents/test-bot/delete_cache",
|
||||
])
|
||||
assert r.stdout == "401", f"expected 401 got {r.stdout!r}"
|
||||
|
||||
|
||||
def test_control_roundtrip():
|
||||
"""Stop test-bot → poll running=false → start → restart.
|
||||
|
||||
SECURITY: Only test-bot is used here. Never call stop on real agents
|
||||
during E2E. test-bot is a stateless robot with no ongoing conversations.
|
||||
"""
|
||||
apikey = _apikey()
|
||||
BASE = _url()
|
||||
|
||||
def get_running(agent_id: str) -> bool:
|
||||
r = _curl([
|
||||
"-fsS",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
])
|
||||
return bool(json.loads(r.stdout).get("running", False))
|
||||
|
||||
# --- Stop ---
|
||||
r = _curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/test-bot/stop",
|
||||
])
|
||||
assert r.returncode == 0, f"stop failed: {r.stderr}"
|
||||
body = json.loads(r.stdout)
|
||||
# Accept "stopped" or "not_running" as valid outcomes
|
||||
assert body.get("status") in ("stopped", "not_running", "ok"), f"unexpected stop status: {body}"
|
||||
|
||||
# Poll until running=false (max 3s)
|
||||
import time
|
||||
for _ in range(15):
|
||||
if not get_running("test-bot"):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
assert not get_running("test-bot"), "test-bot still running after stop"
|
||||
|
||||
# --- Start ---
|
||||
r = _curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/test-bot/start",
|
||||
])
|
||||
assert r.returncode == 0, f"start failed: {r.stderr}"
|
||||
body = json.loads(r.stdout)
|
||||
assert body.get("status") in ("started", "ok"), f"unexpected start status: {body}"
|
||||
|
||||
# Poll until running=true (max 5s)
|
||||
for _ in range(25):
|
||||
if get_running("test-bot"):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
assert get_running("test-bot"), "test-bot not running after start"
|
||||
|
||||
# --- Restart ---
|
||||
r = _curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/test-bot/restart",
|
||||
])
|
||||
assert r.returncode == 0, f"restart failed: {r.stderr}"
|
||||
body = json.loads(r.stdout)
|
||||
assert body.get("status") in ("restarted", "ok"), f"unexpected restart status: {body}"
|
||||
|
||||
# After restart test-bot must be running again (max 5s)
|
||||
for _ in range(25):
|
||||
if get_running("test-bot"):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
assert get_running("test-bot"), "test-bot not running after restart"
|
||||
|
||||
|
||||
def test_unified_stop_does_not_kill_launcher():
|
||||
"""Stopping test-bot must leave assistant-bot (and other real agents) running.
|
||||
|
||||
In unified mode, all agents are goroutines. Per-agent stop must cancel
|
||||
only the target goroutine — not the launcher process.
|
||||
SECURITY: Only test-bot is stopped. assistant-bot is only read, never mutated.
|
||||
"""
|
||||
apikey = _apikey()
|
||||
BASE = _url()
|
||||
|
||||
def get_running(agent_id: str) -> bool:
|
||||
r = _curl([
|
||||
"-fsS",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/{agent_id}",
|
||||
])
|
||||
if r.returncode != 0:
|
||||
return False
|
||||
return bool(json.loads(r.stdout).get("running", False))
|
||||
|
||||
# Stop test-bot only
|
||||
r = _curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/test-bot/stop",
|
||||
])
|
||||
assert r.returncode == 0, f"stop test-bot failed: {r.stderr}"
|
||||
|
||||
# assistant-bot must still be running (launcher is alive)
|
||||
assert get_running("assistant-bot"), \
|
||||
"assistant-bot not running after stopping test-bot — launcher may have crashed"
|
||||
|
||||
# Restore test-bot
|
||||
_curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{BASE}/agents/test-bot/start",
|
||||
])
|
||||
|
||||
|
||||
def test_clear_memory_response_shape():
|
||||
"""POST /agents/{id}/clear_memory (authorized) returns JSON with expected fields.
|
||||
|
||||
Uses test-bot which has minimal/no memory, so deleting is always safe.
|
||||
Verifies: status, messages_deleted, facts_deleted keys present.
|
||||
"""
|
||||
apikey = _apikey()
|
||||
r = _curl([
|
||||
"-sS", "-X", "POST",
|
||||
"-H", f"Authorization: Bearer {apikey}",
|
||||
f"{_url()}/agents/test-bot/clear_memory",
|
||||
])
|
||||
assert r.returncode == 0, f"clear_memory failed: {r.stderr}"
|
||||
body = json.loads(r.stdout)
|
||||
assert body.get("status") == "cleared", f"expected status=cleared: {body}"
|
||||
assert "messages_deleted" in body, f"missing messages_deleted: {body}"
|
||||
assert "facts_deleted" in body, f"missing facts_deleted: {body}"
|
||||
assert isinstance(body["messages_deleted"], int), \
|
||||
f"messages_deleted not int: {body['messages_deleted']!r}"
|
||||
assert isinstance(body["facts_deleted"], int), \
|
||||
f"facts_deleted not int: {body['facts_deleted']!r}"
|
||||
|
||||
Reference in New Issue
Block a user