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
49 changes: 46 additions & 3 deletions tests/test_mission_state_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path

from villani_code.context_projection import build_model_context_packet
from villani_code.execution_memento import build_execution_memento
from villani_code.debug_bundle import create_debug_bundle
from villani_code.event_recorder import RuntimeEventRecorder
from villani_code.mission_state import (
Expand Down Expand Up @@ -36,11 +37,15 @@ def test_mission_state_roundtrip(tmp_path: Path) -> None:
status="active",
verified_facts=[VerifiedFact(kind="k", value="v", source="s")],
open_hypotheses=[OpenHypothesis(hypothesis_id="h1", statement="maybe", confidence=0.4, status="open")],
last_memento_path="x/y/execution_memento.json",
last_memento_turn_index=3,
)
save_mission_state(tmp_path, state)
loaded = load_mission_state(tmp_path, "m1")
assert loaded.verified_facts[0].value == "v"
assert loaded.open_hypotheses[0].hypothesis_id == "h1"
assert loaded.last_memento_path.endswith("execution_memento.json")
assert loaded.last_memento_turn_index == 3


def test_mission_directory_and_current_pointer(tmp_path: Path) -> None:
Expand Down Expand Up @@ -160,7 +165,7 @@ def test_projected_context_not_injected_in_benchmark_mode(tmp_path: Path) -> Non
runner._inject_projected_context(messages)
assert messages[-1]["content"][0]["text"] == "benchmark contract prompt"
assert not any(
"Mission context packet:" in str(block.get("text", ""))
"EXECUTION STATE" in str(block.get("text", ""))
for message in messages
for block in message.get("content", [])
if isinstance(block, dict)
Expand All @@ -170,6 +175,7 @@ def test_projected_context_not_injected_in_benchmark_mode(tmp_path: Path) -> Non
def test_projected_context_injected_into_latest_safe_user_turn(tmp_path: Path) -> None:
runner = Runner(client=DummyClient(), repo=tmp_path, model="x", stream=False, print_stream=False)
runner._ensure_mission("objective")
runner._task_contract = {"success_predicate": "pass", "no_go_paths": [".villani_code/"]}
messages = [
{"role": "user", "content": [{"type": "text", "text": "older prompt"}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1"}]},
Expand All @@ -179,19 +185,20 @@ def test_projected_context_injected_into_latest_safe_user_turn(tmp_path: Path) -
runner._inject_projected_context(messages)
assert messages[0]["content"][0]["text"] == "older prompt"
assert messages[2]["content"] == [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ok"}]
assert "Mission context packet:" in messages[3]["content"][0]["text"]
assert "EXECUTION STATE" in messages[3]["content"][0]["text"]


def test_projected_context_injected_into_string_user_content(tmp_path: Path) -> None:
runner = Runner(client=DummyClient(), repo=tmp_path, model="x", stream=False, print_stream=False)
runner._ensure_mission("objective")
runner._task_contract = {"success_predicate": "pass", "no_go_paths": [".villani_code/"]}
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "ack"}]},
{"role": "user", "content": "final prompt"},
]
runner._inject_projected_context(messages)
assert isinstance(messages[1]["content"], str)
assert "Mission context packet:" in messages[1]["content"]
assert "EXECUTION STATE" in messages[1]["content"]
assert messages[1]["content"].endswith("final prompt")


Expand All @@ -207,6 +214,42 @@ def test_projected_context_skips_when_only_tool_result_user_turn_exists(tmp_path
assert messages == original


def test_execution_memento_saved_to_mission_dir(tmp_path: Path) -> None:
runner = Runner(client=DummyClient(), repo=tmp_path, model="x", stream=False, print_stream=False)
runner._ensure_mission("objective")
runner._task_contract = {"success_predicate": "pass tests", "preferred_targets": ["src/a.py"], "no_go_paths": [".villani_code/"]}
runner._current_turn_index = 2
runner._refresh_execution_memento()
state = load_mission_state(tmp_path, runner._mission_id)
assert state.last_memento_path.endswith("execution_memento.json")
assert state.last_memento_turn_index == 2
memento_path = Path(state.last_memento_path)
assert memento_path.exists()
assert (memento_path.parent / "execution_memento.md").exists()


def test_execution_memento_excludes_runtime_artifact_paths(tmp_path: Path) -> None:
runner = Runner(client=DummyClient(), repo=tmp_path, model="x", stream=False, print_stream=False)
runner._ensure_mission("objective")
runner._mission_state.changed_files = ["src/app.py", ".villani_code/missions/m1/state.json"]
runner._mission_state.intended_targets = ["./.villani_code/sessions/last.json", "tests/test_app.py"]
memento = build_execution_memento(runner)
assert memento.changed_files == ["src/app.py"]
assert memento.in_scope_files == ["tests/test_app.py"]


def test_projected_context_falls_back_when_memento_required_fields_missing(tmp_path: Path) -> None:
runner = Runner(client=DummyClient(), repo=tmp_path, model="x", stream=False, print_stream=False)
runner._ensure_mission("objective")
runner._task_contract = {}
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "ack"}]},
{"role": "user", "content": "final prompt"},
]
runner._inject_projected_context(messages)
assert "Mission context packet:" in messages[1]["content"]


def test_new_mission_id_unique_for_rapid_calls() -> None:
from villani_code.mission_state import new_mission_id

Expand Down
20 changes: 19 additions & 1 deletion tests/test_session_compaction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from villani_code.context_budget import ContextBudget
from villani_code.state_runtime import validate_anthropic_tool_sequence
from villani_code.state_runtime import _trim_regular_runner_transcript_tail, validate_anthropic_tool_sequence
from villani_code.tui.controller import RunnerController


Expand Down Expand Up @@ -103,3 +103,21 @@ def test_follow_up_prompt_can_include_prior_tool_and_assistant_context() -> None
)
assert sent[-1]["role"] == "user"
assert "follow-up" in sent[-1]["content"][0]["text"]


def test_regular_runner_tail_trimming_preserves_tool_use_result_integrity() -> None:
messages = [
{"role": "system", "content": [{"type": "text", "text": "sys"}]},
{"role": "user", "content": [{"type": "text", "text": "start"}]},
{"role": "assistant", "content": [{"type": "text", "text": "a"}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "Read", "input": {"file_path": "a.py"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": "ok"}]},
{"role": "assistant", "content": [{"type": "text", "text": "b"}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "t2", "name": "Read", "input": {"file_path": "b.py"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t2", "content": "ok"}]},
{"role": "assistant", "content": [{"type": "text", "text": "tail"}]},
{"role": "user", "content": [{"type": "text", "text": "latest"}]},
]
trimmed = _trim_regular_runner_transcript_tail(messages, keep_units=2)
validate_anthropic_tool_sequence(trimmed)
assert len(trimmed) < len(messages)
76 changes: 76 additions & 0 deletions tests/test_state_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,82 @@ def save_inventory(self, _inventory):
) == 1


def test_prepare_messages_for_model_regular_injects_memento_without_duplication() -> None:
class _CtxGov:
def load_inventory(self):
return SimpleNamespace(task_id="")

def register_item(self, *args, **kwargs):
return None

def prune_for_budget(self, _inventory):
return None

def save_inventory(self, _inventory):
return None

class _Runner:
small_model = False
villani_mode = False
_context_budget = None
_execution_plan = SimpleNamespace(task_goal="t")
_context_governance = _CtxGov()

@staticmethod
def _inject_projected_context(messages):
state_runtime.prepend_text_to_latest_safe_user_message(messages, "EXECUTION STATE\nObjective: x")

messages = [{"role": "user", "content": [{"type": "text", "text": "Need context on runtime."}]}]
prepared_one = state_runtime.prepare_messages_for_model(_Runner(), messages)
prepared_two = state_runtime.prepare_messages_for_model(_Runner(), messages)
for prepared in (prepared_one, prepared_two):
assert sum(
1
for block in prepared[0]["content"]
if isinstance(block, dict) and "EXECUTION STATE" in str(block.get("text", ""))
) == 1


def test_prepare_messages_for_model_regular_trims_history_and_preserves_tool_sequence() -> None:
class _CtxGov:
def load_inventory(self):
return SimpleNamespace(task_id="")

def register_item(self, *args, **kwargs):
return None

def prune_for_budget(self, _inventory):
return None

def save_inventory(self, _inventory):
return None

runner = SimpleNamespace(
small_model=False,
villani_mode=False,
_context_budget=None,
_context_governance=_CtxGov(),
_execution_plan=SimpleNamespace(task_goal="t"),
_inject_projected_context=lambda _messages: None,
)
messages = [
{"role": "system", "content": [{"type": "text", "text": "sys"}]},
{"role": "user", "content": [{"type": "text", "text": "objective"}]},
{"role": "assistant", "content": [{"type": "text", "text": "old ack"}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "Read", "input": {"file_path": "a.py"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t1", "content": "ok"}]},
{"role": "assistant", "content": [{"type": "text", "text": "middle"}]},
{"role": "assistant", "content": [{"type": "tool_use", "id": "t2", "name": "Read", "input": {"file_path": "b.py"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "t2", "content": "ok"}]},
{"role": "assistant", "content": [{"type": "text", "text": "latest"}]},
{"role": "user", "content": [{"type": "text", "text": "next"}]},
]
prepared = state_runtime.prepare_messages_for_model(runner, messages)
state_runtime.validate_anthropic_tool_sequence(prepared)
assert len(prepared) < len(messages)
assert prepared[0]["role"] == "system"


def test_fail_first_localization_runs_without_strong_signal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_seed_repo(tmp_path)
events: list[dict] = []
Expand Down
Loading
Loading