feat: Node Self-Healing, DAGI Audit, Agent Prompts, Infra Invariants

### Backend (city-service)
- Node Registry + Self-Healing API (migration 039)
- Improved get_all_nodes() with robust fallback for node_registry/node_cache
- Agent Prompts Runtime API for DAGI Router integration
- DAGI Router Audit endpoints (phantom/stale detection)
- Node Agents API (Guardian/Steward)
- Node metrics extended (CPU/GPU/RAM/Disk)

### Frontend (apps/web)
- Node Directory with improved error handling
- Node Cabinet with metrics cards
- DAGI Router Card component
- Node Metrics Card component
- useDAGIAudit hook

### Scripts
- check-invariants.py - deploy verification
- node-bootstrap.sh - node self-registration
- node-guardian-loop.py - continuous self-healing
- dagi_agent_audit.py - DAGI audit utility

### Migrations
- 034: Agent prompts seed
- 035: Agent DAGI audit
- 036: Node metrics extended
- 037: Node agents complete
- 038: Agent prompts full coverage
- 039: Node registry self-healing

### Tests
- test_infra_smoke.py
- test_agent_prompts_runtime.py
- test_dagi_router_api.py

### Documentation
- DEPLOY_CHECKLIST_2024_11_30.md
- Multiple TASK_PHASE docs
This commit is contained in:
Apple
2025-11-30 13:52:01 -08:00
parent 0c7836af5a
commit bca81dc719
36 changed files with 10630 additions and 55 deletions

View File

@@ -0,0 +1,326 @@
"""
Tests for Agent System Prompts Runtime API
Тести для Agent System Prompts MVP v2:
- Runtime prompts API
- build_system_prompt function
- Prompts status check API
"""
import pytest
import asyncio
from typing import Dict, Any
# Mock functions for testing without database
def build_system_prompt_from_parts(
prompts: Dict[str, str],
agent_info: Dict[str, Any] = None,
context: Dict[str, Any] = None
) -> str:
"""Build system prompt from parts (mock implementation for testing)"""
parts = []
# Core prompt (required)
if prompts.get("core"):
parts.append(prompts["core"])
elif agent_info:
agent_name = agent_info.get("display_name") or agent_info.get("name") or "Agent"
agent_kind = agent_info.get("kind") or "assistant"
parts.append(
f"You are {agent_name}, an AI {agent_kind} in DAARION.city ecosystem. "
f"Be helpful, accurate, and follow ethical guidelines."
)
else:
parts.append("You are an AI assistant. Be helpful and accurate.")
# Governance rules
if prompts.get("governance"):
parts.append("\n\n## Governance\n" + prompts["governance"])
# Safety guidelines
if prompts.get("safety"):
parts.append("\n\n## Safety Guidelines\n" + prompts["safety"])
# Tools instructions
if prompts.get("tools"):
parts.append("\n\n## Tools & Capabilities\n" + prompts["tools"])
# Context additions
if context:
context_lines = []
if context.get("node"):
node = context["node"]
context_lines.append(f"- **Node**: {node.get('name', 'Unknown')}")
if context.get("district"):
district = context["district"]
context_lines.append(f"- **District**: {district.get('name', 'Unknown')}")
if context.get("microdao"):
microdao = context["microdao"]
context_lines.append(f"- **MicroDAO**: {microdao.get('name', 'Unknown')}")
if context_lines:
parts.append("\n\n## Current Context\n" + "\n".join(context_lines))
return "\n".join(parts)
class TestBuildSystemPrompt:
"""Tests for build_system_prompt function"""
def test_core_only(self):
"""Test with only core prompt"""
prompts = {
"core": "You are DAARWIZZ, the global orchestrator.",
"safety": None,
"governance": None,
"tools": None
}
result = build_system_prompt_from_parts(prompts)
assert "DAARWIZZ" in result
assert "orchestrator" in result
assert "## Safety" not in result
assert "## Governance" not in result
def test_full_prompts(self):
"""Test with all prompt types"""
prompts = {
"core": "You are DAARWIZZ, the global orchestrator of DAARION.city.",
"safety": "Never execute irreversible actions without confirmation.",
"governance": "Coordinate with district leads for resource allocation.",
"tools": "Use agent_delegate to delegate tasks."
}
result = build_system_prompt_from_parts(prompts)
assert "DAARWIZZ" in result
assert "## Safety Guidelines" in result
assert "irreversible" in result
assert "## Governance" in result
assert "district leads" in result
assert "## Tools" in result
assert "agent_delegate" in result
def test_fallback_without_core(self):
"""Test fallback when no core prompt provided"""
prompts = {
"core": None,
"safety": "Be safe",
"governance": None,
"tools": None
}
agent_info = {
"name": "TestAgent",
"display_name": "Test Agent",
"kind": "coordinator"
}
result = build_system_prompt_from_parts(prompts, agent_info)
assert "Test Agent" in result
assert "coordinator" in result
assert "## Safety Guidelines" in result
assert "Be safe" in result
def test_with_context(self):
"""Test prompt with runtime context"""
prompts = {
"core": "You are a node agent.",
"safety": None,
"governance": None,
"tools": None
}
context = {
"node": {"name": "NODE1", "environment": "production"},
"district": {"name": "ENERGYUNION"},
"microdao": {"name": "DAARION"}
}
result = build_system_prompt_from_parts(prompts, context=context)
assert "node agent" in result
assert "## Current Context" in result
assert "NODE1" in result
assert "ENERGYUNION" in result
assert "DAARION" in result
def test_prompt_order(self):
"""Test that prompts are assembled in correct order"""
prompts = {
"core": "CORE_MARKER",
"safety": "SAFETY_MARKER",
"governance": "GOVERNANCE_MARKER",
"tools": "TOOLS_MARKER"
}
result = build_system_prompt_from_parts(prompts)
# Check order: core → governance → safety → tools
core_pos = result.find("CORE_MARKER")
gov_pos = result.find("GOVERNANCE_MARKER")
safety_pos = result.find("SAFETY_MARKER")
tools_pos = result.find("TOOLS_MARKER")
assert core_pos < gov_pos < safety_pos < tools_pos
class TestRuntimePromptsFormat:
"""Tests for runtime prompts response format"""
def test_response_structure(self):
"""Test expected response structure"""
expected_keys = {"agent_id", "has_prompts", "prompts"}
# Mock response
response = {
"agent_id": "agent-daarwizz",
"has_prompts": True,
"prompts": {
"core": "You are DAARWIZZ...",
"safety": "Safety rules...",
"governance": None,
"tools": None
}
}
assert set(response.keys()) == expected_keys
assert response["has_prompts"] is True
assert "core" in response["prompts"]
assert "safety" in response["prompts"]
assert "governance" in response["prompts"]
assert "tools" in response["prompts"]
def test_has_prompts_when_core_exists(self):
"""Test has_prompts is True when core exists"""
prompts = {"core": "Some core prompt", "safety": None, "governance": None, "tools": None}
has_prompts = prompts.get("core") is not None
assert has_prompts is True
def test_has_prompts_when_core_missing(self):
"""Test has_prompts is False when core is None"""
prompts = {"core": None, "safety": "Safety only", "governance": None, "tools": None}
has_prompts = prompts.get("core") is not None
assert has_prompts is False
class TestPromptsStatusBatch:
"""Tests for batch prompts status check"""
def test_status_response_format(self):
"""Test batch status response format"""
agent_ids = ["agent-daarwizz", "agent-devtools", "agent-unknown"]
# Mock response
response = {
"status": {
"agent-daarwizz": True,
"agent-devtools": True,
"agent-unknown": False
}
}
assert "status" in response
assert isinstance(response["status"], dict)
assert all(aid in response["status"] for aid in agent_ids)
assert all(isinstance(v, bool) for v in response["status"].values())
class TestNodeAgentPrompts:
"""Tests for Node Agent specific prompts"""
def test_node_guardian_prompt_content(self):
"""Test Node Guardian has appropriate content markers"""
guardian_core = """Ти — Node Guardian для НОДА1 (Hetzner GEX44 Production).
Твоя місія: забезпечувати стабільну роботу продакшн-інфраструктури DAARION.city."""
assert "Node Guardian" in guardian_core
assert "НОДА1" in guardian_core
assert "Production" in guardian_core or "production" in guardian_core.lower()
def test_node_guardian_safety_rules(self):
"""Test Node Guardian safety rules"""
guardian_safety = """Ніколи не виконуй деструктивні команди без підтвердження.
Не розкривай чутливу інформацію (паролі, API ключі).
При невизначеності — ескалюй до людини."""
assert "деструктивні" in guardian_safety
assert "підтвердження" in guardian_safety
assert "ескалюй" in guardian_safety
class TestAgentCoverage:
"""Tests for agent prompts coverage requirements"""
REQUIRED_AGENTS = [
# City / Core
"agent-daarwizz",
"agent-microdao-orchestrator",
"agent-devtools",
# District / MicroDAO
"agent-greenfood",
"agent-helion",
"agent-soul",
"agent-druid",
"agent-nutra",
"agent-eonarch",
"agent-clan",
"agent-yaromir",
"agent-monitor",
# Node Agents
"monitor-node1",
"monitor-node2",
"node-steward-node1",
"node-steward-node2"
]
def test_required_agents_list(self):
"""Test required agents are defined"""
assert len(self.REQUIRED_AGENTS) == 16
assert "agent-daarwizz" in self.REQUIRED_AGENTS
assert "monitor-node1" in self.REQUIRED_AGENTS
assert "monitor-node2" in self.REQUIRED_AGENTS
# Integration tests (require running services)
class TestIntegration:
"""Integration tests - skip if services not available"""
@pytest.mark.skip(reason="Requires running services")
async def test_fetch_runtime_prompts(self):
"""Test fetching runtime prompts from API"""
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(
"http://localhost:7001/internal/agents/agent-daarwizz/prompts/runtime"
)
assert response.status_code == 200
data = response.json()
assert data["agent_id"] == "agent-daarwizz"
assert "prompts" in data
@pytest.mark.skip(reason="Requires running services")
async def test_fetch_system_prompt(self):
"""Test fetching full system prompt from API"""
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(
"http://localhost:7001/internal/agents/agent-daarwizz/system-prompt"
)
assert response.status_code == 200
data = response.json()
assert data["agent_id"] == "agent-daarwizz"
assert "system_prompt" in data
assert len(data["system_prompt"]) > 100
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,280 @@
"""
DAGI Router API Tests
Тести для endpoints:
- GET /internal/node/{node_id}/dagi-router/agents
- GET /internal/node/{node_id}/metrics/current
- POST /internal/node/{node_id}/dagi-audit/run
- POST /internal/node/{node_id}/dagi-router/phantom/sync
- POST /internal/node/{node_id}/dagi-router/stale/mark
"""
import pytest
import httpx
from typing import Any, Dict
# Test configuration
CITY_SERVICE_URL = "http://localhost:7001"
NODE1_ID = "node-1-hetzner-gex44"
NODE2_ID = "node-2-macbook-m4max"
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def client():
"""HTTP client для тестування"""
return httpx.Client(base_url=CITY_SERVICE_URL, timeout=30.0)
@pytest.fixture
def node_ids():
"""Node IDs для тестування"""
return [NODE1_ID, NODE2_ID]
# ============================================================================
# DAGI Router Agents Tests
# ============================================================================
class TestDAGIRouterAgents:
"""Тести для GET /internal/node/{node_id}/dagi-router/agents"""
def test_get_agents_returns_valid_response(self, client):
"""Endpoint повертає валідну структуру"""
response = client.get(f"/city/internal/node/{NODE1_ID}/dagi-router/agents")
assert response.status_code == 200
data = response.json()
# Перевірка структури
assert "node_id" in data
assert "summary" in data
assert "agents" in data
# Перевірка summary
summary = data["summary"]
assert "active" in summary
assert "phantom" in summary
assert "stale" in summary
assert "router_total" in summary
assert "system_total" in summary
# Types
assert isinstance(summary["active"], int)
assert isinstance(summary["phantom"], int)
assert isinstance(data["agents"], list)
def test_get_agents_for_unknown_node(self, client):
"""Endpoint повертає пустий response для невідомої ноди"""
response = client.get("/city/internal/node/unknown-node-id/dagi-router/agents")
# Має повернути 200 з пустим списком, не 404
assert response.status_code == 200
data = response.json()
assert data["agents"] == []
assert data["summary"]["active"] == 0
def test_agents_have_required_fields(self, client):
"""Агенти мають всі необхідні поля"""
response = client.get(f"/city/internal/node/{NODE1_ID}/dagi-router/agents")
assert response.status_code == 200
data = response.json()
if data["agents"]:
agent = data["agents"][0]
# Required fields
assert "id" in agent
assert "name" in agent
assert "status" in agent
# Status must be valid
assert agent["status"] in ["active", "phantom", "stale", "error"]
# ============================================================================
# Node Metrics Tests
# ============================================================================
class TestNodeMetrics:
"""Тести для GET /internal/node/{node_id}/metrics/current"""
def test_get_metrics_returns_valid_response(self, client):
"""Endpoint повертає валідну структуру"""
response = client.get(f"/city/internal/node/{NODE1_ID}/metrics/current")
assert response.status_code == 200
data = response.json()
# Required fields
assert "node_id" in data
assert data["node_id"] == NODE1_ID
# Metric fields
assert "cpu_cores" in data
assert "cpu_usage" in data
assert "gpu_model" in data
assert "gpu_memory_total" in data
assert "gpu_memory_used" in data
assert "ram_total" in data
assert "ram_used" in data
assert "disk_total" in data
assert "disk_used" in data
assert "agent_count_router" in data
assert "agent_count_system" in data
def test_get_metrics_for_unknown_node(self, client):
"""Endpoint повертає minimal response для невідомої ноди"""
response = client.get("/city/internal/node/unknown-node-id/metrics/current")
# Має повернути 200 з мінімальним response
assert response.status_code == 200
data = response.json()
assert data["node_id"] == "unknown-node-id"
def test_metrics_have_numeric_values(self, client):
"""Метрики мають числові значення"""
response = client.get(f"/city/internal/node/{NODE1_ID}/metrics/current")
assert response.status_code == 200
data = response.json()
# All numeric fields should be numbers
numeric_fields = [
"cpu_cores", "cpu_usage",
"gpu_memory_total", "gpu_memory_used",
"ram_total", "ram_used",
"disk_total", "disk_used",
"agent_count_router", "agent_count_system"
]
for field in numeric_fields:
assert isinstance(data[field], (int, float)), f"{field} should be numeric"
# ============================================================================
# DAGI Audit Tests
# ============================================================================
class TestDAGIAudit:
"""Тести для POST /internal/node/{node_id}/dagi-audit/run"""
def test_run_audit_returns_valid_response(self, client):
"""POST audit повертає валідну структуру"""
response = client.post(f"/city/internal/node/{NODE1_ID}/dagi-audit/run")
assert response.status_code == 200
data = response.json()
assert "status" in data
assert data["status"] == "completed"
assert "summary" in data
assert "message" in data
# Summary fields
summary = data["summary"]
assert "router_total" in summary
assert "db_total" in summary
assert "active_count" in summary
assert "phantom_count" in summary
assert "stale_count" in summary
def test_get_audit_summary(self, client):
"""GET audit summary повертає дані"""
response = client.get(f"/city/internal/node/{NODE1_ID}/dagi-audit")
# Може бути 200 з даними або null
assert response.status_code == 200
data = response.json()
if data:
assert "node_id" in data
assert "timestamp" in data
assert "active_count" in data
# ============================================================================
# Phantom/Stale Sync Tests
# ============================================================================
class TestPhantomStaleSync:
"""Тести для phantom/stale sync endpoints"""
def test_phantom_sync_empty_list(self, client):
"""Sync з пустим списком не падає"""
response = client.post(
f"/city/internal/node/{NODE1_ID}/dagi-router/phantom/sync",
json={"agent_ids": []}
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "completed"
assert data["created_count"] == 0
def test_stale_mark_empty_list(self, client):
"""Mark stale з пустим списком не падає"""
response = client.post(
f"/city/internal/node/{NODE1_ID}/dagi-router/stale/mark",
json={"agent_ids": []}
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "completed"
assert data["marked_count"] == 0
# ============================================================================
# Integration Tests
# ============================================================================
class TestIntegration:
"""Інтеграційні тести"""
def test_full_audit_flow(self, client):
"""Повний цикл: audit → get agents → get metrics"""
# 1. Run audit
audit_response = client.post(f"/city/internal/node/{NODE1_ID}/dagi-audit/run")
assert audit_response.status_code == 200
# 2. Get agents
agents_response = client.get(f"/city/internal/node/{NODE1_ID}/dagi-router/agents")
assert agents_response.status_code == 200
agents_data = agents_response.json()
# 3. Get metrics
metrics_response = client.get(f"/city/internal/node/{NODE1_ID}/metrics/current")
assert metrics_response.status_code == 200
# 4. Verify consistency
audit_data = audit_response.json()
# Agent counts should match
assert agents_data["summary"]["active"] + agents_data["summary"]["phantom"] + agents_data["summary"]["stale"] >= 0
def test_both_nodes_accessible(self, client, node_ids):
"""Обидві ноди доступні через API"""
for node_id in node_ids:
response = client.get(f"/city/internal/node/{node_id}/metrics/current")
assert response.status_code == 200
data = response.json()
assert data["node_id"] == node_id
# ============================================================================
# Run tests
# ============================================================================
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])

336
tests/test_infra_smoke.py Normal file
View File

@@ -0,0 +1,336 @@
"""
Infrastructure Smoke Tests
Базові API тести для перевірки після деплою.
Запускаються як частина deploy pipeline або вручну.
Використання:
pytest tests/test_infra_smoke.py -v
pytest tests/test_infra_smoke.py -v --base-url http://localhost:7001
"""
import os
import pytest
import requests
from datetime import datetime, timezone, timedelta
from typing import Optional
# Configuration
BASE_URL = os.getenv("CITY_SERVICE_URL", "http://daarion-city-service:7001")
TIMEOUT = 10
# Node IDs
NODE1_ID = "node-1-hetzner-gex44"
NODE2_ID = "node-2-macbook-m4max"
def pytest_addoption(parser):
"""Add command line options"""
parser.addoption(
"--base-url",
action="store",
default=BASE_URL,
help="Base URL of city-service API"
)
@pytest.fixture
def base_url(request):
"""Get base URL from command line or environment"""
return request.config.getoption("--base-url") or BASE_URL
@pytest.fixture
def api_client(base_url):
"""Create API client session"""
session = requests.Session()
session.timeout = TIMEOUT
class Client:
def __init__(self, base_url: str, session: requests.Session):
self.base_url = base_url.rstrip("/")
self.session = session
def get(self, path: str) -> requests.Response:
return self.session.get(f"{self.base_url}{path}", timeout=TIMEOUT)
def post(self, path: str, json: dict) -> requests.Response:
return self.session.post(f"{self.base_url}{path}", json=json, timeout=TIMEOUT)
return Client(base_url, session)
# ==============================================================================
# Health Checks
# ==============================================================================
class TestHealthChecks:
"""Basic health check tests"""
def test_healthz_endpoint(self, api_client):
"""Test /healthz returns 200 and status ok"""
response = api_client.get("/healthz")
assert response.status_code == 200, f"Health check failed: {response.text}"
data = response.json()
assert data.get("status") == "ok", f"Unhealthy status: {data}"
def test_public_nodes_endpoint(self, api_client):
"""Test /public/nodes returns node list"""
response = api_client.get("/public/nodes")
assert response.status_code == 200, f"Nodes endpoint failed: {response.text}"
data = response.json()
assert "items" in data, "Response missing 'items' key"
assert "total" in data, "Response missing 'total' key"
# ==============================================================================
# Node Metrics Tests
# ==============================================================================
class TestNodeMetrics:
"""Node metrics tests"""
@pytest.mark.parametrize("node_id", [NODE1_ID, NODE2_ID])
def test_node_metrics_endpoint(self, api_client, node_id):
"""Test node metrics endpoint returns data"""
response = api_client.get(f"/internal/node/{node_id}/metrics/current")
assert response.status_code == 200, f"Node metrics failed for {node_id}: {response.text}"
data = response.json()
# Check required fields
assert "node_id" in data, "Missing node_id"
assert "agent_count_router" in data, "Missing agent_count_router"
assert "agent_count_system" in data, "Missing agent_count_system"
def test_node1_has_agents(self, api_client):
"""Test NODE1 has at least 1 agent in router"""
response = api_client.get(f"/internal/node/{NODE1_ID}/metrics/current")
if response.status_code != 200:
pytest.skip(f"NODE1 metrics not available: {response.status_code}")
data = response.json()
agent_count = data.get("agent_count_router", 0)
assert agent_count >= 1, f"NODE1 has {agent_count} agents in router, expected >= 1"
def test_node2_has_agents(self, api_client):
"""Test NODE2 has at least 1 agent in system"""
response = api_client.get(f"/internal/node/{NODE2_ID}/metrics/current")
if response.status_code != 200:
pytest.skip(f"NODE2 metrics not available: {response.status_code}")
data = response.json()
agent_count = data.get("agent_count_system", 0)
assert agent_count >= 1, f"NODE2 has {agent_count} agents in system, expected >= 1"
# ==============================================================================
# Node Agents Tests
# ==============================================================================
class TestNodeAgents:
"""Node agents (Guardian/Steward) tests"""
@pytest.mark.parametrize("node_id", [NODE1_ID, NODE2_ID])
def test_node_agents_endpoint(self, api_client, node_id):
"""Test node agents endpoint returns data"""
response = api_client.get(f"/internal/node/{node_id}/agents")
assert response.status_code == 200, f"Node agents failed for {node_id}: {response.text}"
data = response.json()
assert "node_id" in data, "Missing node_id"
assert "total" in data, "Missing total"
assert "agents" in data, "Missing agents list"
def test_node1_has_guardian(self, api_client):
"""Test NODE1 has Node Guardian"""
response = api_client.get(f"/internal/node/{NODE1_ID}/agents")
if response.status_code != 200:
pytest.skip(f"NODE1 agents not available: {response.status_code}")
data = response.json()
guardian = data.get("guardian")
assert guardian is not None, "NODE1 missing Node Guardian"
assert guardian.get("id"), "Guardian has no ID"
def test_node1_has_steward(self, api_client):
"""Test NODE1 has Node Steward"""
response = api_client.get(f"/internal/node/{NODE1_ID}/agents")
if response.status_code != 200:
pytest.skip(f"NODE1 agents not available: {response.status_code}")
data = response.json()
steward = data.get("steward")
assert steward is not None, "NODE1 missing Node Steward"
assert steward.get("id"), "Steward has no ID"
def test_node2_has_guardian(self, api_client):
"""Test NODE2 has Node Guardian"""
response = api_client.get(f"/internal/node/{NODE2_ID}/agents")
if response.status_code != 200:
pytest.skip(f"NODE2 agents not available: {response.status_code}")
data = response.json()
guardian = data.get("guardian")
assert guardian is not None, "NODE2 missing Node Guardian"
# ==============================================================================
# DAGI Router Tests
# ==============================================================================
class TestDAGIRouter:
"""DAGI Router tests"""
@pytest.mark.parametrize("node_id", [NODE1_ID, NODE2_ID])
def test_dagi_router_agents_endpoint(self, api_client, node_id):
"""Test DAGI Router agents endpoint returns data"""
response = api_client.get(f"/internal/node/{node_id}/dagi-router/agents")
# May return empty if no audit yet
if response.status_code == 404:
pytest.skip(f"DAGI Router not configured for {node_id}")
assert response.status_code == 200, f"DAGI Router failed for {node_id}: {response.text}"
data = response.json()
assert "node_id" in data, "Missing node_id"
assert "summary" in data, "Missing summary"
assert "agents" in data, "Missing agents list"
def test_node1_router_has_agents(self, api_client):
"""Test NODE1 DAGI Router has agents"""
response = api_client.get(f"/internal/node/{NODE1_ID}/dagi-router/agents")
if response.status_code != 200:
pytest.skip(f"NODE1 DAGI Router not available: {response.status_code}")
data = response.json()
summary = data.get("summary", {})
router_total = summary.get("router_total", 0)
# Warn but don't fail - router may not be configured
if router_total == 0:
pytest.skip("NODE1 DAGI Router has 0 agents (may not be configured)")
assert router_total >= 1, f"DAGI Router has {router_total} agents, expected >= 1"
# ==============================================================================
# Core Agents Tests
# ==============================================================================
class TestCoreAgents:
"""Core agents tests"""
def test_prompts_status_endpoint(self, api_client):
"""Test prompts status batch endpoint"""
agent_ids = ["agent-daarwizz", "agent-devtools", "agent-soul"]
response = api_client.post("/internal/agents/prompts/status", {"agent_ids": agent_ids})
assert response.status_code == 200, f"Prompts status failed: {response.text}"
data = response.json()
assert "status" in data, "Missing status in response"
assert isinstance(data["status"], dict), "Status should be a dict"
def test_daarwizz_runtime_prompt(self, api_client):
"""Test DAARWIZZ has runtime prompt"""
# Try both possible slugs
for agent_id in ["agent-daarwizz", "daarwizz"]:
response = api_client.get(f"/internal/agents/{agent_id}/prompts/runtime")
if response.status_code == 200:
data = response.json()
if data.get("has_prompts"):
assert data.get("prompts", {}).get("core"), "DAARWIZZ missing core prompt"
return
pytest.skip("DAARWIZZ agent not found or no prompts configured")
def test_runtime_system_prompt_endpoint(self, api_client):
"""Test runtime system prompt endpoint works"""
response = api_client.get("/internal/agents/agent-daarwizz/system-prompt")
if response.status_code == 404:
pytest.skip("DAARWIZZ agent not found")
assert response.status_code == 200, f"System prompt failed: {response.text}"
data = response.json()
assert "agent_id" in data, "Missing agent_id"
assert "system_prompt" in data, "Missing system_prompt"
assert len(data.get("system_prompt", "")) > 10, "System prompt too short"
# ==============================================================================
# Integration Tests
# ==============================================================================
class TestIntegration:
"""End-to-end integration tests"""
def test_node_to_agents_flow(self, api_client):
"""Test full flow: node → agents → prompts"""
# Get node
response = api_client.get(f"/internal/node/{NODE1_ID}/agents")
if response.status_code != 200:
pytest.skip(f"NODE1 not available: {response.status_code}")
data = response.json()
agents = data.get("agents", [])
if not agents:
pytest.skip("No agents found for NODE1")
# Get first agent's prompts
agent = agents[0]
agent_id = agent.get("id")
response = api_client.get(f"/internal/agents/{agent_id}/prompts/runtime")
# Should return successfully even if no prompts
assert response.status_code == 200, f"Agent prompts failed for {agent_id}: {response.text}"
def test_public_nodes_have_metrics(self, api_client):
"""Test public nodes endpoint includes metrics"""
response = api_client.get("/public/nodes")
assert response.status_code == 200
data = response.json()
items = data.get("items", [])
if not items:
pytest.skip("No nodes in system")
# Check first node has metrics
node = items[0]
# Should have metrics object after our changes
if "metrics" in node:
metrics = node["metrics"]
assert "cpu_cores" in metrics or "ram_total" in metrics, "Metrics object empty"
# ==============================================================================
# Run as script
# ==============================================================================
if __name__ == "__main__":
pytest.main([__file__, "-v"])