Files
codexpi/tests/test_orchestration_workflow.py
CodexPi 995b17a6e2
Some checks are pending
Qualité / tests (push) Waiting to run
feat: publish CodexPi contest snapshot
2026-11-14 20:23:59 +01:00

120 lines
5.8 KiB
Python

from __future__ import annotations
import subprocess
import uuid
from pathlib import Path
def disposable_repository(logged_in):
client, app, projects, _, headers = logged_in
root = projects / "orchestration-repo"
root.mkdir()
subprocess.run(["git", "init", "-b", "main"], cwd=root, check=True, capture_output=True)
(root / "app.txt").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "add", "app.txt"], cwd=root, check=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "-m", "Base"],
cwd=root, check=True, capture_output=True,
)
owner = app.state.db.one("SELECT id FROM users LIMIT 1")["id"]
project_id = str(uuid.uuid4())
app.state.db.execute(
"INSERT INTO projects(id,name,path,owner_id,full_access,merge_confirmation_required,created_at) VALUES(?,?,?,?,?,?,?)",
(project_id, "Disposable", str(root), owner, 1, 1, "2026-01-01"),
)
return client, app, headers, root, project_id
def finish_agent(app, orchestration: dict, content: str) -> Path:
worktree = Path(orchestration["worktree_path"])
(worktree / "app.txt").write_text(content, encoding="utf-8")
app.state.db.execute("UPDATE prompt_queue SET status='done',finished_at='2026-01-02' WHERE id=?", (orchestration["queue_id"],))
app.state.db.execute("UPDATE conversations SET status='completed',worker_pid=NULL WHERE id=?", (orchestration["conversation_id"],))
return worktree
def test_complete_orchestration_refusal_correction_resume_and_merge(logged_in, monkeypatch):
client, app, headers, root, project_id = disposable_repository(logged_in)
monkeypatch.setattr(app.state.codex, "detect", lambda *args: "/synthetic-agent")
monkeypatch.setattr(app.state.codex, "_spawn", lambda *args: 4242)
created = client.post(
"/api/orchestrations", headers=headers,
json={
"project_id": project_id, "name": "Refonte jetable",
"request": "Modifier le fichier de démonstration sans toucher au dépôt principal avant validation.",
"acceptance_criteria": ["Le contenu final vaut approved", "La vérification dédiée réussit"],
"tests": [["python3", "-c", "from pathlib import Path; assert Path('app.txt').read_text().strip() in {'draft','approved'}"]],
"engine": "codex",
},
)
assert created.status_code == 201, created.text
flow = created.json()
assert flow["status"] == "DRAFT_REVIEW"
rejected = client.post(
f"/api/orchestrations/{flow['id']}/draft/reject", headers=headers,
json={"feedback": "Préciser la preuve attendue et conserver la branche dédiée."},
)
assert rejected.status_code == 200
flow = rejected.json()
assert flow["draft_version"] == 2
assert "preuve attendue" in flow["draft"]
approved = client.post(
f"/api/orchestrations/{flow['id']}/draft/approve", headers=headers,
json={"draft_hash": flow["draft_hash"]},
)
assert approved.status_code == 200, approved.text
flow = approved.json()
worktree = finish_agent(app, flow, "draft\n")
assert not (root / "app.txt").read_text().startswith("draft")
first_detail = client.get(f"/api/orchestrations/{flow['id']}").json()
second_detail = client.get(f"/api/orchestrations/{flow['id']}").json()
assert first_detail["status"] == second_detail["status"] == "AWAITING_VERIFICATION"
assert len([event for event in second_detail["events"] if event["kind"] == "agent.finished"]) == 1
verified = client.post(f"/api/orchestrations/{flow['id']}/verify", headers=headers)
assert verified.status_code == 200, verified.text
flow = verified.json()
assert flow["status"] == "USER_REVIEW"
first_revision = flow["verified_revision"]
corrections = client.post(
f"/api/orchestrations/{flow['id']}/corrections", headers=headers,
json={"feedback": "La recette refuse draft : remplacer par approved."},
)
assert corrections.status_code == 200, corrections.text
flow = corrections.json()
assert flow["conversation_id"] == first_detail["conversation_id"]
assert Path(flow["worktree_path"]) == worktree
finish_agent(app, flow, "approved\n")
assert client.get(f"/api/orchestrations/{flow['id']}").json()["status"] == "AWAITING_VERIFICATION"
flow = client.post(f"/api/orchestrations/{flow['id']}/verify", headers=headers).json()
assert flow["status"] == "USER_REVIEW"
assert flow["verified_revision"] != first_revision
flow = client.post(
f"/api/orchestrations/{flow['id']}/approve", headers=headers,
json={"revision": flow["verified_revision"]},
).json()
assert flow["status"] == "AWAITING_MERGE_CONFIRMATION"
assert client.post(f"/api/orchestrations/{flow['id']}/merge", headers=headers, json={"confirm": False}).status_code == 409
merged = client.post(f"/api/orchestrations/{flow['id']}/merge", headers=headers, json={"confirm": True})
assert merged.status_code == 200, merged.text
assert merged.json()["status"] == "MERGED"
assert (root / "app.txt").read_text(encoding="utf-8") == "approved\n"
def test_orchestration_cannot_be_started_by_unauthorized_user(logged_in):
client, app, headers, _, project_id = disposable_repository(logged_in)
app.state.auth.create_user("other", "a-different-long-test-password", is_admin=False)
client.post("/api/auth/logout", headers=headers)
login = client.post("/api/auth/login", json={"username": "other", "password": "a-different-long-test-password"}).json()
response = client.post(
"/api/orchestrations", headers={"X-CSRF-Token": login["csrf_token"]},
json={"project_id": project_id, "name": "Interdit", "request": "Cette demande ne doit jamais démarrer dans le projet privé.", "acceptance_criteria": ["Refus"], "tests": []},
)
assert response.status_code == 404