"""Live planning turn: a real model must answer with a submit_plan or ask_human tool call, not prose. Plans with the test config's planner role. Planning escalates to a frontier model unconditionally (A12), and small local models do not hold the tool-call contract reliably, so that role must be one that does. """ import io from pathlib import Path import pytest from nisse_dev.canonical import UserMessage from nisse_dev.control import AskHuman, SubmitPlan from nisse_dev.llm import LiteLLMClient, RoleMap from nisse_dev.oplog import OpLog from nisse_dev.planner import LLMPlanningAgent from nisse_dev.state import StateStore pytestmark = pytest.mark.integration def test_live_planning_turn_returns_a_tool_action( tmp_path: Path, litellm: LiteLLMClient, live_model: str ) -> None: model = live_model roles = RoleMap(executor=model, planner=model, delegate=model, consultant=model) store = StateStore(tmp_path / "state.db", oplog=OpLog("nisse-dev", io.StringIO())) store.append_event( UserMessage( text=( "Project 'hello-cli': a Python script that prints a greeting. " "Draft the architecture plan." ), source="live-test", ) ) agent = LLMPlanningAgent(litellm, store, roles) action = agent.plan("arch", "hello-cli") assert isinstance(action, SubmitPlan | AskHuman) text = action.content if isinstance(action, SubmitPlan) else action.question assert text.strip(), "tool call carried empty content" # The turn must be durable regardless of what the model said (A2/A10). assert len(store.read_events()) > 1