feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run
Some checks are pending
Qualité / tests (push) Waiting to run
This commit is contained in:
398
tests/test_coordination.py
Normal file
398
tests/test_coordination.py
Normal file
@@ -0,0 +1,398 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from codexpi.agent_protocol import parse_decision_request, parse_specialist_call, record_agent_outcome
|
||||
from codexpi.codex_service import CodexManager
|
||||
from codexpi.coordination_service import CoordinationError, CoordinationService
|
||||
from codexpi.database import Database, utcnow
|
||||
from codexpi.session_worker import _background_window_open, concurrent_agent_limit
|
||||
|
||||
|
||||
def test_existing_queue_schema_is_migrated_before_priority_index(tmp_path):
|
||||
path = tmp_path / "legacy.db"
|
||||
database = Database(path)
|
||||
database.initialize()
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.execute("DROP INDEX idx_prompt_queue_priority")
|
||||
conn.execute("ALTER TABLE prompt_queue DROP COLUMN slot_acquired_at")
|
||||
conn.execute("ALTER TABLE prompt_queue DROP COLUMN origin_kind")
|
||||
conn.execute("ALTER TABLE prompt_queue DROP COLUMN delegation_depth")
|
||||
conn.execute("ALTER TABLE prompt_queue DROP COLUMN priority")
|
||||
|
||||
database.initialize()
|
||||
|
||||
with sqlite3.connect(path) as conn:
|
||||
columns = {row[1] for row in conn.execute("PRAGMA table_info(prompt_queue)")}
|
||||
indexes = {row[1] for row in conn.execute("PRAGMA index_list(prompt_queue)")}
|
||||
assert {"priority", "delegation_depth", "origin_kind", "slot_acquired_at"} <= columns
|
||||
assert "idx_prompt_queue_priority" in indexes
|
||||
|
||||
|
||||
def environment(tmp_path):
|
||||
database = Database(tmp_path / "codexpi.db")
|
||||
database.initialize()
|
||||
owner = "owner-1"
|
||||
project_path = tmp_path / "project"
|
||||
self_path = tmp_path / "self"
|
||||
project_path.mkdir()
|
||||
self_path.mkdir()
|
||||
project_id = str(uuid.uuid4())
|
||||
self_id = str(uuid.uuid4())
|
||||
database.execute(
|
||||
"INSERT INTO projects(id,name,path,owner_id,full_access,created_at) VALUES(?,?,?,?,?,?)",
|
||||
(project_id, "Projet source", str(project_path), owner, 0, utcnow()),
|
||||
)
|
||||
database.execute(
|
||||
"INSERT INTO projects(id,name,path,owner_id,full_access,execution_group,created_at) VALUES(?,?,?,?,?,?,?)",
|
||||
(self_id, "CodexPi (Self)", str(self_path), owner, 1, "__global_agent__", utcnow()),
|
||||
)
|
||||
codex = CodexManager(database, tmp_path / "data", "/bin/true")
|
||||
codex._spawn = lambda *args, **kwargs: 4242
|
||||
service = CoordinationService(database, codex, tmp_path / "data")
|
||||
return database, codex, service, owner, project_id
|
||||
|
||||
|
||||
def completed_output(database: Database, conversation_id: str, queue_id: str, content: str) -> None:
|
||||
database.execute(
|
||||
"UPDATE prompt_queue SET status='done',finished_at=? WHERE id=?",
|
||||
(utcnow(), queue_id),
|
||||
)
|
||||
database.execute(
|
||||
"UPDATE conversations SET status='completed',worker_pid=NULL,updated_at=? WHERE id=?",
|
||||
(utcnow(), conversation_id),
|
||||
)
|
||||
database.execute(
|
||||
"INSERT INTO messages(id,conversation_id,role,content,metadata,created_at) VALUES(?,?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), conversation_id, "assistant", content, json.dumps({"queue_id": queue_id}), utcnow()),
|
||||
)
|
||||
|
||||
|
||||
def test_coordination_markers_are_strict():
|
||||
decision = parse_decision_request(
|
||||
'CODEXPI_DECISION {"title":"Déploiement","question":"Quelle cible ?","options":['
|
||||
'{"id":"staging","label":"Staging"},{"id":"production","label":"Production"}],'
|
||||
'"allow_multiple":false,"allow_custom":true}'
|
||||
)
|
||||
assert decision and decision["question"] == "Quelle cible ?"
|
||||
assert len(decision["options"]) == 2
|
||||
assert parse_decision_request('CODEXPI_DECISION {"question":"Sans choix","options":[]}') is None
|
||||
assert parse_specialist_call(
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.interface","task":"Auditer les écrans mobiles", "context":"PWA"}'
|
||||
) == {"capability": "design.interface", "task": "Auditer les écrans mobiles", "context": "PWA"}
|
||||
|
||||
|
||||
def test_coordination_http_endpoints_are_owner_scoped_and_csrf_protected(logged_in, monkeypatch):
|
||||
client, app, _, _, headers = logged_in
|
||||
monkeypatch.setattr(app.state.codex, "_spawn", lambda *args, **kwargs: 4242)
|
||||
monkeypatch.setattr(app.state.codex, "detect", lambda: "/bin/true")
|
||||
|
||||
assert client.put(
|
||||
"/api/coordination/runtime-policies",
|
||||
json={
|
||||
"max_concurrent": 2, "load_pause_percent": 85, "memory_pause_percent": 88,
|
||||
"background_start_hour": 22, "background_end_hour": 7,
|
||||
},
|
||||
).status_code == 403
|
||||
saved = client.put(
|
||||
"/api/coordination/runtime-policies", headers=headers,
|
||||
json={
|
||||
"max_concurrent": 2, "load_pause_percent": 85, "memory_pause_percent": 88,
|
||||
"background_start_hour": 22, "background_end_hour": 7,
|
||||
},
|
||||
)
|
||||
assert saved.status_code == 200
|
||||
assert client.get("/api/coordination/runtime-policies").json()["global"]["max_concurrent"] == 2
|
||||
overview = client.get("/api/coordination/overview")
|
||||
assert overview.status_code == 200
|
||||
assert set(overview.json()) == {"decisions", "incidents", "runtime_policies", "specialists"}
|
||||
|
||||
preferences = client.put(
|
||||
"/api/notifications/preferences", headers=headers,
|
||||
json={"task_completion": False, "decisions": True, "incidents": True},
|
||||
)
|
||||
assert preferences.status_code == 200
|
||||
assert preferences.json() == {"task_completion": False, "decisions": True, "incidents": True}
|
||||
|
||||
created = client.post(
|
||||
"/api/coordination/specialists", headers=headers,
|
||||
json={
|
||||
"name": "Studio Interface",
|
||||
"description": "Conçoit des interfaces PWA accessibles et responsive pour les projets.",
|
||||
"capabilities": ["design.interface", "design.ux"],
|
||||
},
|
||||
)
|
||||
assert created.status_code == 201
|
||||
assert created.json()["status"] == "BUILDING"
|
||||
assert created.json()["full_access"] is False
|
||||
assert len(client.get("/api/coordination/specialists").json()) == 1
|
||||
|
||||
|
||||
def test_decision_answer_resumes_the_same_conversation(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
project = database.one("SELECT * FROM projects WHERE id=?", (project_id,))
|
||||
conversation = codex.create_conversation(project_id, "Travail", user_id=owner)
|
||||
execution = codex.enqueue(conversation, project, "Prépare le changement", requested_by=owner)
|
||||
completed_output(database, conversation["id"], execution["id"], "Réponse provisoire")
|
||||
marker = (
|
||||
'CODEXPI_DECISION {"title":"Choix","question":"Quelle option ?","options":['
|
||||
'{"id":"a","label":"Option A"},{"id":"b","label":"Option B"}],'
|
||||
'"allow_multiple":false,"allow_custom":true}'
|
||||
)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(conn, conversation["id"], execution["id"], "done", marker, None)
|
||||
decision = service.decisions(owner)[0]
|
||||
|
||||
answered = service.answer_decision(owner, decision["id"], ["a"], "")
|
||||
|
||||
assert answered["status"] == "ANSWERED"
|
||||
response = database.one("SELECT * FROM prompt_queue WHERE id=?", (answered["response_queue_id"],))
|
||||
assert response["conversation_id"] == conversation["id"]
|
||||
assert response["origin_kind"] == "decision_answer"
|
||||
assert "Option A" in response["content"]
|
||||
with pytest.raises(CoordinationError, match="déjà"):
|
||||
service.answer_decision(owner, decision["id"], ["b"], "")
|
||||
|
||||
|
||||
def test_failed_agent_becomes_a_self_repairable_incident(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
project = database.one("SELECT * FROM projects WHERE id=?", (project_id,))
|
||||
conversation = codex.create_conversation(project_id, "Travail", user_id=owner)
|
||||
execution = codex.enqueue(conversation, project, "Exécute", requested_by=owner)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(
|
||||
conn, conversation["id"], execution["id"], "failed", None,
|
||||
"Le worker s’est arrêté avec une exception inattendue",
|
||||
)
|
||||
|
||||
incident = service.incidents(owner)[0]
|
||||
assert incident["status"] == "OPEN"
|
||||
assert incident["auto_repair"] is True
|
||||
service.tick()
|
||||
repairing = service.incidents(owner)[0]
|
||||
assert repairing["status"] == "REPAIRING"
|
||||
assert repairing["repair_project_name"] == "CodexPi (Self)"
|
||||
|
||||
|
||||
def test_specialist_builds_itself_and_receives_a_delegation(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
specialist = service.create_specialist(
|
||||
owner, "Studio Interface", "Conçoit et audite les interfaces mobiles PWA des projets.", ["design.interface"],
|
||||
)
|
||||
assert specialist["status"] == "BUILDING"
|
||||
assert specialist["full_access"] is False
|
||||
assert (service.data_dir / "specialists").is_dir()
|
||||
completed_output(
|
||||
database, specialist["bootstrap_conversation_id"], specialist["bootstrap_queue_id"],
|
||||
'CODEXPI_SPECIALIST_READY {"summary":"Prêt","capabilities":["design.ux"]}',
|
||||
)
|
||||
service.tick()
|
||||
specialist = service.specialist(owner, specialist["id"])
|
||||
assert specialist["status"] == "ACTIVE"
|
||||
assert "design.interface" in specialist["capabilities"]
|
||||
assert "design.ux" in specialist["capabilities"]
|
||||
|
||||
source_project = database.one("SELECT * FROM projects WHERE id=?", (project_id,))
|
||||
source_conversation = codex.create_conversation(project_id, "Refonte", user_id=owner)
|
||||
source_execution = codex.enqueue(source_conversation, source_project, "Refonds la page", requested_by=owner)
|
||||
completed_output(
|
||||
database, source_conversation["id"], source_execution["id"],
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.interface","task":"Préparer la maquette mobile",'
|
||||
'"context":"Respecter le thème crème"}',
|
||||
)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(
|
||||
conn, source_conversation["id"], source_execution["id"], "done",
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.interface","task":"Préparer la maquette mobile",'
|
||||
'"context":"Respecter le thème crème"}', None,
|
||||
)
|
||||
service.tick()
|
||||
delegation = database.one("SELECT * FROM specialist_delegations WHERE source_queue_id=?", (source_execution["id"],))
|
||||
assert delegation["status"] == "RUNNING"
|
||||
completed_output(
|
||||
database, delegation["specialist_conversation_id"], delegation["queue_id"],
|
||||
"Maquette prête dans output/mobile-interface.svg",
|
||||
)
|
||||
service.tick()
|
||||
delegation = database.one("SELECT * FROM specialist_delegations WHERE id=?", (delegation["id"],))
|
||||
assert delegation["status"] == "COMPLETED"
|
||||
follow_up = database.one(
|
||||
"SELECT * FROM prompt_queue WHERE conversation_id=? AND origin_kind='specialist_result' ORDER BY created_at DESC LIMIT 1",
|
||||
(source_conversation["id"],),
|
||||
)
|
||||
assert follow_up and "mobile-interface.svg" in follow_up["content"]
|
||||
|
||||
|
||||
def test_failed_specialist_can_rebuild_without_replacing_its_workspace(tmp_path):
|
||||
database, _, service, owner, _ = environment(tmp_path)
|
||||
specialist = service.create_specialist(
|
||||
owner, "Studio Logo", "Conçoit des identités visuelles et des logos cohérents pour les projets.", ["design.logo"],
|
||||
)
|
||||
workspace = database.one("SELECT path FROM projects WHERE id=?", (specialist["project_id"],))["path"]
|
||||
database.execute(
|
||||
"UPDATE specialist_agents SET status='FAILED',error='incident de construction' WHERE id=?",
|
||||
(specialist["id"],),
|
||||
)
|
||||
|
||||
rebuilt = service.rebuild_specialist(owner, specialist["id"])
|
||||
|
||||
assert rebuilt["status"] == "BUILDING"
|
||||
assert rebuilt["error"] == ""
|
||||
assert rebuilt["bootstrap_conversation_id"] != specialist["bootstrap_conversation_id"]
|
||||
assert database.one("SELECT path FROM projects WHERE id=?", (specialist["project_id"],))["path"] == workspace
|
||||
|
||||
|
||||
def test_two_specialists_can_collaborate_and_return_to_source(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
interface = service.create_specialist(
|
||||
owner, "Studio Interface", "Conçoit les interfaces et coordonne leur identité visuelle.", ["design.interface"],
|
||||
)
|
||||
logo = service.create_specialist(
|
||||
owner, "Studio Logo", "Conçoit les logos et systèmes d’identité pour les interfaces.", ["design.logo"],
|
||||
)
|
||||
for specialist, capability in ((interface, "design.interface"), (logo, "design.logo")):
|
||||
completed_output(
|
||||
database, specialist["bootstrap_conversation_id"], specialist["bootstrap_queue_id"],
|
||||
f'CODEXPI_SPECIALIST_READY {{"summary":"Prêt","capabilities":["{capability}"]}}',
|
||||
)
|
||||
service.tick()
|
||||
|
||||
project = database.one("SELECT * FROM projects WHERE id=?", (project_id,))
|
||||
source_conversation = codex.create_conversation(project_id, "Identité", user_id=owner)
|
||||
source_queue = codex.enqueue(source_conversation, project, "Refonds la marque", requested_by=owner)
|
||||
first_call = (
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.interface","task":"Préparer la direction UI",'
|
||||
'"context":"Coordonner le logo"}'
|
||||
)
|
||||
completed_output(database, source_conversation["id"], source_queue["id"], first_call)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(conn, source_conversation["id"], source_queue["id"], "done", first_call, None)
|
||||
service.tick()
|
||||
parent = database.one("SELECT * FROM specialist_delegations WHERE source_queue_id=?", (source_queue["id"],))
|
||||
|
||||
second_call = (
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.logo","task":"Produire le symbole de marque",'
|
||||
'"context":"Doit fonctionner avec la direction UI"}'
|
||||
)
|
||||
completed_output(database, parent["specialist_conversation_id"], parent["queue_id"], second_call)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(
|
||||
conn, parent["specialist_conversation_id"], parent["queue_id"], "done", second_call, None,
|
||||
)
|
||||
assert database.one("SELECT status FROM specialist_delegations WHERE id=?", (parent["id"],))["status"] == "WAITING_CHILD"
|
||||
service.tick()
|
||||
child = database.one("SELECT * FROM specialist_delegations WHERE parent_delegation_id=?", (parent["id"],))
|
||||
assert child["status"] == "RUNNING"
|
||||
|
||||
completed_output(
|
||||
database, child["specialist_conversation_id"], child["queue_id"],
|
||||
"Logo prêt dans output/logo.svg",
|
||||
)
|
||||
service.tick()
|
||||
resumed_parent = database.one("SELECT * FROM specialist_delegations WHERE id=?", (parent["id"],))
|
||||
assert resumed_parent["status"] == "RUNNING"
|
||||
completed_output(
|
||||
database, parent["specialist_conversation_id"], resumed_parent["queue_id"],
|
||||
"Interface et logo coordonnés dans output/ui-kit.svg",
|
||||
)
|
||||
service.tick()
|
||||
assert database.one("SELECT status FROM specialist_delegations WHERE id=?", (parent["id"],))["status"] == "COMPLETED"
|
||||
delivered = database.one(
|
||||
"SELECT content FROM prompt_queue WHERE conversation_id=? AND origin_kind='specialist_result' ORDER BY created_at DESC LIMIT 1",
|
||||
(source_conversation["id"],),
|
||||
)
|
||||
assert delivered and "ui-kit.svg" in delivered["content"]
|
||||
|
||||
|
||||
def test_decision_inside_specialist_resumes_delegation_chain(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
specialist = service.create_specialist(
|
||||
owner, "Studio Interface", "Conçoit les interfaces mobiles et leurs règles d’intégration.", ["design.interface"],
|
||||
)
|
||||
completed_output(
|
||||
database, specialist["bootstrap_conversation_id"], specialist["bootstrap_queue_id"],
|
||||
'CODEXPI_SPECIALIST_READY {"summary":"Prêt","capabilities":["design.interface"]}',
|
||||
)
|
||||
service.tick()
|
||||
project = database.one("SELECT * FROM projects WHERE id=?", (project_id,))
|
||||
conversation = codex.create_conversation(project_id, "Refonte", user_id=owner)
|
||||
source = codex.enqueue(conversation, project, "Refonds la page", requested_by=owner)
|
||||
call = (
|
||||
'CODEXPI_SPECIALIST_CALL {"capability":"design.interface","task":"Préparer deux directions visuelles",'
|
||||
'"context":"PWA mobile"}'
|
||||
)
|
||||
completed_output(database, conversation["id"], source["id"], call)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(conn, conversation["id"], source["id"], "done", call, None)
|
||||
service.tick()
|
||||
delegation = database.one("SELECT * FROM specialist_delegations WHERE source_queue_id=?", (source["id"],))
|
||||
decision_marker = (
|
||||
'CODEXPI_DECISION {"title":"Direction","question":"Quelle direction retenir ?","options":['
|
||||
'{"id":"calme","label":"Calme"},{"id":"dense","label":"Dense"}],'
|
||||
'"allow_multiple":false,"allow_custom":true}'
|
||||
)
|
||||
completed_output(database, delegation["specialist_conversation_id"], delegation["queue_id"], decision_marker)
|
||||
with database.connect() as conn:
|
||||
record_agent_outcome(
|
||||
conn, delegation["specialist_conversation_id"], delegation["queue_id"], "done", decision_marker, None,
|
||||
)
|
||||
waiting = database.one("SELECT * FROM specialist_delegations WHERE id=?", (delegation["id"],))
|
||||
assert waiting["status"] == "WAITING_CHILD"
|
||||
|
||||
decision = service.decisions(owner)[0]
|
||||
answered = service.answer_decision(owner, decision["id"], ["calme"], "")
|
||||
resumed = database.one("SELECT * FROM specialist_delegations WHERE id=?", (delegation["id"],))
|
||||
assert resumed["status"] == "RUNNING"
|
||||
assert resumed["queue_id"] == answered["response_queue_id"]
|
||||
|
||||
completed_output(
|
||||
database, delegation["specialist_conversation_id"], answered["response_queue_id"],
|
||||
"Direction calme préparée dans output/interface-calme.svg",
|
||||
)
|
||||
service.tick()
|
||||
completed = database.one("SELECT * FROM specialist_delegations WHERE id=?", (delegation["id"],))
|
||||
assert completed["status"] == "COMPLETED"
|
||||
follow_up = database.one(
|
||||
"SELECT content FROM prompt_queue WHERE conversation_id=? AND origin_kind='specialist_result' ORDER BY created_at DESC LIMIT 1",
|
||||
(conversation["id"],),
|
||||
)
|
||||
assert follow_up and "interface-calme.svg" in follow_up["content"]
|
||||
|
||||
|
||||
def test_runtime_policy_is_dynamic_and_background_window_supports_midnight(tmp_path):
|
||||
database, codex, service, owner, project_id = environment(tmp_path)
|
||||
service.update_runtime_policy(owner, {
|
||||
"max_concurrent": 2, "load_pause_percent": 85, "memory_pause_percent": 88,
|
||||
"background_start_hour": 22, "background_end_hour": 7,
|
||||
})
|
||||
assert concurrent_agent_limit(str(database.path), project_id) == 2
|
||||
legacy_id = str(uuid.uuid4())
|
||||
legacy_path = tmp_path / "legacy-project"
|
||||
legacy_path.mkdir()
|
||||
database.execute(
|
||||
"INSERT INTO projects(id,name,path,owner_id,created_at) VALUES(?,?,?,?,?)",
|
||||
(legacy_id, "Projet historique", str(legacy_path), None, utcnow()),
|
||||
)
|
||||
legacy_project = database.one("SELECT * FROM projects WHERE id=?", (legacy_id,))
|
||||
legacy_conversation = codex.create_conversation(legacy_id, "Travail", user_id=owner)
|
||||
legacy_queue = codex.enqueue(legacy_conversation, legacy_project, "Analyse", requested_by=owner)
|
||||
assert concurrent_agent_limit(str(database.path), legacy_id, legacy_queue["id"]) == 2
|
||||
assert any(item["id"] == legacy_id for item in service.runtime_policies(owner)["projects"])
|
||||
assert _background_window_open(22, 7, 23) is True
|
||||
assert _background_window_open(22, 7, 5) is True
|
||||
assert _background_window_open(22, 7, 12) is False
|
||||
|
||||
|
||||
def test_coordination_lease_prevents_double_processing_during_blue_green(tmp_path):
|
||||
database, codex, service, owner, _ = environment(tmp_path)
|
||||
standby = CoordinationService(database, codex, service.data_dir)
|
||||
assert service._acquire_lease() is True
|
||||
try:
|
||||
assert standby.tick() == {"state": "standby"}
|
||||
finally:
|
||||
service._release_lease()
|
||||
assert standby.tick() == {"state": "processed"}
|
||||
Reference in New Issue
Block a user