163 lines
8.4 KiB
Python
163 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from codexpi.ecosystem_service import PI_DOCTOR_PROMPT
|
|
|
|
|
|
def initialize_minimal_portfolio(app, projects):
|
|
owner = app.state.db.one("SELECT id FROM users LIMIT 1")["id"]
|
|
for slug in ("pi-doctor", "codex-commander", "autonomous-dev-studio"):
|
|
portfolio = app.state.db.one("SELECT * FROM portfolio_projects WHERE slug=?", (slug,))
|
|
path = projects / slug
|
|
path.mkdir(exist_ok=True)
|
|
workspace_id = str(uuid.uuid4())
|
|
app.state.db.execute(
|
|
"INSERT INTO projects(id,name,path,owner_id,created_at) VALUES(?,?,?,?,?)",
|
|
(workspace_id, slug, str(path), owner, "2026-01-01T00:00:00+00:00"),
|
|
)
|
|
conversation = app.state.codex.create_conversation(workspace_id, f"{slug} V0", user_id=owner)
|
|
app.state.db.execute("UPDATE conversations SET portfolio_project_id=? WHERE id=?", (portfolio["id"], conversation["id"]))
|
|
app.state.db.execute("UPDATE portfolio_projects SET workspace_id=?,owner_id=?,status='INITIALIZED' WHERE id=?", (workspace_id, owner, portfolio["id"]))
|
|
return owner
|
|
|
|
|
|
def test_pi_doctor_agent_has_exact_persistent_prompt(logged_in):
|
|
_, app, projects, _, _ = logged_in
|
|
owner = initialize_minimal_portfolio(app, projects)
|
|
|
|
agents = app.state.ecosystem.seed_agents(owner)
|
|
|
|
doctor = next(item for item in agents if item["key"] == "pi-doctor-repair-agent")
|
|
assert doctor["name"] == "Pi Doctor — Raspberry Pi Repair Agent"
|
|
assert doctor["system_prompt"] == PI_DOCTOR_PROMPT
|
|
session = app.state.db.one("SELECT title,bootstrap_context FROM conversations WHERE id=?", (doctor["conversation_id"],))
|
|
assert session == {"title": doctor["name"], "bootstrap_context": PI_DOCTOR_PROMPT}
|
|
|
|
app.state.db.execute("UPDATE conversations SET codex_thread_id='existing-thread',bootstrap_context=NULL WHERE id=?", (doctor["conversation_id"],))
|
|
app.state.ecosystem.seed_agents(owner)
|
|
persisted = app.state.db.one("SELECT bootstrap_context FROM conversations WHERE id=?", (doctor["conversation_id"],))
|
|
assert persisted["bootstrap_context"] == PI_DOCTOR_PROMPT
|
|
|
|
|
|
def test_commander_seeds_seven_roles_with_bounded_permissions(logged_in):
|
|
_, app, projects, _, _ = logged_in
|
|
owner = initialize_minimal_portfolio(app, projects)
|
|
|
|
agents = app.state.ecosystem.seed_agents(owner)
|
|
|
|
commander = [item for item in agents if item["key"].startswith("commander-")]
|
|
assert len(commander) == 7
|
|
assert all("root" not in item["permissions"] for item in commander)
|
|
assert all(item["conversation_id"] for item in commander)
|
|
assert len({item["conversation_id"] for item in commander}) == 7
|
|
|
|
|
|
def test_file_scope_lock_prevents_two_agents_on_same_files(logged_in):
|
|
_, app, projects, _, _ = logged_in
|
|
owner = initialize_minimal_portfolio(app, projects)
|
|
agents = app.state.ecosystem.seed_agents(owner)
|
|
target = app.state.db.one("SELECT id FROM portfolio_projects WHERE slug='pi-doctor'")["id"]
|
|
|
|
app.state.ecosystem.assign_agent(agents[1]["id"], target, "A", "Première tâche", ["backend/api"])
|
|
|
|
with pytest.raises(RuntimeError, match="déjà réservé"):
|
|
app.state.ecosystem.assign_agent(agents[2]["id"], target, "B", "Seconde tâche", ["backend/api/routes.py"])
|
|
|
|
with pytest.raises(RuntimeError, match="relatif"):
|
|
app.state.ecosystem.assign_agent(agents[2]["id"], target, "C", "Périmètre invalide", ["../secrets"])
|
|
|
|
|
|
def test_repair_plan_is_immutable_and_arbitrary_operation_is_refused(logged_in):
|
|
_, app, *_ = logged_in
|
|
owner = app.state.db.one("SELECT id FROM users LIMIT 1")["id"]
|
|
plan = app.state.ecosystem.create_plan(None, owner, "run_shell", {"command": "id"}, "Commande libre", "Privilèges", True, "Restaurer")
|
|
|
|
with pytest.raises(RuntimeError, match="modifié"):
|
|
app.state.ecosystem.approve_plan(plan["id"], "0" * 64, owner)
|
|
|
|
approved = app.state.ecosystem.approve_plan(plan["id"], plan["plan_hash"], owner)
|
|
assert approved["status"] == "APPROVED"
|
|
with pytest.raises(PermissionError, match="diagnostic uniquement"):
|
|
app.state.ecosystem.execute_diagnostic(plan["id"], plan["plan_hash"])
|
|
|
|
|
|
def test_incidents_are_deduplicated_and_counted(logged_in):
|
|
_, app, *_ = logged_in
|
|
incident = {"fingerprint": "mount-storage1", "severity": "warning", "service": "/media/Storage1", "symptoms": "NAS unavailable", "details": {"state": "network_or_nas_unavailable"}}
|
|
|
|
assert app.state.ecosystem.ingest_incidents([incident, incident]) == 2
|
|
|
|
row = app.state.db.one("SELECT * FROM pi_doctor_incidents WHERE fingerprint='mount-storage1'")
|
|
assert row["occurrences"] == 2
|
|
assert row["status"] == "OPEN"
|
|
|
|
|
|
def test_diagnostic_execution_records_result(logged_in):
|
|
_, app, *_ = logged_in
|
|
owner = app.state.db.one("SELECT id FROM users LIMIT 1")["id"]
|
|
plan = app.state.ecosystem.create_plan(None, owner, "diagnostic_snapshot", {}, "Instantané", "Charge brève", False, "Aucune restauration")
|
|
app.state.ecosystem.approve_plan(plan["id"], plan["plan_hash"], owner)
|
|
|
|
execution = app.state.ecosystem.execute_diagnostic(plan["id"], plan["plan_hash"])
|
|
|
|
assert execution["status"] == "SUCCEEDED"
|
|
assert "cpu_percent" in execution["result"]
|
|
|
|
|
|
def test_collector_snapshot_is_authenticated_persisted_and_used_by_dashboard(logged_in):
|
|
client, app, *_ = logged_in
|
|
token = (app.state.settings.data_dir / "pi-doctor" / "ingest.token").read_text(encoding="utf-8").strip()
|
|
snapshot = {
|
|
"system": {"timestamp": "2026-10-27T12:00:00+00:00", "cpu_percent": 12.5, "memory_percent": 44,
|
|
"temperature_c": 52.3, "boot_time": "2026-10-27T08:00:00+00:00", "kernel_error_count": 2,
|
|
"previous_boot_error_count": 1, "abnormal_restart_count": 0},
|
|
"storage": {"root_percent": 63, "mounts": [{"path": "/media/Storage1", "fstype": "cifs", "state": "mounted", "percent": 71}]},
|
|
"docker": {"available": True, "containers": [{"name": "jellyfin", "state": "running", "status": "Up"}]},
|
|
"network": {"nas_smb": {"configured": True, "reachable": True}},
|
|
"services": {"jellyfin": {"available": True}, "codexpi": {"available": True}},
|
|
}
|
|
|
|
assert client.post("/api/pi-doctor/snapshots/import", json={"snapshot": snapshot}).status_code == 401
|
|
imported = client.post(
|
|
"/api/pi-doctor/snapshots/import", json={"snapshot": snapshot}, headers={"X-Pi-Doctor-Token": token},
|
|
)
|
|
assert imported.status_code == 201
|
|
dashboard = client.get("/api/pi-doctor").json()
|
|
assert dashboard["snapshot"]["source"] == "collector"
|
|
assert dashboard["snapshot"]["temperature_c"] == 52.3
|
|
assert dashboard["snapshot"]["mounts"][0]["status"] == "mounted"
|
|
assert dashboard["snapshot"]["docker"]["containers"][0]["name"] == "jellyfin"
|
|
|
|
token_path = app.state.settings.data_dir / "pi-doctor" / "ingest.token"
|
|
token_path.chmod(0o644)
|
|
refused = client.post(
|
|
"/api/pi-doctor/snapshots/import", json={"snapshot": snapshot}, headers={"X-Pi-Doctor-Token": token},
|
|
)
|
|
assert refused.status_code == 503
|
|
|
|
|
|
def test_agent_assignment_actions_require_completion_before_approval(logged_in):
|
|
client, app, projects, _, headers = logged_in
|
|
owner = initialize_minimal_portfolio(app, projects)
|
|
agent = next(item for item in app.state.ecosystem.seed_agents(owner) if item["key"] == "commander-backend")
|
|
target = app.state.db.one("SELECT id FROM portfolio_projects WHERE slug='pi-doctor'")["id"]
|
|
assignment = app.state.ecosystem.assign_agent(agent["id"], target, "API", "Ajouter une route", ["backend/api"])
|
|
|
|
pending = client.post(f"/api/ecosystem/assignments/{assignment['id']}/action", headers=headers, json={"action": "approve"})
|
|
assert pending.status_code == 409
|
|
paused = client.post(f"/api/ecosystem/assignments/{assignment['id']}/action", headers=headers, json={"action": "pause"})
|
|
assert paused.status_code == 200
|
|
assert paused.json()["status"] == "PAUSED"
|
|
resumed = client.post(f"/api/ecosystem/assignments/{assignment['id']}/action", headers=headers, json={"action": "resume"})
|
|
assert resumed.status_code == 200
|
|
assert resumed.json()["status"] == "QUEUED"
|
|
rejected = client.post(f"/api/ecosystem/assignments/{assignment['id']}/action", headers=headers, json={"action": "reject"})
|
|
assert rejected.status_code == 200
|
|
assert rejected.json()["status"] == "REJECTED"
|
|
listed = client.get("/api/ecosystem/agents").json()
|
|
listed_assignment = next(item for item in listed if item["id"] == agent["id"])["assignments"][0]
|
|
assert listed_assignment["status"] == "REJECTED"
|