Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions tests/test_bounded_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,58 @@ def test_villani_task_reports_completed_when_done(tmp_path: Path) -> None:

assert result["execution"]["completed"] is True
assert result["execution"]["terminated_reason"] == "completed"


def test_completed_budget_does_not_bypass_finalization_gate(tmp_path: Path) -> None:
runner = _runner(
tmp_path,
[
{"role": "assistant", "content": [{"type": "text", "text": "I'll update x.py to fix this."}]},
{"role": "assistant", "content": [{"type": "text", "text": "done"}]},
],
)
budget = ExecutionBudget(max_turns=20, max_tool_calls=20, max_seconds=100.0, max_no_edit_turns=20, max_reconsecutive_recon_turns=20)

result = runner.run("implement fix", execution_budget=budget)

nudges = [
block.get("text", "")
for msg in result["messages"]
if msg.get("role") == "user"
for block in msg.get("content", [])
if isinstance(block, dict) and block.get("type") == "text"
]
assert any("no file was modified" in text for text in nudges)
assert result["execution"]["terminated_reason"] == "completed"


def test_failed_bash_verification_uses_exit_code_for_followup_gate(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr("villani_code.state_runtime.run_verification", lambda _runner, _trigger="edit": "<verification>status=pass</verification>")
runner = _runner(
tmp_path,
[
{"role": "assistant", "content": [{"type": "tool_use", "id": "1", "name": "Bash", "input": {"command": "python -c \"import sys; sys.exit(1)\""}}]},
{"role": "assistant", "content": [{"type": "text", "text": "done"}]},
{"role": "assistant", "content": [{"type": "text", "text": "done"}]},
],
)
monkeypatch.setattr(
runner,
"_execute_tool_with_policy",
lambda tool_name, _tool_input, _tool_use_id, _msg_index: {
"content": "simulated bash run",
"is_error": False,
"exit_code": 1 if tool_name == "Bash" else 0,
},
)
budget = ExecutionBudget(max_turns=20, max_tool_calls=20, max_seconds=100.0, max_no_edit_turns=20, max_reconsecutive_recon_turns=20)

result = runner.run("implement fix", execution_budget=budget)
nudges = [
block.get("text", "")
for msg in result["messages"]
if msg.get("role") == "user"
for block in msg.get("content", [])
if isinstance(block, dict) and block.get("type") == "text"
]
assert any("last verification failed" in text for text in nudges)
171 changes: 171 additions & 0 deletions tests/test_final_answer_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from villani_code.state import final_answer_block_reason


def test_concrete_edit_claim_without_edits_blocks_once() -> None:
nudge = {}
reason = final_answer_block_reason(
instruction="implement fix",
final_text="I'll update x.py to handle this case.",
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state=nudge,
)
assert reason is not None
reason2 = final_answer_block_reason(
instruction="implement fix",
final_text="I'll update x.py to handle this case.",
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state=nudge,
)
assert reason2 is None


def test_vague_suggestion_does_not_block() -> None:
reason = final_answer_block_reason(
instruction="implement fix",
final_text="One possible fix would be to update parser.py.",
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert reason is None


def test_new_concrete_edit_phrases_detected() -> None:
phrases = [
"The fix is straightforward: register it.",
"Found it. The markdown_handler is imported but never added to the REGISTRY dictionary. The fix is straightforward: register it.",
"The fix is simple: add it to the registry.",
"Need to register it.",
"Register it in the registry.",
"Add it to the registry.",
"Wire it into the registry.",
"Hook it up in the dispatch table.",
"Include it in the mapping.",
"Fix it by registering the handler.",
]
for phrase in phrases:
reason = final_answer_block_reason(
instruction="implement fix",
final_text=phrase,
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert reason is not None


def test_vague_add_suggestions_still_do_not_trigger() -> None:
for phrase in [
"One possible fix would be to register it.",
"You could add it to the registry.",
"I recommend registering it.",
"It might need to be added.",
"Maybe wire it into the registry.",
"This looks like a registry issue.",
"No code changes are needed.",
]:
reason = final_answer_block_reason(
instruction="implement fix",
final_text=phrase,
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert reason is None


def test_plan_only_task_does_not_block() -> None:
reason = final_answer_block_reason(
instruction="Plan only, do not implement.",
final_text="I'll update parser.py",
has_any_edit=False,
changed_files=[],
known_verification_command=True,
verification_ran_after_last_edit=False,
last_verification_failed=True,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert reason is None


def test_changed_files_without_verification_blocks_when_command_known() -> None:
reason = final_answer_block_reason(
instruction="implement fix",
final_text="Done",
has_any_edit=True,
changed_files=["src/a.py"],
known_verification_command=True,
verification_ran_after_last_edit=False,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert "have not verified" in str(reason)


def test_changed_files_without_known_verification_does_not_block() -> None:
reason = final_answer_block_reason(
instruction="implement fix",
final_text="Done",
has_any_edit=True,
changed_files=["src/a.py"],
known_verification_command=False,
verification_ran_after_last_edit=False,
last_verification_failed=False,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert reason is None


def test_failed_verification_requires_followup_action() -> None:
reason = final_answer_block_reason(
instruction="implement fix",
final_text="Done",
has_any_edit=True,
changed_files=["src/a.py"],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=True,
had_corrective_action_after_last_failure=False,
nudge_state={},
)
assert "last verification failed" in str(reason)


def test_failed_verification_with_followup_does_not_block() -> None:
reason = final_answer_block_reason(
instruction="implement fix",
final_text="Done",
has_any_edit=True,
changed_files=["src/a.py"],
known_verification_command=True,
verification_ran_after_last_edit=True,
last_verification_failed=True,
had_corrective_action_after_last_failure=True,
nudge_state={},
)
assert reason is None
35 changes: 34 additions & 1 deletion tests/test_state_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ def test_repeated_stale_verification_returns_compact_block(tmp_path: Path, monke
_seed_repo(tmp_path)
runner = Runner(client=DummyClient(), repo=tmp_path, model="m", stream=False, small_model=True)
runner._verification_baseline_changed = set()
monkeypatch.setattr(state_runtime, "git_changed_files", lambda _repo: [])
(tmp_path / "tests").mkdir(exist_ok=True)
(tmp_path / "tests" / "test_x.py").write_text("def test_x():\n assert False\n", encoding="utf-8")
monkeypatch.setattr(state_runtime, "git_changed_files", lambda _repo: ["tests/test_x.py"])

first = runner._run_verification("edit")
second = runner._run_verification("edit")
Expand Down Expand Up @@ -238,6 +240,37 @@ def test_verification_detail_event_keeps_raw_trace(tmp_path: Path, monkeypatch:
assert "last_validation_summary:" in out


def test_failed_verification_summary_includes_visible_failure_lines(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from types import SimpleNamespace
from villani_code.autonomy import VerificationStatus

_seed_repo(tmp_path)
runner = Runner(client=DummyClient(), repo=tmp_path, model="m", stream=False, small_model=True)
runner._verification_baseline_changed = set()
(tmp_path / "tests").mkdir(exist_ok=True)
(tmp_path / "tests" / "test_x.py").write_text("def test_x():\n assert False\n", encoding="utf-8")
monkeypatch.setattr(state_runtime, "git_changed_files", lambda _repo: ["tests/test_x.py"])

def fake_run(_cmd, **_kwargs):
return SimpleNamespace(
returncode=1,
stdout="E AssertionError: expected 2, got 3\nFile \"src/a.py\", line 10\nassert x == y",
stderr="",
)

monkeypatch.setattr(state_runtime.subprocess, "run", fake_run)
runner._verification_engine.verify = lambda *args, **kwargs: SimpleNamespace(
status=VerificationStatus.FAIL,
confidence_score=0.1,
findings=[],
summary="failed",
)
out = runner._run_verification("edit")
assert "AssertionError" in out
assert "expected 2, got 3" in out
assert "Treat visible test assertions as authoritative evidence." in out


def test_repeated_validation_updates_compact_state_without_repeated_prose(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
Loading
Loading