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