Files
coordinator/tests/test_orchestrator.py
ClaudeBotandClaude Opus 4.7 77a4ebec8d Judges renamed to neutral A/B/C; Execution Context override applied to both meta-judge and judge payloads
- Rename JUDGE_NAMES from [Judge-GPT, Judge-Claude, Judge-Gemini] to
  [Judge A, Judge B, Judge C] everywhere (orchestrator + tests + ad-hoc
  A/B script). Removes the Claude-family naming anchor that was
  repeatedly pulling the panel toward Claude models even when Student
  plan blocked them — the slot is just "one of three judges", not
  "the Claude judge".

- Apply the A/B-test-winning headless override (EXECUTION_CONTEXT_BLOCK)
  to both _build_meta_judge_chat_payload and _build_judge_chat_payload.
  Phrases are verbatim from OpenAI GPT-5 Prompting Guide + Anthropic
  Claude headless docs. Tells agents there's no human to answer
  clarifying questions; to commit to best-default interpretation and
  document the assumption.

End-to-end pipeline run on WYL-77 (2026-04-20) confirmed:
  - Meta-judge produced rubric without clarifying question (override
    working on ambiguous Tierra prompt)
  - 3 judges ran in parallel without SQLite contention (XDG fix held)
  - Consensus math gracefully excluded Judge B's malformed YAML
  - Verdict REJECT avg=1.00 on prose-only worker delivery
  - Retrigger fired; worker honored anchor + blocked path

Known blockers surfaced (not fixed in this commit):
  - Shared HOME allows cross-daemon workdir snooping (Judge B read AI
    Engineer's task workspace directly). User flagged as feature for
    now; revisit if it causes drift.
  - gpt-5.4-mini (Judge B) produced malformed YAML on this run; n=1,
    can't distinguish chance vs consistent inability — need multi-run
    baseline to decide.
  - REJECT on "no commit URL" conflates missing-delivery with
    bad-work; pipeline signal is correct but reasoning upstream of
    judges is unclear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 02:42:46 +02:00

866 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for Pipeline A — clean-context judge pipeline via chat-tasks + shared mount.
Covers:
- Parsing (YAML extraction, rubric, judge reports, consensus math) — unchanged
semantics from the prior pipeline.
- Chat-task invocation payloads (what meta-judge and each judge receive).
- Shared-mount report collection.
- C1C6 contamination-fix invariants (see plan).
- Retrigger + verdict summary flow (still posts on the issue thread).
"""
from __future__ import annotations
import logging
import subprocess
from pathlib import Path
from typing import Any
import pytest
import yaml
from coordinator.queue import DebateQueue, Round
from coordinator.orchestrator import (
ACCEPT_MIN_SCORE,
CONSENSUS_CRITERION_THRESHOLD,
CONSENSUS_OVERALL_THRESHOLD,
JUDGE_NAMES,
MAX_DEBATE_ROUNDS,
META_JUDGE_NAME,
REWORK_INSTRUCTIONS,
_advance_awaiting_debate,
_advance_awaiting_judges,
_advance_awaiting_rubric,
_advance_round,
_apply_verdict,
_build_coordinator_note_no_agent,
_build_debate_chat_payload,
_build_judge_chat_payload,
_build_meta_judge_chat_payload,
_build_retrigger_comment,
_check_consensus,
_criterion_scores,
_extract_yaml,
_find_commit_url,
_format_verdict_summary,
_materialize_artifact,
_overall_score,
_parse_judge_report,
_parse_rubric,
_poll_assistant_reply,
_post_rejection_retrigger,
_read_reports_from_disk,
_report_path,
_start_round,
_utcnow,
)
_logger = logging.getLogger("test.orchestrator")
# ---------------------------------------------------------------------------
# Fakes
# ---------------------------------------------------------------------------
class FakeConfig:
"""Mirrors coordinator.config.Config for test use (not frozen)."""
def __init__(self, rounds_root: Path):
self.server_url = "http://x"
self.workspace_id = "wid"
self.token = "tok"
self.poll_interval_s = 30
self.round_timeout_s = 600
self.max_concurrent_rounds = 3
self.rounds_root = rounds_root
self.gitea_base_url = ""
self.git_username = ""
self.git_token = ""
class FakeClient:
def __init__(self) -> None:
self.issue: dict[str, Any] = {
"id": "issue-1",
"title": "Do the thing",
"description": "Please do the thing clearly.",
"status": "in_review",
"assignee_type": None,
"assignee_id": None,
}
self.comments: list[dict[str, Any]] = []
self.posted_comments: list[str] = []
self.agents: list[dict[str, Any]] = [
{"name": META_JUDGE_NAME, "id": "agent-meta"},
{"name": "Judge A", "id": "agent-gpt"},
{"name": "Judge B", "id": "agent-claude"},
{"name": "Judge C", "id": "agent-gemini"},
]
# Chat state
self.chat_sessions: dict[str, dict[str, Any]] = {} # sid -> {agent_id, messages[]}
self._next_sid = 1
self._next_msg_id = 1
self._next_comment_id = 1000
# Test-controlled script for assistant replies: {session_id: content or None}
self.assistant_replies: dict[str, str] = {}
# ---- issue / comment API ------------------------------------------------
def get_issue(self, issue_id: str) -> dict[str, Any]:
return dict(self.issue)
def update_issue_status(self, issue_id: str, status: str) -> dict[str, Any]:
self.issue["status"] = status
return dict(self.issue)
def list_comments(self, issue_id: str) -> list[dict[str, Any]]:
return list(self.comments)
def post_comment(self, issue_id: str, content: str) -> dict[str, Any]:
self._next_comment_id += 1
cid = f"c-{self._next_comment_id}"
self.posted_comments.append(content)
c = {
"id": cid, "content": content, "author_id": "coord",
"created_at": _utcnow(),
}
self.comments.append(c)
return c
def list_agents(self) -> list[dict[str, Any]]:
return list(self.agents)
def find_agents_by_name(self, names: Any) -> dict[str, str]:
wanted = set(names)
return {a["name"]: a["id"] for a in self.agents if a["name"] in wanted}
def get_agent_name(self, agent_id: str) -> str | None:
for a in self.agents:
if a["id"] == agent_id:
return a["name"]
return None
# ---- chat API -----------------------------------------------------------
def create_chat_session(self, agent_id: str, title: str = "") -> str:
sid = f"s-{self._next_sid}"
self._next_sid += 1
self.chat_sessions[sid] = {"agent_id": agent_id, "title": title, "messages": []}
return sid
def post_chat_message(self, session_id: str, content: str) -> str:
mid = f"m-{self._next_msg_id}"
self._next_msg_id += 1
session = self.chat_sessions[session_id]
session["messages"].append({
"id": mid, "role": "user", "content": content, "created_at": _utcnow(),
})
return mid
def list_chat_messages(self, session_id: str) -> list[dict[str, Any]]:
session = self.chat_sessions.get(session_id)
if not session:
return []
messages = list(session["messages"])
# If the test has pre-scripted an assistant reply for this session, append it
# (but only once — subsequent calls just see it).
reply = self.assistant_replies.get(session_id)
if reply is not None and not any(m["role"] == "assistant" for m in messages):
mid = f"m-{self._next_msg_id}"
self._next_msg_id += 1
assistant_msg = {
"id": mid, "role": "assistant", "content": reply, "created_at": _utcnow(),
}
session["messages"].append(assistant_msg)
messages.append(assistant_msg)
return messages
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def tmp_rounds(tmp_path: Path) -> Path:
d = tmp_path / "rounds"
d.mkdir()
return d
@pytest.fixture
def cfg(tmp_rounds: Path) -> FakeConfig:
return FakeConfig(tmp_rounds)
@pytest.fixture
def queue(tmp_path: Path) -> DebateQueue:
return DebateQueue(path=tmp_path / "queue.json")
@pytest.fixture
def client() -> FakeClient:
return FakeClient()
# ===========================================================================
# Parser tests (medium-independent — unchanged semantics)
# ===========================================================================
class TestYAMLExtract:
def test_fenced_yaml_block(self) -> None:
content = "preamble\n```yaml\nfoo: 1\nbar: 2\n```\ntrailing"
assert _extract_yaml(content) == "foo: 1\nbar: 2"
def test_html_entities_unescaped(self) -> None:
content = "```yaml\nfoo: &#34;hello&#34;\n```"
assert _extract_yaml(content) == 'foo: "hello"'
def test_backslash_backtick_repair(self) -> None:
content = "```yaml\nevidence: \"see \\`foo.py\\`\"\n```"
assert _extract_yaml(content) == 'evidence: "see `foo.py`"'
def test_no_fence_returns_stripped_content(self) -> None:
assert _extract_yaml(" foo: 1\n ") == "foo: 1"
class TestParseRubric:
def test_wrapped_form(self) -> None:
r = _parse_rubric("```yaml\nevaluation_specification:\n checklist:\n - q: yes\n```")
assert r is not None and "checklist" in r
def test_flat_form(self) -> None:
r = _parse_rubric("```yaml\nchecklist:\n - q: yes\nrubric_dimensions: []\n```")
assert r is not None and "checklist" in r
def test_missing_both_keys_rejected(self) -> None:
assert _parse_rubric("```yaml\nfoo: 1\n```") is None
def test_malformed_yaml_rejected(self) -> None:
assert _parse_rubric("```yaml\n{not valid\n```") is None
def test_non_dict_rejected(self) -> None:
assert _parse_rubric("```yaml\n- 1\n- 2\n```") is None
class TestParseJudgeReport:
def test_with_final_score(self) -> None:
r = _parse_judge_report("```yaml\nscore_calculation:\n final_score: 4.0\n```")
assert r is not None and r["score_calculation"]["final_score"] == 4.0
def test_with_rubric_scores(self) -> None:
r = _parse_judge_report("```yaml\nrubric_scores:\n - name: a\n score: 3\n```")
assert r is not None and r["rubric_scores"][0]["score"] == 3
def test_empty_report_rejected(self) -> None:
assert _parse_judge_report("```yaml\nfoo: 1\n```") is None
# ===========================================================================
# Score and consensus math
# ===========================================================================
class TestOverallScore:
def test_final_score_preferred(self) -> None:
r = {"score_calculation": {"final_score": 4.2}, "rubric_scores": [{"score": 1}]}
assert _overall_score(r) == 4.2
def test_weighted_average_fallback(self) -> None:
r = {
"rubric_scores": [
{"name": "a", "score": 4.0, "weight": 2},
{"name": "b", "score": 2.0, "weight": 1},
]
}
# (4*2 + 2*1) / 3 = 10/3
assert _overall_score(r) == pytest.approx(10.0 / 3.0)
def test_plain_average_when_no_weights(self) -> None:
r = {"rubric_scores": [{"score": 4.0}, {"score": 2.0}]}
assert _overall_score(r) == 3.0
def test_returns_none_on_empty(self) -> None:
assert _overall_score({}) is None
class TestCheckConsensus:
def _mk(self, overall: float, crits: dict[str, float]) -> dict[str, Any]:
return {
"score_calculation": {"final_score": overall},
"rubric_scores": [{"name": k, "score": v} for k, v in crits.items()],
}
def test_converge_and_accept(self) -> None:
reports = {
"Judge A": self._mk(4.0, {"clarity": 4, "depth": 4}),
"Judge B": self._mk(4.2, {"clarity": 4, "depth": 4}),
"Judge C": self._mk(4.1, {"clarity": 4, "depth": 4}),
}
converged, verdict, avg = _check_consensus(reports)
assert converged is True
assert verdict == "ACCEPT"
assert avg == pytest.approx((4.0 + 4.2 + 4.1) / 3)
def test_converge_and_reject(self) -> None:
reports = {
"Judge A": self._mk(2.0, {"c": 2}),
"Judge B": self._mk(2.1, {"c": 2}),
"Judge C": self._mk(2.2, {"c": 2}),
}
converged, verdict, avg = _check_consensus(reports)
assert converged and verdict == "REJECT"
def test_overall_spread_blocks_convergence(self) -> None:
reports = {
"Judge A": self._mk(3.0, {"c": 3}),
"Judge B": self._mk(4.0, {"c": 3}),
"Judge C": self._mk(3.5, {"c": 3}),
}
converged, verdict, _avg = _check_consensus(reports)
assert not converged and verdict is None
def test_criterion_spread_blocks_convergence(self) -> None:
reports = {
"Judge A": self._mk(4.0, {"c": 3}),
"Judge B": self._mk(4.1, {"c": 5}), # criterion delta 2 > 1.0
"Judge C": self._mk(4.2, {"c": 4}),
}
converged, verdict, _avg = _check_consensus(reports)
assert not converged and verdict is None
def test_converge_exactly_at_threshold(self) -> None:
reports = {
"Judge A": self._mk(3.0, {"c": 3}),
"Judge B": self._mk(3.5, {"c": 4}), # exactly 0.5 spread
"Judge C": self._mk(3.25, {"c": 3.5}),
}
converged, _v, _a = _check_consensus(reports)
assert converged is True
# ===========================================================================
# Chat-task payload builders — CEK-shape contamination checks (C1C6)
# ===========================================================================
class TestMetaJudgeChatPayload:
"""C2 fix: meta-judge chat contains NO commit link, NO delivery prose."""
def test_contains_task_and_artifact_type(self) -> None:
body = _build_meta_judge_chat_payload("Write a paper", "Produce a 10-page paper.")
assert "Write a paper" in body
assert "Produce a 10-page paper." in body
assert "## Artifact Type" in body
assert "code" in body
def test_no_commit_link(self) -> None:
body = _build_meta_judge_chat_payload("t", "d")
assert "commit" not in body.lower()
assert "http" not in body
def test_no_judge_names(self) -> None:
# Meta-judge should not know which judges will evaluate downstream.
body = _build_meta_judge_chat_payload("t", "d")
for name in JUDGE_NAMES:
assert name not in body
def test_no_mention_syntax(self) -> None:
body = _build_meta_judge_chat_payload("t", "d")
assert "@" not in body # no @-mention
assert "mention://" not in body
class TestJudgeChatPayload:
"""C4 + C6 fix: judge chat reveals no peer identity, no worker prose."""
def _payload(self) -> str:
return _build_judge_chat_payload(
issue_title="Write a paper",
issue_description="Produce a 10-page paper.",
rubric_yaml="checklist:\n - q: yes",
artifact_dir="/mnt/rounds/rid/artifact",
diff_path="/mnt/rounds/rid/artifact.diff",
output_path="/mnt/rounds/rid/reports/Judge A.round-1.md",
)
def test_contains_paths_not_inlined(self) -> None:
p = self._payload()
assert "/mnt/rounds/rid/artifact" in p
assert "/mnt/rounds/rid/artifact.diff" in p
assert "/mnt/rounds/rid/reports/Judge A.round-1.md" in p
def test_contains_rubric_yaml_fenced(self) -> None:
p = self._payload()
assert "```yaml" in p
assert "checklist" in p
def test_no_peer_identity(self) -> None:
p = self._payload()
# payload builder doesn't receive peer names; check none leak
assert "Judge B" not in p
assert "Judge C" not in p
# It can mention "Judge A" via output path; that's the SELF identity
def test_no_other_agent_output_inlined(self) -> None:
# C1 + C5: no meta-judge reasoning prose, no other judge reports inlined
p = self._payload()
assert "meta-judge" not in p.lower()
assert "other judges" not in p.lower()
assert "prior report" not in p.lower()
class TestDebateChatPayload:
"""C5 fix: debate round passes PATHS to peer reports, not inlined content."""
def _payload(self) -> str:
return _build_debate_chat_payload(
issue_title="t", issue_description="d",
rubric_yaml="checklist: []",
artifact_dir="/mnt/rounds/rid/artifact",
diff_path="/mnt/rounds/rid/artifact.diff",
own_prior_path="/mnt/rounds/rid/reports/Judge A.round-1.md",
peer_prior_paths=[
"/mnt/rounds/rid/reports/Judge B.round-1.md",
"/mnt/rounds/rid/reports/Judge C.round-1.md",
],
output_path="/mnt/rounds/rid/reports/Judge A.round-2.md",
round_num=2,
)
def test_paths_present_contents_absent(self) -> None:
p = self._payload()
assert "Judge B.round-1.md" in p
assert "Judge C.round-1.md" in p
# Contents are NOT in the payload — only paths
# Sanity: peer score text that would only appear if contents were inlined
assert "final_score:" not in p
def test_contains_cek_anti_sycophancy_block_verbatim(self) -> None:
p = self._payload()
assert "Only revise if you find their evidence compelling" in p
assert "Defend your original scores if you still believe them" in p
assert "Quote specific evidence from the solution." in p
def test_round_num_in_body(self) -> None:
p = self._payload()
assert "Debate Round 2" in p or "Debate round 2" in p
# ===========================================================================
# _start_round + _advance_awaiting_rubric — chat-session flow
# ===========================================================================
def _make_round(queue: DebateQueue, issue_id: str = "issue-1") -> Round:
return queue.enqueue(issue_id, "WYL-1", "Do the thing")
class TestStartRound:
def test_opens_meta_judge_chat_with_task_only(self, cfg, queue, client) -> None:
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
# Exactly one chat session opened for meta-judge
assert len(client.chat_sessions) == 1
sid = next(iter(client.chat_sessions))
session = client.chat_sessions[sid]
assert session["agent_id"] == "agent-meta"
# The single user message is the meta-judge payload
msgs = session["messages"]
assert len(msgs) == 1
body = msgs[0]["content"]
assert "Do the thing" in body
assert "Please do the thing clearly." in body
# C2: no commit link
assert "commit" not in body.lower()
def test_phase_advances_to_awaiting_rubric(self, cfg, queue, client) -> None:
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
updated = queue.rounds[0]
assert updated.phase == "awaiting_rubric"
assert updated.meta_judge_chat_id
assert updated.meta_judge_user_msg_id
assert updated.shared_dir
# Shared dir exists on disk
assert Path(updated.shared_dir).is_dir()
def test_no_issue_comments_posted_on_start(self, cfg, queue, client) -> None:
"""Pipeline A: round start produces NO comment on the issue."""
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
assert client.posted_comments == []
def test_missing_meta_judge_agent_errors_out(self, cfg, queue, client) -> None:
client.agents = [a for a in client.agents if a["name"] != META_JUDGE_NAME]
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
assert queue.rounds[0].status == "error"
class TestAdvanceAwaitingRubric:
"""Meta-judge reply → rubric parsed → artifact materialized → 3 judge chats opened."""
def _prime(self, cfg, queue, client, monkeypatch) -> Round:
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
# Prevent real git clone — no commit URL yet
queue.rounds[0].commit_url = "" # materialize step will warn + proceed
return queue.rounds[0]
def test_opens_three_judge_chats_in_parallel(
self, cfg, queue, client, monkeypatch
) -> None:
r = self._prime(cfg, queue, client, monkeypatch)
# Script meta-judge's assistant reply
rubric = "```yaml\nchecklist:\n - q: first\nrubric_dimensions:\n - name: clarity\n weight: 1\n```"
client.assistant_replies[r.meta_judge_chat_id] = rubric
_advance_awaiting_rubric(r, client, queue, cfg, _logger)
updated = queue.rounds[0]
# 3 judge chats + 1 meta-judge chat = 4 sessions total
assert len(client.chat_sessions) == 4
assert set(updated.judge_chat_ids.keys()) == set(JUDGE_NAMES)
assert set(updated.judge_user_msg_ids.keys()) == set(JUDGE_NAMES)
def test_rubric_written_to_shared_dir(
self, cfg, queue, client, monkeypatch
) -> None:
r = self._prime(cfg, queue, client, monkeypatch)
client.assistant_replies[r.meta_judge_chat_id] = (
"```yaml\nchecklist:\n - q: x\n```"
)
_advance_awaiting_rubric(r, client, queue, cfg, _logger)
rubric_file = Path(queue.rounds[0].shared_dir) / "rubric.yaml"
assert rubric_file.exists()
assert "checklist" in rubric_file.read_text()
def test_judge_chat_payload_contains_artifact_paths(
self, cfg, queue, client, monkeypatch
) -> None:
r = self._prime(cfg, queue, client, monkeypatch)
client.assistant_replies[r.meta_judge_chat_id] = (
"```yaml\nchecklist:\n - q: x\n```"
)
_advance_awaiting_rubric(r, client, queue, cfg, _logger)
updated = queue.rounds[0]
for name in JUDGE_NAMES:
sid = updated.judge_chat_ids[name]
body = client.chat_sessions[sid]["messages"][0]["content"]
assert str(Path(updated.shared_dir) / "artifact") in body
assert str(Path(updated.shared_dir) / "artifact.diff") in body
expected_output = str(_report_path(cfg, r.round_id, name, 1))
assert expected_output in body
def test_malformed_rubric_errors_round(
self, cfg, queue, client, monkeypatch
) -> None:
r = self._prime(cfg, queue, client, monkeypatch)
client.assistant_replies[r.meta_judge_chat_id] = "I don't know what YAML is."
_advance_awaiting_rubric(r, client, queue, cfg, _logger)
assert queue.rounds[0].status == "error"
assert queue.rounds[0].phase == "error"
def test_no_issue_comments_during_rubric_phase(
self, cfg, queue, client, monkeypatch
) -> None:
r = self._prime(cfg, queue, client, monkeypatch)
client.assistant_replies[r.meta_judge_chat_id] = (
"```yaml\nchecklist:\n - q: x\n```"
)
_advance_awaiting_rubric(r, client, queue, cfg, _logger)
# C1: no coordinator comments posted on the issue during the whole initial phase
assert client.posted_comments == []
# ===========================================================================
# _read_reports_from_disk — filesystem-based report collection
# ===========================================================================
class TestReadReportsFromDisk:
def _write_report(
self, cfg, round_id: str, judge: str, round_num: int, content: str
) -> None:
p = _report_path(cfg, round_id, judge, round_num)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
def test_reads_all_three_when_present(self, cfg: FakeConfig) -> None:
rid = "r1"
for name in JUDGE_NAMES:
self._write_report(cfg, rid, name, 1, (
f"```yaml\nscore_calculation:\n final_score: 3.5\n```"
))
raw, parsed = _read_reports_from_disk(cfg, rid, 1)
assert set(raw.keys()) == set(JUDGE_NAMES)
assert set(parsed.keys()) == set(JUDGE_NAMES)
def test_partial_reports_returned(self, cfg: FakeConfig) -> None:
rid = "r1"
self._write_report(cfg, rid, "Judge A", 1, (
"```yaml\nscore_calculation:\n final_score: 3.0\n```"
))
raw, parsed = _read_reports_from_disk(cfg, rid, 1)
assert set(raw.keys()) == {"Judge A"}
assert set(parsed.keys()) == {"Judge A"}
def test_unparseable_report_in_raw_not_parsed(self, cfg: FakeConfig) -> None:
rid = "r1"
self._write_report(cfg, rid, "Judge A", 1, "garbage, not yaml")
raw, parsed = _read_reports_from_disk(cfg, rid, 1)
assert "Judge A" in raw
assert "Judge A" not in parsed
def test_round_num_isolation(self, cfg: FakeConfig) -> None:
rid = "r1"
self._write_report(cfg, rid, "Judge A", 1, "```yaml\nscore_calculation:\n final_score: 3.0\n```")
self._write_report(cfg, rid, "Judge A", 2, "```yaml\nscore_calculation:\n final_score: 4.0\n```")
_raw1, p1 = _read_reports_from_disk(cfg, rid, 1)
_raw2, p2 = _read_reports_from_disk(cfg, rid, 2)
assert p1["Judge A"]["score_calculation"]["final_score"] == 3.0
assert p2["Judge A"]["score_calculation"]["final_score"] == 4.0
# ===========================================================================
# _advance_awaiting_judges — consensus → verdict; no-consensus → debate
# ===========================================================================
class TestAdvanceAwaitingJudges:
def _setup_round_in_awaiting_judges(self, cfg, queue, client) -> Round:
r = _make_round(queue)
_start_round(r, client, queue, cfg, _logger)
# Script meta-judge reply to advance past awaiting_rubric
meta_sid = queue.rounds[0].meta_judge_chat_id
client.assistant_replies[meta_sid] = "```yaml\nchecklist:\n - q: x\n```"
_advance_awaiting_rubric(queue.rounds[0], client, queue, cfg, _logger)
return queue.rounds[0]
def _write_all_reports(self, cfg, rid, round_num, overalls: dict[str, float]) -> None:
for name, score in overalls.items():
p = _report_path(cfg, rid, name, round_num)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(
f"```yaml\nscore_calculation:\n final_score: {score}\n"
f"rubric_scores:\n - name: c\n score: {score}\n```"
)
def test_consensus_accept_marks_issue_done(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
self._write_all_reports(cfg, r.round_id, 1, {
"Judge A": 4.0, "Judge B": 4.1, "Judge C": 4.2,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
assert queue.rounds[0].phase == "accepted"
assert queue.rounds[0].status == "done"
assert client.issue["status"] == "done"
def test_consensus_reject_marks_issue_in_progress(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
# Set assignee so retrigger path fires
client.issue["assignee_type"] = "agent"
client.issue["assignee_id"] = "agent-worker"
client.agents.append({"name": "Worker", "id": "agent-worker"})
self._write_all_reports(cfg, r.round_id, 1, {
"Judge A": 2.0, "Judge B": 2.1, "Judge C": 2.2,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
assert queue.rounds[0].phase == "rejected"
assert queue.rounds[0].status == "done"
assert client.issue["status"] == "in_progress"
def test_reject_posts_retrigger_with_anchor(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
client.issue["assignee_type"] = "agent"
client.issue["assignee_id"] = "agent-worker"
client.agents.append({"name": "Worker", "id": "agent-worker"})
self._write_all_reports(cfg, r.round_id, 1, {
"Judge A": 2.0, "Judge B": 2.1, "Judge C": 2.2,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
# Verdict summary + retrigger = 2 posted comments
assert len(client.posted_comments) == 2
retrigger = client.posted_comments[1]
assert "ANCHOR" in retrigger
assert REWORK_INSTRUCTIONS in retrigger
assert "@Worker" in retrigger
def test_no_consensus_opens_debate_chats(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
self._write_all_reports(cfg, r.round_id, 1, {
"Judge A": 2.0, "Judge B": 3.5, "Judge C": 4.0,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
assert queue.rounds[0].phase == "awaiting_debate"
assert queue.rounds[0].debate_round == 1
# 1 meta + 3 phase-1 + 3 debate = 7 sessions
assert len(client.chat_sessions) == 7
def test_debate_chat_contains_peer_paths_not_content(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
self._write_all_reports(cfg, r.round_id, 1, {
"Judge A": 2.0, "Judge B": 3.5, "Judge C": 4.0,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
updated = queue.rounds[0]
for name in JUDGE_NAMES:
sid = updated.judge_chat_ids[name]
body = client.chat_sessions[sid]["messages"][0]["content"]
# C5: peer paths present, peer content NOT inlined
for other in JUDGE_NAMES:
if other != name:
assert str(_report_path(cfg, r.round_id, other, 1)) in body
# Sanity: no peer final_score value leaks into the prompt
assert "final_score: 3.5" not in body
assert "final_score: 4.0" not in body
def test_max_debate_rounds_errors_out(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
# Force debate_round to cap
queue.rounds[0].debate_round = MAX_DEBATE_ROUNDS
# Write non-converging reports at the expected round (cap+1 reports)
self._write_all_reports(cfg, r.round_id, MAX_DEBATE_ROUNDS + 1, {
"Judge A": 2.0, "Judge B": 3.5, "Judge C": 4.0,
})
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
assert queue.rounds[0].phase == "error"
assert queue.rounds[0].status == "error"
def test_all_unparseable_reports_errors_out(self, cfg, queue, client) -> None:
r = self._setup_round_in_awaiting_judges(cfg, queue, client)
for name in JUDGE_NAMES:
p = _report_path(cfg, r.round_id, name, 1)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("garbage")
_advance_awaiting_judges(queue.rounds[0], client, queue, cfg, _logger)
assert queue.rounds[0].phase == "error"
# ===========================================================================
# Retrigger corner cases
# ===========================================================================
class TestRetrigger:
def test_no_assignee_posts_non_mentioning_note(self, cfg, queue, client) -> None:
r = _make_round(queue)
client.issue["assignee_type"] = None
client.issue["assignee_id"] = None
_post_rejection_retrigger(r, client, client.issue, "VERDICT: REJECT", _logger)
assert len(client.posted_comments) == 1
assert "@" not in client.posted_comments[0]
assert "Manual follow-up" in client.posted_comments[0]
def test_human_assignee_posts_non_mentioning_note(self, cfg, queue, client) -> None:
r = _make_round(queue)
client.issue["assignee_type"] = "user"
client.issue["assignee_id"] = "user-123"
_post_rejection_retrigger(r, client, client.issue, "VERDICT: REJECT", _logger)
assert len(client.posted_comments) == 1
assert "Manual follow-up" in client.posted_comments[0]
def test_assignee_not_found_posts_non_mentioning_note(self, cfg, queue, client) -> None:
r = _make_round(queue)
client.issue["assignee_type"] = "agent"
client.issue["assignee_id"] = "ghost"
_post_rejection_retrigger(r, client, client.issue, "VERDICT: REJECT", _logger)
assert len(client.posted_comments) == 1
assert "not found" in client.posted_comments[0]
# ===========================================================================
# _materialize_artifact — ephemeral token, no credential persistence
# ===========================================================================
class TestMaterializeArtifact:
def test_empty_commit_url_returns_false(self, cfg, tmp_path) -> None:
ok = _materialize_artifact(cfg, "rid", "", _logger)
assert ok is False
def test_unparseable_commit_url_returns_false(self, cfg, tmp_path) -> None:
ok = _materialize_artifact(cfg, "rid", "https://example.com/blurg", _logger)
assert ok is False
def test_clone_args_use_credential_helper_none(self, cfg, monkeypatch) -> None:
"""Verify git is invoked with `-c credential.helper=` so the token in the URL
is not persisted into the repo's config."""
calls: list[list[str]] = []
def fake_run(cmd, **kwargs):
calls.append(list(cmd))
# Simulate the clone creating the target dir
if "clone" in cmd:
idx = cmd.index("clone")
target = cmd[-1]
Path(target).mkdir(parents=True, exist_ok=True)
(Path(target) / ".git").mkdir(exist_ok=True)
class R:
returncode = 0
stdout = b"diff content"
stderr = b""
return R()
monkeypatch.setattr(subprocess, "run", fake_run)
cfg.git_username = "u"
cfg.git_token = "t"
ok = _materialize_artifact(
cfg, "rid",
"https://git.example.com/org/repo/commit/deadbeef",
_logger,
)
assert ok is True
# The clone call must include credential.helper override
clone_call = next(c for c in calls if "clone" in c)
assert "credential.helper=" in clone_call
# The URL in argv contains the token (ephemeral, argv only)
assert any("u:t@" in arg for arg in clone_call)
# Verify remote set-url was called with a clean URL (no token)
set_url = next(c for c in calls if "set-url" in c)
assert not any("u:t@" in arg for arg in set_url)
# ===========================================================================
# Verdict summary formatting
# ===========================================================================
class TestFormatVerdictSummary:
def test_includes_verdict_and_avg(self) -> None:
reports = {
"Judge A": {"score_calculation": {"final_score": 4.0}},
"Judge B": {"score_calculation": {"final_score": 4.2}},
"Judge C": {"score_calculation": {"final_score": 4.1}},
}
s = _format_verdict_summary("ACCEPT", 4.1, reports)
assert "VERDICT: ACCEPT" in s
assert "4.10" in s
assert "Judge A: 4.00" in s
assert "Judge B: 4.20" in s
assert "Judge C: 4.10" in s
def test_missing_judge_shown_as_no_score(self) -> None:
s = _format_verdict_summary("ACCEPT", 4.0, {"Judge A": {"score_calculation": {"final_score": 4.0}}})
assert "Judge B: (no score)" in s
# ===========================================================================
# _find_commit_url (still used to find worker's commit in thread)
# ===========================================================================
class TestFindCommitUrl:
def test_finds_commit_url_newest_first(self) -> None:
comments = [
{"content": "no url here"},
{"content": "https://git.example.com/a/b/commit/aaaa1111"},
{"content": "later https://git.example.com/a/b/commit/bbbb2222"},
]
url = _find_commit_url(comments)
assert url == "https://git.example.com/a/b/commit/bbbb2222"
def test_empty_when_absent(self) -> None:
assert _find_commit_url([{"content": "no links"}]) == ""