"""Decision tree and hall portrait planning.""" import json from pathlib import Path import pytest from llm_connect.cli import main from llm_connect.exceptions import LLMConfigurationError from llm_connect.hall_portraits import plan_missing_portraits, reveal_portrait from llm_connect.image_selection import classify_image_branch, selection_for_prompt HALL_PROMPT = ( "Constellation dialect. Square, dark indigo. A gold-wire helix. " "Fine technical-illustration linework, no logos, no readable text." ) def test_hall_house_prompt_is_general_efficient() -> None: selection = selection_for_prompt(HALL_PROMPT) assert classify_image_branch(HALL_PROMPT) == "general" assert selection.tier == "efficient" assert selection.model == "google/gemini-nano-banana-2.1" assert selection.quote_usd == 0.04 assert selection.max_cost_usd == 0.08 def test_negated_text_does_not_become_text_heavy() -> None: assert classify_image_branch("no logos, no readable text, pale gold") == "general" def test_abstract_marks_stay_on_the_general_branch() -> None: prompt = ( "a ledger glows with the words rendered as abstract marks, not readable text. " "no logos, no lettering" ) assert classify_image_branch(prompt) == "general" def test_requested_words_take_the_text_branch() -> None: selection = selection_for_prompt("A poster whose headline is the words HELIX") assert selection.branch == "text_heavy" assert selection.model == "openai/gpt-image-2.5-sunburst" def test_vector_and_self_host_follow_the_tree() -> None: assert selection_for_prompt("Export as a vector svg icon").model.startswith("recraft/") try: selection_for_prompt("self-host the open weights on our own hardware") except Exception as exc: assert "OpenRouter" in str(exc) else: raise AssertionError("self-host branch should refuse") def _entry(visual: str, comment: str | None, entry_id: str = "hall-worker-ada-1") -> str: commented = f"\n" if comment else "" return f"""--- id: {entry_id} status: draft --- # Ada — a seat ## Visual prompt > {visual} {commented} ## Handoff Next. """ def test_plan_skips_a_portrait_that_is_already_on_disk(tmp_path: Path) -> None: entries = tmp_path / "entries" visuals = tmp_path / "visuals" entries.mkdir() visuals.mkdir() (entries / "one.md").write_text(_entry(HALL_PROMPT, "Ada"), encoding="utf-8") (entries / "two.md").write_text(_entry(HALL_PROMPT, None, "hall-worker-bea-2"), encoding="utf-8") (visuals / "bea-file.jpg").write_bytes(b"x") # two.md has no comment, so its derived file is the id. Put that file on disk. (visuals / "hall-worker-bea-2.jpg").write_bytes(b"x") jobs = plan_missing_portraits(tmp_path) assert [job.entry_id for job in jobs] == ["hall-worker-ada-1"] assert jobs[0].output_path == visuals / "ada.jpg" assert jobs[0].relative_path == "../visuals/ada.jpg" assert jobs[0].alt == "Ada" assert "no readable text" in jobs[0].prompt def test_reveal_portrait_uncomments_the_intended_line() -> None: text = _entry(HALL_PROMPT, "Ada") updated = reveal_portrait(text, "../visuals/ada.jpg", "Ada") assert "