Skip to content

Commit bcf8a39

Browse files
solomonneasclaudecursoragent
authored
test(run): add deliberation invariant regression tests (#455)
Lock in grounded scope diversity, challenger ordering, minority-report preservation, and default-run/outcome isolation for #442/#452. Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 319a6b3 commit bcf8a39

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

tests/test_deliberation.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def fake_run_agent(cli_ref, prompt, **kwargs):
6464
assert not (output_dir / "deliberation.json").exists()
6565
plan = json.loads((output_dir / "plan.json").read_text())
6666
assert plan.get("mode") is None
67+
run_meta = json.loads((output_dir / "run.json").read_text())
68+
assert "deliberation" not in run_meta
6769

6870

6971
def test_cli_passes_deliberation_flag_to_aboyeur(tmp_path, monkeypatch):
@@ -110,6 +112,55 @@ def test_mark_duplicate_scopes_flags_later_entries():
110112
assert marked[2].status == "valid"
111113

112114

115+
def test_perspective_prompts_embed_distinct_recorded_scopes(monkeypatch, tmp_path):
116+
scopes = [
117+
_scope("graphtrail-context", "migration"),
118+
_scope("graphtrail-callers", "migrate.run"),
119+
]
120+
monkeypatch.setattr(deliberation, "derive_evidence_scopes", lambda cwd, task, count=3: scopes)
121+
plan = deliberation.build_plan(_roster(), "choose migration", cwd=tmp_path)
122+
roster = _roster()
123+
prompt_by_worker: dict[str, str] = {}
124+
for lens in plan.lenses:
125+
if lens.role != "perspective":
126+
continue
127+
prompt_by_worker[lens.worker] = deliberation.build_worker_prompt(
128+
roster.agents[lens.worker],
129+
Assignment(worker=lens.worker, task=lens.task, stage=lens.stage),
130+
plan=plan,
131+
prior_results=None,
132+
read_only=False,
133+
)
134+
assert len(prompt_by_worker) == 2
135+
assert len(set(prompt_by_worker.values())) == 2
136+
for lens in plan.lenses:
137+
if lens.role != "perspective":
138+
continue
139+
prompt = prompt_by_worker[lens.worker]
140+
assert lens.scope.text in prompt
141+
for other in plan.lenses:
142+
if other.role != "perspective" or other.worker == lens.worker or not other.scope.text:
143+
continue
144+
assert other.scope.text not in prompt
145+
146+
147+
def test_plan_payload_lists_distinct_evidence_scopes(monkeypatch, tmp_path):
148+
scopes = [
149+
_scope("graphtrail-context", "migration"),
150+
_scope("graphtrail-callers", "migrate.run"),
151+
]
152+
monkeypatch.setattr(deliberation, "derive_evidence_scopes", lambda cwd, task, count=3: scopes)
153+
plan = deliberation.build_plan(_roster(), "choose migration", cwd=tmp_path)
154+
payload = deliberation.plan_payload(plan)
155+
assert payload["mode"] == "deliberation"
156+
evidence_scopes = payload["evidence_scopes"]
157+
fingerprints = {(item["kind"], item["reference"]) for item in evidence_scopes}
158+
assert len(fingerprints) == len(evidence_scopes) == 2
159+
assigned_stage_one = {assignment.worker for assignment in plan.assignments if assignment.stage == 1}
160+
invalid_workers = {lens.worker for lens in plan.invalid_lenses}
161+
assert not assigned_stage_one & invalid_workers
162+
163+
113164
def test_build_plan_records_invalid_role_label_lens(monkeypatch, tmp_path):
114165
scopes = [
115166
_scope("graphtrail-context", "task-a"),
@@ -336,6 +387,129 @@ def test_assemble_artifact_marks_unavailable_challenger(monkeypatch, tmp_path):
336387
deliberation.validate_schema(artifact)
337388

338389

390+
def test_stage_one_dispatch_prompts_exclude_prior_perspectives(monkeypatch, tmp_path):
391+
scopes = [
392+
_scope("graphtrail-context", "migration"),
393+
_scope("graphtrail-callers", "migrate.run"),
394+
]
395+
monkeypatch.setattr(deliberation, "derive_evidence_scopes", lambda cwd, task, count=3: scopes)
396+
plan = deliberation.build_plan(_roster(), "choose migration strategy", cwd=tmp_path)
397+
captured: list[str] = []
398+
399+
def fake_run_agent(cli_ref, prompt, **kwargs):
400+
captured.append(prompt)
401+
return agents.AgentResult(
402+
text=json.dumps(
403+
{
404+
"position": "ok",
405+
"assumptions": [],
406+
"evidence_references": [],
407+
"agreements": [],
408+
"conflicts": [],
409+
}
410+
),
411+
ok=True,
412+
)
413+
414+
monkeypatch.setattr(agents, "run_agent", fake_run_agent)
415+
aboyeur.dispatch(
416+
list(plan.assignments),
417+
_roster(),
418+
build_prompt=deliberation.make_prompt_builder(plan),
419+
cwd=tmp_path,
420+
)
421+
stage_one = [prompt for prompt in captured if "Independent perspectives" not in prompt]
422+
stage_two = [prompt for prompt in captured if "Independent perspectives" in prompt]
423+
assert len(stage_one) == 2
424+
assert len(stage_two) == 1
425+
426+
427+
def test_synthesis_context_preserves_dissenting_perspective_positions():
428+
artifact = {
429+
"recommendation": "pick redis",
430+
"confidence": "medium",
431+
"minority_report": "sessions remain safer for rollback",
432+
"unresolved_conflicts": [],
433+
"perspectives": [
434+
{
435+
"worker": "coder",
436+
"position": "use redis",
437+
"evidence_scope": {"kind": "graphtrail-context"},
438+
},
439+
{
440+
"worker": "reviewer",
441+
"position": "use sessions",
442+
"evidence_scope": {"kind": "graphtrail-callers"},
443+
},
444+
],
445+
"challenger": {"worker": "analyst", "minority_report": "sessions remain safer for rollback"},
446+
}
447+
context = deliberation.synthesis_context(artifact)
448+
assert "use redis" in context
449+
assert "use sessions" in context
450+
assert "sessions remain safer for rollback" in context
451+
452+
453+
def test_deliberation_run_leaves_outcome_ledger_unchanged(monkeypatch, tmp_path):
454+
output_dir = tmp_path / "run"
455+
memory_dir = tmp_path / "memory" / "outcome"
456+
memory_dir.mkdir(parents=True)
457+
records_path = memory_dir / "records.jsonl"
458+
records_path.write_text('{"artifact_id":"existing"}\n', encoding="utf-8")
459+
scopes = [
460+
_scope("graphtrail-context", "migration"),
461+
_scope("graphtrail-callers", "migrate.run"),
462+
]
463+
monkeypatch.setattr(deliberation, "derive_evidence_scopes", lambda cwd, task, count=3: scopes)
464+
monkeypatch.setattr(
465+
aboyeur,
466+
"dispatch",
467+
lambda assignments, roster, **kwargs: [
468+
WorkerResult(
469+
worker=assignment.worker,
470+
task=assignment.task,
471+
text=json.dumps(
472+
{
473+
"position": f"{assignment.worker} position",
474+
"assumptions": [],
475+
"evidence_references": [],
476+
"agreements": [],
477+
"conflicts": [],
478+
}
479+
if assignment.stage == 1
480+
else {
481+
"attacks": [],
482+
"minority_report": "minority",
483+
"recommendation": "ship",
484+
"confidence": "medium",
485+
"unresolved_conflicts": [],
486+
"agreements": [],
487+
}
488+
),
489+
ok=True,
490+
)
491+
for assignment in assignments
492+
],
493+
)
494+
monkeypatch.setattr(
495+
aboyeur,
496+
"_run_orchestrator",
497+
lambda roster, prompt, **kwargs: agents.AgentResult(text="final", ok=True),
498+
)
499+
500+
rc = aboyeur.run(
501+
"choose migration strategy",
502+
_roster(),
503+
output_dir=output_dir,
504+
cwd=tmp_path,
505+
code_graph_enabled=False,
506+
route_enabled=False,
507+
deliberation=True,
508+
)
509+
assert rc == 0
510+
assert records_path.read_text(encoding="utf-8") == '{"artifact_id":"existing"}\n'
511+
512+
339513
def test_deliberation_dispatch_passes_prior_results_to_challenger(monkeypatch, tmp_path):
340514
scopes = [
341515
_scope("graphtrail-context", "migration"),

0 commit comments

Comments
 (0)