ouroboros/tests/test_workflow_blocked_handoff_commit.py

160 lines
9.7 KiB
Python

"""Real two-task Git continuation: saved Blocking correction earns fresh authority."""
from types import SimpleNamespace
import json
import subprocess
import sys
from ouroboros.agent import OuroborosAgent
from ouroboros.agent_startup_checks import validate_task_authority_sources
from ouroboros.mutation_attribution import (
attributed_git_candidates, capture_mutation_baseline, record_terminal_mutation_candidates,
)
from ouroboros.review_state import load_state
from ouroboros.task_results import load_task_result, write_task_result
from ouroboros.tools import git
from ouroboros.tools.registry import ToolContext
from ouroboros.tools.scope_review import ScopeReviewResult
from tests.test_mutation_attribution import _git, _repo
def _promote_retained_correction(root, data, monkeypatch):
from ouroboros.server_routing_context import _task_result_ground_truth
from ouroboros.tools import control_routing
from supervisor import queue, workers
from supervisor.events import _handle_promote_chat_to_task
pending, running, pool = [], {}, {0: SimpleNamespace()}
for module in (queue, workers):
monkeypatch.setattr(module, "DRIVE_ROOT", data)
monkeypatch.setattr(module, "PENDING", pending)
monkeypatch.setattr(module, "RUNNING", running)
monkeypatch.setattr(workers, "WORKERS", pool)
monkeypatch.setattr(workers, "_WORKER_POOL_DISABLED_REASON", "")
monkeypatch.setattr(queue, "QUEUE_SNAPSHOT_PATH", data / "state/queue_snapshot.json")
for name in ("ADMISSION_RESERVATIONS", "ACCEPTANCE_FENCES", "BUDGET_ROOT_FENCES"):
monkeypatch.setattr(queue, name, {})
monkeypatch.setattr(queue, "QUEUE_SEQ_COUNTER_REF", {"value": 0})
supervisor = SimpleNamespace(
DRIVE_ROOT=data, WORKERS=pool, PENDING=pending, RUNNING=running, bridge=None,
enqueue_task=queue.enqueue_task, persist_queue_snapshot=queue.persist_queue_snapshot,
load_state=lambda: {"owner_chat_id": 1}, append_jsonl=lambda *_a, **_kw: None,
)
def deliver(_ctx, event):
assert event["predecessor_task_id"] == "first"
return "test_event_bus", _handle_promote_chat_to_task(event, supervisor)
monkeypatch.setattr(control_routing, "_emit_and_wait_for_routing", deliver)
router = ToolContext(repo_dir=root, drive_root=data, task_id="decision",
current_chat_id=1, task_metadata={"main_routing_manifest": {
"final_results": [_task_result_ground_truth(load_task_result(data, "first"))]}})
response = control_routing._promote_chat_to_task(
router, "Finish the retained correction", predecessor_task_id="first", workspace="none")
assert "durably scheduled" in response, response
assert len(pending) == 1 and "predecessor_task_id" not in pending[0]
snapshot = json.loads(queue.QUEUE_SNAPSHOT_PATH.read_text(encoding="utf-8"))
assert snapshot["pending"][0]["task"]["predecessor_authority_source"] == pending[0]["predecessor_authority_source"]
pending.clear()
assert queue.restore_pending_from_snapshot() == 1
assert "predecessor_task_id" not in pending[0]
return pending[0]
def test_second_task_reviews_and_commits_only_explicitly_selected_correction(tmp_path, monkeypatch):
root, data = _repo(tmp_path), tmp_path / "data"
(root / "VERSION").write_text("1.0.0\n", encoding="utf-8")
_git(root, "add", "VERSION")
_git(root, "commit", "-qm", "fixture version")
before = _git(root, "rev-parse", "HEAD")
(root / "dirty.txt").write_text("unrelated owner WIP\n", encoding="utf-8")
monkeypatch.setenv("OUROBOROS_REVIEW_ENFORCEMENT", "blocking")
monkeypatch.setenv("OUROBOROS_REVIEW_MAX_CYCLES", "1")
reviews, checks, publications = [], [], []
monkeypatch.setattr(git, "advisory_gate_unavailable", lambda: False)
monkeypatch.setattr(git, "_managed_candidate_needs_proof", lambda _ctx: False)
monkeypatch.setattr(git, "_post_commit_result", lambda *_a, **_kw: None)
monkeypatch.setattr(git, "_auto_push", lambda *_a, **_kw: publications.append("mock") or "")
# These independent language services are not the critic transport under test.
monkeypatch.setattr("ouroboros.tools.review_synthesis.synthesize_to_canonical_issues", lambda findings, **_kw: findings)
monkeypatch.setattr("ouroboros.review_state.compute_obligation_semantic_redirects", lambda *_a, **_kw: {})
def context(task_id):
return ToolContext(repo_dir=root, drive_root=data, task_id=task_id,
branch_dev=_git(root, "branch", "--show-current"), task_metadata={"root_task_id": task_id},
emit_progress_fn=lambda *_a, **_kw: None)
def preflight(ctx, **_kw):
result = subprocess.run([sys.executable, "-c",
"from pathlib import Path; assert Path('clean.txt').read_text(encoding='utf-8').strip(); assert Path('new.txt').is_file()"],
cwd=root, capture_output=True, text=True)
checks.append({"task": ctx.task_id, "exit": result.returncode})
return result.stderr if result.returncode else None
def critic(ctx, message, **kw):
from ouroboros.review_dispatch import invoke_review_paid_stamp
invoke_review_paid_stamp(ctx._review_paid_stamp)
staged_paths = _git(root, "diff", "--cached", "--name-only").splitlines()
assert staged_paths == ["clean.txt", "new.txt"]
content = _git(root, "show", ":clean.txt")
critical = ctx.task_id == "first"
assert content == ("draft before criticism" if critical else "corrected after final criticism")
finding = {"item": "content", "severity": "critical", "verdict": "FAIL" if critical else "PASS",
"reason": "Correct the draft" if critical else "Corrected exact staged bytes verified"}
ctx._last_review_critical_findings = [finding] if critical else []
ctx._last_triad_raw_results = [{"slot_id": "critic", "status": "responded", "parsed": [finding],
"raw_text": finding["reason"], "operation_state": "settled"}]
scope = ScopeReviewResult(blocked=False, status="responded", critical_findings=[])
ctx._last_scope_raw_result = {"status": "responded", "critical_findings": []}
reviews.append({"task": ctx.task_id, "fingerprint": kw["review_binding_fingerprint"], "content": content})
return ("Draft needs correction" if critical else None), scope, "critical_findings" if critical else "", []
monkeypatch.setattr(git, "_run_review_preflight_tests", preflight)
monkeypatch.setattr(git, "_run_parallel_review", critic)
write_task_result(data, "first", "running")
capture_mutation_baseline(data, "first", [{"surface_type": "system_repo", "host_root": str(root)}])
(root / "clean.txt").write_text("draft before criticism\n", encoding="utf-8")
(root / "new.txt").write_text("retained companion\n", encoding="utf-8")
first = context("first")
blocked = git._repo_commit_push(first, "Correct draft", skip_advisory_review=True)
assert "Draft needs correction" in blocked
assert _git(root, "rev-parse", "HEAD") == before and not publications
(root / "clean.txt").write_text("corrected after final criticism\n", encoding="utf-8")
exhausted = git._repo_commit_push(first, "Correct draft", skip_advisory_review=True)
assert "REVIEW_CYCLES_EXHAUSTED" in exhausted
assert len(reviews) == 1 and _git(root, "rev-parse", "HEAD") == before
record_terminal_mutation_candidates(data, "first")
write_task_result(data, "first", "completed", reason_code="review_cycles_exhausted",
outcome_axes={"execution": {"status": "ok"}, "objective": {"status": "fail",
"source": "task_acceptance_review", "reason": "review_cycles_exhausted",
"outcome_tier": "blocked_with_evidence"}, "review": {"status": "fail"}})
first_result = load_task_result(data, "first")
assert {path.stem for path in (data / "task_results").glob("*.json")} == {"first"}
# Independent admission explicitly selects the retained predecessor. It does
# not copy its paid wallet or critic authority, and does not reset first.
task = _promote_retained_correction(root, data, monkeypatch)
second_id = task["id"]
assert second_id != "first" and task["root_task_id"] == second_id
agent = SimpleNamespace(env=SimpleNamespace(repo_dir=root, drive_root=data, budget_drive_root=str(data)))
assert not validate_task_authority_sources(agent.env, task)
write_task_result(data, second_id, "running")
OuroborosAgent._capture_mutation_baseline(agent, task, {})
second = context(second_id)
candidates = attributed_git_candidates(data, second_id, root)
assert candidates["candidates"] == ["clean.txt", "new.txt"]
assert candidates["excluded_preexisting_dirty"] == ["dirty.txt"]
completed = git._repo_commit_push(second, "Commit retained correction after fresh review", skip_advisory_review=True)
assert _git(root, "rev-parse", "HEAD") != before, completed
assert _git(root, "show", "HEAD:clean.txt") == "corrected after final criticism"
assert _git(root, "show", "HEAD:new.txt") == "retained companion"
assert _git(root, "show", "HEAD:dirty.txt") == "base"
assert (root / "dirty.txt").read_text(encoding="utf-8") == "unrelated owner WIP\n"
assert _git(root, "diff", "--name-only") == "dirty.txt"
assert [row["task"] for row in reviews] == ["first", second_id]
assert reviews[0]["fingerprint"] != reviews[1]["fingerprint"]
attempts = load_state(data).attempts
assert {task_id: sum(row.paid for row in attempts if row.root_task_id == task_id)
for task_id in ("first", second_id)} == {"first": 1, second_id: 1}
assert attempts[-1].status == "succeeded" and not attempts[-1].author_disposition
assert load_task_result(data, "first") == first_result
assert len(publications) == 1 and all(row["exit"] == 0 for row in checks)