from __future__ import annotations
import json
import unittest
from iris.config import IrisConfig
from iris.engine import (
DIRECTION_SOFT_ATTEMPTS,
DistillResult,
IrisEngine,
PressureResult,
advice_language_phrase,
)
from iris.errors import IrisError, IrisResponseError
from iris.gate import score_spiral
from iris.http_client import ChatCompletionsClient
from iris.parser import parse_json_object, parse_json_object_with_key
from iris.spiral import SpiralRun
from iris.ui import (
APP_CSS,
APP_JS,
CenterView,
MAX_TRAIL_LINES,
RingView,
SpatialSession,
SpiralView,
build_frame_context,
pressure_cards_to_constraints,
render_interactive_canvas_html,
render_spiral_html,
run_canvas_engine,
session_to_view,
)
class FakeClient:
def __init__(self, responses: list[str]):
self.responses = responses
self.messages: list[list[dict[str, str]]] = []
def complete(
self, messages: list[dict[str, str]], temperature: float | None = None
) -> str:
self.messages.append(messages)
return self.responses.pop(0)
class FakeHTTPClient(ChatCompletionsClient):
def __init__(self, responses: list[str]):
self.responses = responses
self.messages: list[list[dict[str, str]]] = []
def complete(
self, messages: list[dict[str, str]], temperature: float | None = None
) -> str:
self.messages.append(messages)
return self.responses.pop(0)
class ParserTests(unittest.TestCase):
def test_parses_plain_json(self) -> None:
data = parse_json_object('{"pressure": "Name the real blocker."}')
self.assertEqual(data["pressure"], "Name the real blocker.")
def test_parses_fenced_json(self) -> None:
data = parse_json_object('```json\n{"next_step": "Call one user."}\n```')
self.assertEqual(data["next_step"], "Call one user.")
def test_parses_embedded_json(self) -> None:
data = parse_json_object(
'Reasoning hidden by model. {"pressure": "Be precise.", "why_it_bites": "Vague fails."}'
)
self.assertEqual(data["why_it_bites"], "Vague fails.")
def test_parses_first_json_object_only(self) -> None:
data = parse_json_object(
'{"pressure":"First?", "why_it_bites":"First bite."}\n{"pressure":"Second?", "why_it_bites":"Second bite."}'
)
self.assertEqual(data["pressure"], "First?")
def test_parses_json_object_with_requested_key(self) -> None:
data = parse_json_object_with_key(
'{"pressure_question": "What hard rule blocks the saw?"} {"why_it_bites": "The hidden risk appears at handoff."}',
"why_it_bites",
)
self.assertEqual(data["why_it_bites"], "The hidden risk appears at handoff.")
def test_parses_python_literal_dict(self) -> None:
data = parse_json_object(
'{"pressure":("Be precise."), "why_it_bites":("Vague fails.")}'
)
self.assertEqual(data["pressure"], "Be precise.")
def test_repairs_missing_comma_between_fields(self) -> None:
data = parse_json_object(
'{"pressure":"Be precise." "why_it_bites":"Vague fails."}'
)
self.assertEqual(data["why_it_bites"], "Vague fails.")
def test_repairs_missing_opening_quote_on_value(self) -> None:
data = parse_json_object(
'{"pressure":"Be precise.", "why_it_bites": Vague fails."}'
)
self.assertEqual(data["why_it_bites"], "Vague fails.")
def test_parses_unkeyed_bite_sentence(self) -> None:
data = parse_json_object(
'{"pressure":"Be precise." "This bites because vague fails."}'
)
self.assertEqual(data["why_it_bites"], "vague fails.")
def test_rejects_missing_json(self) -> None:
with self.assertRaises(IrisResponseError):
parse_json_object("think about your users")
def test_rejects_non_text_response(self) -> None:
with self.assertRaises(IrisResponseError):
parse_json_object(None) # type: ignore[arg-type]
class EngineTests(unittest.TestCase):
def test_pressure_appends_thinking_toggle_when_enabled(self) -> None:
client = FakeClient(
['{"pressure": "What happens when someone tries the idea?", "why_it_bites": "The first contact may fail."}']
)
engine = IrisEngine(
client,
config=IrisConfig(
api_base_url="http://localhost:11434/v1",
model="openbmb/minicpm4.1",
api_key="not-needed",
enable_thinking=True,
),
)
engine.pressure("Idea", [], 1, 4)
self.assertTrue(client.messages[0][1]["content"].rstrip().endswith("/think"))
def test_pressure_omits_thinking_toggle_when_disabled(self) -> None:
client = FakeClient(
['{"pressure": "What happens when someone tries the idea?", "why_it_bites": "The first contact may fail."}']
)
engine = IrisEngine(
client,
config=IrisConfig(
api_base_url="http://localhost:11434/v1",
model="openbmb/minicpm4.1",
api_key="not-needed",
enable_thinking=False,
),
)
engine.pressure("Idea", [], 1, 4)
self.assertFalse(client.messages[0][1]["content"].rstrip().endswith("/think"))
def test_pressure_parses_required_fields(self) -> None:
engine = IrisEngine(
FakeClient(
['{"pressure": "What happens when elderly people ignore the reminder?", "why_it_bites": "The reminder fails if the app is ignored."}']
)
)
result = engine.pressure(
"An app that reminds elderly people to take medication.",
[],
1,
4,
)
self.assertIn("elderly", result.pressure)
self.assertIn("ignored", result.why_it_bites)
def test_pressure_accepts_key_aliases(self) -> None:
engine = IrisEngine(
FakeClient(
['{"constraint": "What happens when someone tries the idea?", "why_it_bits": "The buyer and user may differ."}']
)
)
result = engine.pressure("Idea", [], 1, 4)
self.assertEqual(result.pressure, "What happens when someone tries the idea?")
self.assertEqual(result.why_it_bites, "The buyer and user may differ.")
def test_pressure_splits_inline_why(self) -> None:
engine = IrisEngine(
FakeClient(
['{"pressure": "What happens when someone tries the idea? Why it bites: The buyer and user may differ."}']
)
)
result = engine.pressure("Idea", [], 1, 4)
self.assertEqual(result.pressure, "What happens when someone tries the idea?")
self.assertEqual(result.why_it_bites, "The buyer and user may differ.")
def test_pressure_splits_inline_this_bites(self) -> None:
engine = IrisEngine(
FakeClient(
['{"pressure": "What happens when someone tries the idea? This bites because the buyer and user may differ."}']
)
)
result = engine.pressure("Idea", [], 1, 4)
self.assertEqual(result.pressure, "What happens when someone tries the idea?")
self.assertEqual(result.why_it_bites, "the buyer and user may differ.")
def test_pressure_retries_generic_output(self) -> None:
client = FakeClient(
[
'{"pressure": "The app needs user adoption.", "why_it_bites": "People may not use it."}',
'{"pressure": "What happens when a neighbor returns a borrowed tool broken?", "why_it_bites": "The trust problem appears before marketplace supply matters."}',
]
)
engine = IrisEngine(client)
result = engine.pressure("A marketplace for renting tools between neighbors.", [], 1, 4)
self.assertEqual(
result.pressure,
"What happens when a neighbor returns a borrowed tool broken?",
)
self.assertEqual(len(client.messages), 2)
def test_pressure_retries_unrelated_output(self) -> None:
client = FakeClient(
[
'{"pressure": "When the calendar alert fires during school pickup, what makes the parent stop?", "why_it_bites": "It is unrelated."}',
'{"pressure": "What happens when lecture notes contain half-finished diagrams that cannot become clean flashcards?", "why_it_bites": "The tool fails if real notes are messier than the conversion assumes."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"A study tool that turns lecture notes into flashcards.", [], 1, 4
)
self.assertIn("lecture notes", result.pressure)
self.assertEqual(len(client.messages), 2)
def test_pressure_retries_wrong_ring_opening(self) -> None:
client = FakeClient(
[
'{"pressure": "Why would a student use this?", "why_it_bites": "Wrong ring shape."}',
'{"pressure": "Who decides whether lecture notes become flashcards before the exam?", "why_it_bites": "The visible student may not control the study workflow."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"A study tool that turns lecture notes into flashcards.", [], 2, 4
)
self.assertTrue(result.pressure.startswith("Who"))
self.assertEqual(len(client.messages), 2)
def test_pressure_retries_advice_bite(self) -> None:
client = FakeClient(
[
'{"pressure": "What happens when elderly people miss a medication dose?", "why_it_bites": "The app should incorporate features for medication routines."}',
'{"pressure": "What happens when elderly people miss a medication dose?", "why_it_bites": "A missed dose can stay invisible until health consequences appear."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"An app that reminds elderly people to take their medication.", [], 1, 4
)
self.assertEqual(
result.why_it_bites,
"A missed dose can stay invisible until health consequences appear.",
)
self.assertEqual(len(client.messages), 2)
def test_existing_alternative_ring_parses_alternative(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What do people use today when neighbors need a power saw but no marketplace listing exists: hardware store rentals?", "alternative": "hardware store rentals", "why_it_bites": "A store rental may already solve the urgent access moment."}'
]
)
)
result = engine.pressure(
"A marketplace for renting tools between neighbors.",
[],
3,
4,
)
self.assertEqual(result.alternative, "hardware store rentals")
self.assertIn("Alternative: hardware store rentals", result.as_constraint())
def test_existing_alternative_ring_retries_missing_alternative(self) -> None:
client = FakeClient(
[
'{"pressure": "What do people use today when a neighbor needs a power saw but has no safety gear?", "why_it_bites": "The same safety issue repeats."}',
'{"pressure": "What do people use today when neighbors need a power saw quickly: hardware store rentals?", "alternative": "hardware store rentals", "why_it_bites": "A store rental may already solve the urgent access moment."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"A marketplace for renting tools between neighbors.",
[],
3,
4,
)
self.assertEqual(result.alternative, "hardware store rentals")
self.assertEqual(len(client.messages), 2)
def test_existing_alternative_ring_retries_prior_failure_copy(self) -> None:
client = FakeClient(
[
'{"pressure": "What do people use today when neighbors need a power saw quickly: safety gear?", "alternative": "safety gear", "why_it_bites": "The same safety issue repeats."}',
'{"pressure": "What do people use today when neighbors need a power saw quickly: hardware store rentals?", "alternative": "hardware store rentals", "why_it_bites": "A store rental may already solve the urgent access moment."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"A marketplace for renting tools between neighbors.",
[
"What happens when a neighbor lacks safety gear? Why it bites: Injury appears before trust matters."
],
3,
4,
)
self.assertEqual(result.alternative, "hardware store rentals")
self.assertEqual(len(client.messages), 2)
def test_existing_alternative_ring_retries_because_failure_shape(self) -> None:
client = FakeClient(
[
'{"pressure": "What do people use today when elderly people miss medication because they are distracted?", "alternative": "caregiver check-ins", "why_it_bites": "The same failure frame repeats."}',
'{"pressure": "What do people use today when elderly people need medication reminders without an app?", "alternative": "caregiver check-ins", "why_it_bites": "A personal reminder may already cover the daily medication moment."}',
]
)
engine = IrisEngine(client)
result = engine.pressure(
"An app that reminds elderly people to take their medication.",
[],
3,
4,
)
self.assertEqual(result.alternative, "caregiver check-ins")
self.assertNotIn("because", result.pressure)
self.assertEqual(len(client.messages), 2)
def test_advice_detector_allows_descriptive_need(self) -> None:
self.assertIsNone(
advice_language_phrase(
"A neighbor needs to rent a power saw before the weekend."
)
)
self.assertEqual(
advice_language_phrase("The app needs to include reminder features."),
"need to",
)
def test_distill_parses_center_fields_and_formats_action(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"actor": "one caregiver", "situation": "the last time a medication reminder was missed", "assumption_to_test": "reminders fail because caregivers do not see missed medication doses"}'
]
)
)
result = engine.distill("Medication reminder app.", ["pressure"])
self.assertEqual(result.actor, "one caregiver")
self.assertEqual(
result.next_step,
"Ask one caregiver to walk through this situation: the last time a medication reminder was missed, so you can test whether reminders fail because caregivers do not see missed medication doses.",
)
def test_distill_accepts_key_alias(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"role": "caregiver", "moment": "the last time a medication reminder was ignored", "assumption": "reminders fail because caregivers miss the medication breakdown"}'
]
)
)
result = engine.distill("Medication reminder app.", ["pressure"])
self.assertEqual(result.actor, "caregiver")
self.assertTrue(result.next_step.startswith("Ask one caregiver to walk through"))
def test_distill_retries_implementation_output(self) -> None:
client = FakeClient(
[
'{"actor": "caregiver", "situation": "the last time a medication reminder failed at home", "assumption_to_test": "implement a better reminder flow for medication safety"}',
'{"actor": "caregiver", "situation": "the last time a medication reminder failed at home", "assumption_to_test": "caregivers notice missed medication doses before harm appears"}',
]
)
engine = IrisEngine(client)
result = engine.distill("Medication reminder app.", ["pressure"])
self.assertEqual(
result.next_step,
"Ask one caregiver to walk through this situation: the last time a medication reminder failed at home, so you can test whether caregivers notice missed medication doses before harm appears.",
)
self.assertEqual(len(client.messages), 2)
def test_distill_retries_too_short_output(self) -> None:
client = FakeClient(
[
'{"actor": "Interview", "situation": "Interview", "assumption_to_test": "Interview"}',
'{"actor": "caregiver", "situation": "the last time a medication dose was missed", "assumption_to_test": "medication reminders fail because responsibility is unclear"}',
]
)
engine = IrisEngine(client)
result = engine.distill("Medication reminder app.", ["pressure"])
self.assertEqual(
result.next_step,
"Ask one caregiver to walk through this situation: the last time a medication dose was missed, so you can test whether medication reminders fail because responsibility is unclear.",
)
self.assertEqual(len(client.messages), 2)
def test_rejects_missing_pressure_field(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"why_it_bites": "No pressure."}',
'{"why_it_bites": "No pressure."}',
'{"why_it_bites": "No pressure."}',
'{"why_it_bites": "No pressure."}',
]
)
)
with self.assertRaises(IrisResponseError):
engine.pressure("Idea", [], 1, 4)
def test_gate_scores_clean_spiral(self) -> None:
run = SpiralRun(
idea="A study tool that turns lecture notes into flashcards.",
pressures=[
PressureResult(
"What happens when lecture notes include half-finished diagrams?",
"Students may memorize broken material before the exam.",
"",
),
PressureResult(
"Who decides whether lecture notes are accurate enough for exam prep?",
"The student may not know which parts are safe to trust.",
"",
),
PressureResult(
"What do people use today when lecture notes are too messy for flashcards: paper annotations?",
"Paper annotations may already cover the messy review moment.",
"",
alternative="paper annotations",
),
PressureResult(
"What if the real problem is not flashcards, but not knowing which lecture notes matter?",
"The study failure may happen before cards are useful.",
"",
),
],
center=DistillResult(
actor="student",
situation="the last time lecture notes felt too messy to study",
assumption_to_test="flashcards help only when lecture notes already mark what matters",
next_step="Ask one student to walk through this situation: the last time lecture notes felt too messy to study, so you can test whether flashcards help only when lecture notes already mark what matters.",
raw="",
),
)
report = score_spiral(run)
self.assertTrue(report.ok)
def test_gate_flags_advice_and_weak_center(self) -> None:
run = SpiralRun(
idea="A marketplace for renting tools between neighbors.",
pressures=[
PressureResult(
"What happens when a neighbor returns a borrowed tool broken?",
"The marketplace should include features for disputes.",
"",
),
PressureResult(
"What happens when a neighbor returns a borrowed tool broken?",
"The same pressure repeats.",
"",
),
],
center=DistillResult(
actor="Interview",
situation="Interview",
assumption_to_test="Interview",
next_step="Interview",
raw="",
),
)
report = score_spiral(run)
failures = {criterion.name for criterion in report.criteria if not criterion.ok}
self.assertFalse(report.ok)
self.assertIn("no_advice_language", failures)
self.assertIn("concrete_center", failures)
def test_ui_interactive_shell_has_empty_canvas_contract(self) -> None:
html = render_interactive_canvas_html()
# Focused sidebar + scrolling thread shell (no pan/zoom canvas).
self.assertIn("iris-frame-list", html)
self.assertIn("iris-thread", html)
self.assertIn("iris-thread-scroll", html)
self.assertIn('data-action="new-frame"', html)
self.assertIn("iris-sidebar", html)
self.assertNotIn("iris-canvas-viewport", html)
self.assertNotIn("pan-left", html)
self.assertNotIn("zoom-in", html)
self.assertNotIn("iris-frame-primary", html)
self.assertNotIn("marketplace", html.lower())
self.assertNotIn("lecture notes", html.lower())
self.assertNotIn("localStorage", html)
self.assertNotIn("sessionStorage", html)
assets = f"{APP_JS}\n{APP_CSS}"
self.assertNotIn("fonts.googleapis.com", assets)
self.assertNotIn("cdn.jsdelivr.net", assets)
self.assertNotIn("html2pdf", assets)
self.assertIn("Save PDF", APP_JS)
def test_engine_returns_four_direction_pressures(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What hard rule stops neighbors from lending a power saw on weekends?", "why_it_bites": "A safety or liability boundary can block the rental before trust matters."}',
'{"pressure": "Where does weekend tool rental break when the neighbor needs a rare saw size?", "why_it_bites": "The idea may only work for common tools and fail at the urgent edge."}',
'{"pressure": "What capability must exist to verify a neighbor power saw is available and safe?", "why_it_bites": "Without that capability, the marketplace cannot distinguish useful supply from risky supply."}',
'{"pressure": "What happens when a neighbor arrives Saturday morning and the borrowed saw is missing a blade?", "why_it_bites": "The real-world handoff can fail at the exact moment the rental is supposed to help."}',
]
)
)
results = engine.pressure_directions(
"A marketplace for renting tools between neighbors.", [], 1, 4
)
self.assertEqual(
[result.direction for result in results],
["Constraints", "Limitations", "Capabilities", "Reality Contact"],
)
self.assertIn("power saw", results[0].pressure)
def test_engine_repairs_direction_pressure_missing_bite(self) -> None:
client = FakeClient(
[
'{"pressure": "What hard rule stops neighbors from lending a power saw on weekends?"}',
'{"why_it_bites": "A safety or liability boundary can block the rental before trust matters."}',
'{"pressure": "Where does weekend tool rental break when the neighbor needs a rare saw size?", "why_it_bites": "The idea may only work for common tools and fail at the urgent edge."}',
'{"pressure": "What capability must exist to verify a neighbor power saw is available and safe?", "why_it_bites": "Without that capability, the marketplace cannot distinguish useful supply from risky supply."}',
'{"pressure": "What happens when a neighbor arrives Saturday morning and the borrowed saw is missing a blade?", "why_it_bites": "The real-world handoff can fail at the exact moment the rental is supposed to help."}',
]
)
engine = IrisEngine(client)
results = engine.pressure_directions(
"A marketplace for renting tools between neighbors.", [], 1, 4
)
self.assertEqual(
results[0].why_it_bites,
"A safety or liability boundary can block the rental before trust matters.",
)
self.assertEqual(len(client.messages), 5)
def test_engine_repairs_nested_direction_pressure_missing_question(self) -> None:
client = FakeClient(
[
'{"pressure": {"why_it_bites": "A safety or liability boundary can block the rental before trust matters."}}',
'{"pressure": "What hard safety rule blocks a neighbor from borrowing a power saw without training?"}',
'{"pressure": "Where does weekend tool rental break when the neighbor needs a rare saw size?", "why_it_bites": "The idea may only work for common tools and fail at the urgent edge."}',
'{"pressure": "What capability verifies a neighbor power saw is available and safe?", "why_it_bites": "Without verification, the marketplace cannot distinguish useful supply from risky supply."}',
'{"pressure": "What happens when a neighbor arrives Saturday morning and the borrowed saw is missing a blade?", "why_it_bites": "The real-world handoff can fail at the exact moment the rental is supposed to help."}',
]
)
engine = IrisEngine(client)
results = engine.pressure_directions(
"A marketplace for renting tools between neighbors.", [], 1, 4
)
self.assertEqual(
results[0].pressure,
"What hard safety rule blocks a neighbor from borrowing a power saw without training?",
)
self.assertEqual(
results[0].why_it_bites,
"A safety or liability boundary can block the rental before trust matters.",
)
self.assertIn("Existing why_it_bites", client.messages[1][1]["content"])
self.assertEqual(len(client.messages), 5)
def test_engine_reads_split_thinking_pressure_and_final_bite(self) -> None:
client = FakeClient(
[
'{"idea": "tool board", "direction": "Constraints", "pressure_question": "What hard safety rule stops neighbors from lending a power saw?"}',
'{"idea": "tool board", "direction": "Constraints", "pressure_question": "What hard safety rule stops neighbors from lending a power saw?"} {"why_it_bites": "A safety boundary can block the handoff before trust matters."}',
'{"pressure": "Where does weekend tool rental break when the neighbor needs a rare saw size?", "why_it_bites": "The idea may only work for common tools and fail at the urgent edge."}',
'{"pressure": "What capability must exist to verify a neighbor power saw is available and safe?", "why_it_bites": "Without that capability, the marketplace cannot distinguish useful supply from risky supply."}',
'{"pressure": "What happens when a neighbor arrives Saturday morning and the borrowed saw is missing a blade?", "why_it_bites": "The real-world handoff can fail at the exact moment the rental is supposed to help."}',
]
)
engine = IrisEngine(client)
results = engine.pressure_directions(
"A marketplace for renting tools between neighbors.", [], 1, 4
)
self.assertEqual(
results[0].pressure,
"What hard safety rule stops neighbors from lending a power saw?",
)
self.assertEqual(
results[0].why_it_bites,
"A safety boundary can block the handoff before trust matters.",
)
self.assertEqual(len(client.messages), 5)
def test_engine_does_not_treat_build_as_concrete_keyword(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What hard rule makes this impossible to implement?", "why_it_bites": "A hard rule can block the idea before any useful test exists."}',
'{"pressure": "Where does this break outside the easiest case?", "why_it_bites": "The idea may only survive when no real edge case appears."}',
'{"pressure": "What capability must exist before this can work?", "why_it_bites": "Without the capability, the idea has no real operating surface."}',
'{"pressure": "What happens when the first person tries to use this in a real moment?", "why_it_bites": "The first contact can reveal that the idea has no concrete behavior to attach to."}',
]
)
)
results = engine.pressure_directions("build idea", [], 1, 4)
self.assertEqual(len(results), 4)
def test_ui_engine_request_returns_four_pressure_payload(self) -> None:
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What hard rule stops neighbors from lending a power saw on weekends?", "why_it_bites": "A safety or liability boundary can block the rental before trust matters."}',
'{"pressure": "Where does weekend tool rental break when the neighbor needs a rare saw size?", "why_it_bites": "The idea may only work for common tools and fail at the urgent edge."}',
'{"pressure": "What capability must exist to verify a neighbor power saw is available and safe?", "why_it_bites": "Without that capability, the marketplace cannot distinguish useful supply from risky supply."}',
'{"pressure": "What happens when a neighbor arrives Saturday morning and the borrowed saw is missing a blade?", "why_it_bites": "The real-world handoff can fail at the exact moment the rental is supposed to help."}',
]
)
)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": "A marketplace for renting tools between neighbors.",
"depth": 1,
"prior_cards": [],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
self.assertEqual(payload["frame_id"], "frame-1")
self.assertEqual(len(payload["cards"]), 4)
self.assertEqual(
[card["direction"] for card in payload["cards"]],
["Constraints", "Limitations", "Capabilities", "Reality Contact"],
)
self.assertIn("power saw", payload["cards"][0]["pressure"])
def test_live_http_engine_uses_single_combined_direction_call(self) -> None:
client = FakeHTTPClient(
[
json.dumps(
{
"cards": [
{
"direction": "Constraints",
"pressure": "What hard trust rule stops neighbors from lending drills?",
"why_it_bites": "A trust boundary can block the handoff before supply matters.",
},
{
"direction": "Limitations",
"pressure": "Where does neighborhood tool lending break when the ladder is needed twice?",
"why_it_bites": "The idea may fail when one shared object has overlapping urgent demand.",
},
{
"direction": "Capabilities",
"pressure": "What capability verifies the drill, ladder, and pickup time before neighbors lend?",
"why_it_bites": "Without coordination, the board cannot tell real availability from hopeful inventory.",
},
{
"direction": "Reality Contact",
"pressure": "What happens when a neighbor arrives to lend the drill and the battery is already dead?",
"why_it_bites": "The first handoff can fail at the exact moment the idea needs trust.",
},
]
}
)
]
)
engine = IrisEngine(client)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": "A neighborhood tool lending board where trusted neighbors lend drills and ladders.",
"depth": 1,
"prior_cards": [],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["cards"]), 4)
self.assertEqual(len(client.messages), 1)
self.assertIn("exactly four direction cards", client.messages[0][-1]["content"])
def test_ui_engine_request_formats_prior_pressure_constraints(self) -> None:
prior_cards = [
{
"pressure": "What happens when a neighbor returns a borrowed tool broken?",
"why_it_bites": "The trust failure appears before marketplace supply matters.",
}
]
prior_cards[0]["direction"] = "Constraints"
constraints = pressure_cards_to_constraints(prior_cards)
self.assertEqual(
constraints,
[
"Constraints: What happens when a neighbor returns a borrowed tool broken? Why it bites: The trust failure appears before marketplace supply matters."
],
)
client = FakeClient(
[
'{"pressure": "What hard constraint stops the neighbor drill from being lent on Saturday?", "why_it_bites": "A single hard boundary can block the pickup before the idea creates value."}',
'{"pressure": "Where does the neighbor drill rental fail when the first borrower needs special bits?", "why_it_bites": "The rental may only cover the easy case and fail at the real job."}',
'{"pressure": "What capability verifies the neighbor drill, bits, and pickup time before Saturday?", "why_it_bites": "Without verification, the stack may show supply that cannot actually satisfy the job."}',
'{"pressure": "What happens when the neighbor arrives Saturday and the drill battery is dead?", "why_it_bites": "The handoff fails at the moment when the idea needs to feel reliable."}',
]
)
engine = IrisEngine(client)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": "A neighbor tool rental focused on one Saturday pickup.",
"depth": 2,
"prior_cards": prior_cards,
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
self.assertEqual(len(payload["cards"]), 4)
self.assertIn("borrowed tool broken", client.messages[0][1]["content"])
def test_ui_engine_request_passes_full_frame_context(self) -> None:
prior_cards = [
{
"depth": 1,
"direction": "Constraints",
"pressure": "What hard trust boundary blocks neighbors from sharing drills?",
"why_it_bites": "The first handoff can fail before the marketplace creates value.",
}
]
context = build_frame_context(
current_idea="Focus on roommate access first.",
iterations=[
{
"version": 1,
"idea": "A marketplace for renting tools between neighbors.",
},
{"version": 2, "idea": "Focus on roommate access first."},
],
prior_cards=prior_cards,
)
self.assertIn("Frame continuity:", context)
self.assertIn("Original idea:", context)
self.assertIn("A marketplace for renting tools between neighbors.", context)
self.assertIn("Current iteration:", context)
self.assertIn("Focus on roommate access first.", context)
self.assertIn("Prior AI pressure trail:", context)
self.assertIn("Depth 01 / Constraints", context)
self.assertIn("The first handoff can fail", context)
self.assertNotIn("Previous model pressure cards:", context)
client = FakeClient(
[
'{"pressure": "What hard access rule stops roommates from sharing rented tools?", "why_it_bites": "A household boundary can break the neighbor rental before trust matters."}',
'{"pressure": "Where does roommate tool access break when the renter is away?", "why_it_bites": "The idea may only work when the same person controls pickup and use."}',
'{"pressure": "What capability verifies roommate access to the neighbor tool before pickup?", "why_it_bites": "Without verification, the frame cannot tell permitted access from risky sharing."}',
'{"pressure": "What happens when a roommate arrives for the neighbor tool and the owner refuses?", "why_it_bites": "The real handoff can fail because the original renter is not present."}',
]
)
engine = IrisEngine(client)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": "Focus on roommate access first.",
"depth": 2,
"iterations": [
{
"version": 1,
"idea": "A marketplace for renting tools between neighbors.",
},
{"version": 2, "idea": "Focus on roommate access first."},
],
"prior_cards": prior_cards,
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
messages = client.messages[0]
prompt = messages[-1]["content"]
convo = "\n".join(message["content"] for message in messages)
# The final instruction still carries the grounded frame blob.
self.assertIn("Original idea:", prompt)
self.assertIn("A marketplace for renting tools between neighbors.", prompt)
self.assertIn("Current iteration:", prompt)
self.assertIn("Focus on roommate access first.", prompt)
self.assertIn("Prior pressure already applied:", prompt)
self.assertIn("trust boundary", prompt)
self.assertIn("ongoing dialogue with one specific person", prompt)
self.assertIn("living memory of THIS conversation", prompt)
self.assertIn("could not be asked of any other idea", prompt)
# The prior pressure now lives in replayed conversation turns, not the blob.
self.assertNotIn("Prior AI pressure trail:", prompt)
self.assertIn("My idea (v1):", convo)
self.assertIn("Pressure I already raised:", convo)
self.assertTrue(
any(message["role"] == "assistant" for message in messages)
)
def test_direction_pressure_retries_when_current_iteration_is_ignored(self) -> None:
context = build_frame_context(
current_idea="Focus on roommates borrowing from the original neighbor board first.",
iterations=[
{
"version": 1,
"idea": "A simple tool rental board for neighbors sharing drills and ladders.",
},
{
"version": 2,
"idea": "Focus on roommates borrowing from the original neighbor board first.",
},
],
prior_cards=[],
)
client = FakeClient(
[
'{"pressure": "What hard rule prevents neighbors from sharing drills and ladders?", "why_it_bites": "A hard boundary can block the board before trust matters."}',
'{"pressure": "What hard permission rule stops roommates from borrowing a neighbor drill?", "why_it_bites": "Roommate access can violate the trust boundary before the board creates value."}',
'{"pressure": "Where does roommate borrowing break when the original renter is away?", "why_it_bites": "The handoff may depend on a person who is not present."}',
'{"pressure": "What capability verifies a roommate is allowed to borrow the neighbor drill?", "why_it_bites": "Without permission proof, the board cannot separate allowed access from risky sharing."}',
'{"pressure": "What happens when a roommate arrives to borrow the neighbor drill and the owner refuses?", "why_it_bites": "The real handoff can collapse when the named borrower differs from the person at the door."}',
]
)
engine = IrisEngine(client)
results = engine.pressure_directions(context, [], 2, 4)
self.assertIn("roommates", results[0].pressure)
self.assertEqual(len(results), 4)
self.assertEqual(len(client.messages), 5)
self.assertIn("ignored the current iteration", client.messages[1][1]["content"])
def test_soft_mode_never_shows_template_placeholder_cards(self) -> None:
# The model keeps echoing the prompt template with bracketed placeholders.
# Soft mode must NOT surface "[Constraint]"/"[this]" cards to the user;
# it returns a clean honest fallback instead.
leak = (
'{"pressure": "What hard [Constraint] does [this] collide with?", '
'"why_it_bites": "It collides with [the idea]."}'
)
engine = IrisEngine(FakeClient([leak] * 64))
results = engine.pressure_directions(
"An app that reminds elderly people to take medication.",
[],
1,
4,
allow_soft_failures=True,
)
self.assertEqual(len(results), 4)
for result in results:
self.assertNotIn("[", result.pressure)
self.assertNotIn("[", result.why_it_bites)
def test_pressure_directions_soft_mode_never_crashes_on_unparseable_echo(self) -> None:
# The local model echoes the prompt instead of answering: no "pressure"
# key, and invalid JSON. Soft mode must still return four cards.
echo = (
'{"idea": "build app", "direction": "Limitations", '
'"current_iteration": "make it simpler", "depth": 01}'
)
engine = IrisEngine(FakeClient([echo] * 64))
results = engine.pressure_directions(
"A study tool that turns lecture notes into flashcards.",
[],
2,
4,
allow_soft_failures=True,
)
self.assertEqual(len(results), 4)
self.assertEqual(
[result.direction for result in results],
["Constraints", "Limitations", "Capabilities", "Reality Contact"],
)
for result in results:
self.assertTrue(result.pressure.strip())
self.assertTrue(result.why_it_bites.strip())
def test_pressure_directions_strict_mode_still_raises_on_unparseable(self) -> None:
echo = '{"idea": "build app", "depth": 01}'
engine = IrisEngine(FakeClient([echo] * 64))
with self.assertRaises(IrisError):
engine.pressure_directions(
"A study tool that turns lecture notes into flashcards.",
[],
2,
4,
)
def test_finalize_mode_returns_brief_payload(self) -> None:
client = FakeClient(
[
'{"refined_idea": "A free tool-lending board for one neighborhood that '
'leans on existing neighbor trust instead of deposits or payments."}',
'{"actor": "a neighbor who lends tools", "situation": "lending a power '
'drill to someone down the street", "assumption_to_test": "neighbors '
'will lend expensive tools without a deposit or contract"}',
]
)
engine = IrisEngine(client)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"mode": "finalize",
"idea": "Keep the neighbor tool board free and trust-based.",
"iterations": [
{"version": 1, "idea": "A marketplace for renting tools between neighbors."},
{"version": 2, "idea": "Keep the neighbor tool board free and trust-based."},
],
"prior_cards": [
{
"depth": 1,
"direction": "Constraints",
"pressure": "What hard trust boundary blocks neighbors from sharing drills?",
"why_it_bites": "The first handoff can fail before value appears.",
}
],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "final")
self.assertIn("trust", payload["refined_idea"].lower())
self.assertEqual(len(payload["journey"]), 2)
self.assertEqual(len(payload["pressures"]), 1)
self.assertTrue(payload["next_step"])
self.assertIn("neighbor", payload["actor"].lower())
def test_finalize_mode_survives_unparseable_model(self) -> None:
# Even if the model only echoes garbage, finalize must still return.
engine = IrisEngine(FakeClient(['{"junk": 1, "depth": 01}'] * 64))
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"mode": "finalize",
"idea": "Keep the neighbor tool board free and trust-based.",
"iterations": [
{"version": 1, "idea": "A marketplace for renting tools between neighbors."},
{"version": 2, "idea": "Keep the neighbor tool board free and trust-based."},
],
"prior_cards": [],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "final")
# Falls back to the current iteration text when the model cannot refine.
self.assertIn("trust-based", payload["refined_idea"].lower())
def test_build_frame_context_anchors_original_and_marks_current(self) -> None:
context = build_frame_context(
current_idea="Make it caregiver-first instead of elder-first.",
iterations=[
{"version": 1, "idea": "An app that reminds elderly people to take meds."},
{"version": 2, "idea": "Make it caregiver-first instead of elder-first."},
],
prior_cards=[],
)
# Original idea must appear before the current iteration so a small model
# anchors on the root idea first.
self.assertLess(
context.index("Original idea:"), context.index("Current iteration:")
)
# The latest history entry is flagged so the model knows which to pressure.
self.assertIn("(current)", context)
self.assertIn("- Idea v2: Make it caregiver-first", context)
def test_build_frame_context_caps_long_pressure_trail(self) -> None:
prior_cards = [
{
"depth": index,
"direction": "Constraints",
"pressure": f"What hard rule blocks step {index}?",
"why_it_bites": f"Risk {index} bites here.",
}
for index in range(1, MAX_TRAIL_LINES + 6)
]
context = build_frame_context(
current_idea="Keep iterating the same frame.",
iterations=[{"version": 1, "idea": "An original framed idea to pressure."}],
prior_cards=prior_cards,
)
trail_lines = [
line
for line in context.splitlines()
if line.startswith("- Depth ")
]
# The freshest rings are kept; older ones are trimmed with a marker.
self.assertEqual(len(trail_lines), MAX_TRAIL_LINES)
self.assertIn("older entries)", context)
self.assertIn(f"What hard rule blocks step {MAX_TRAIL_LINES + 5}?", context)
self.assertNotIn("What hard rule blocks step 1?", context)
def test_ui_engine_request_preserves_deep_frame_memory(self) -> None:
prior_cards = [
{
"depth": 1,
"direction": "Reality Contact",
"pressure": "What happens when a dispatcher receives weather data late?",
"why_it_bites": "The flight decision can already be locked before the data arrives.",
},
{
"depth": 4,
"direction": "Capabilities",
"pressure": "What capability verifies a rare weather source during a flight?",
"why_it_bites": "A fast model can still amplify untrusted data.",
},
{
"depth": 7,
"direction": "Limitations",
"pressure": "Where does cheap compute fail when the data source is delayed?",
"why_it_bites": "Compute speed does not solve the freshness of the signal.",
},
]
client = FakeClient(
[
'{"pressure": "What hard certification rule blocks vetted weather data from changing a live flight model?", "why_it_bites": "Certification may freeze the decision path before fresh data matters."}',
'{"pressure": "Where does vetted weather data break when cheap compute receives the source after the pilot decision?", "why_it_bites": "The strongest data is useless if it arrives after the operational moment."}',
'{"pressure": "What capability proves the vetted weather source is trustworthy before cheap compute acts?", "why_it_bites": "Without trust proof, fast updates can make the system confidently wrong."}',
'{"pressure": "What happens when a pilot sees a cheap-compute recommendation from a vetted data source mid-flight?", "why_it_bites": "The cockpit moment tests whether the model output is trusted under pressure."}',
]
)
engine = IrisEngine(client)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-9",
"idea": "Focus on vetted weather data plus cheap compute.",
"depth": 8,
"iterations": [
{
"version": 1,
"idea": "An aviation model that updates with rare weather data.",
},
{
"version": 2,
"idea": "Use cheap compute to make updates affordable.",
},
{
"version": 3,
"idea": "Require vetted weather data before model updates.",
},
{
"version": 4,
"idea": "Focus on vetted weather data plus cheap compute.",
},
],
"prior_cards": prior_cards,
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["depth"], 8)
self.assertEqual(len(payload["cards"]), 4)
messages = client.messages[0]
prompt = messages[-1]["content"]
convo = "\n".join(message["content"] for message in messages)
self.assertIn("Idea v1: An aviation model", prompt)
self.assertIn("Idea v4: Focus on vetted weather data", prompt)
self.assertIn("living memory of THIS conversation", prompt)
# Prior ideas and the pressures Iris raised are replayed as chat turns.
self.assertIn("My idea (v4):", convo)
self.assertIn("Pressure I already raised:", convo)
self.assertIn(
"What capability verifies a rare weather source during a flight?", convo
)
def test_ui_engine_request_keeps_pressure_rounds_open_ended(self) -> None:
prior_cards = [
{
"pressure": "What happens when lecture notes include half-finished diagrams?",
"why_it_bites": "Students may memorize broken material before the exam.",
},
{
"pressure": "Who decides whether lecture notes are accurate enough for exam prep?",
"why_it_bites": "The student may not know which parts are safe to trust.",
},
{
"pressure": "What do people use today when lecture notes are too messy for flashcards: paper annotations?",
"why_it_bites": "Paper annotations may already cover the messy review moment.",
"alternative": "paper annotations",
},
{
"pressure": "What if the real problem is not flashcards, but not knowing which lecture notes matter?",
"why_it_bites": "The study failure may happen before cards are useful.",
},
]
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What hard exam rule makes lecture flashcards risky when notes are incomplete?", "why_it_bites": "A course constraint can make bad cards harmful before the student notices."}',
'{"pressure": "Where does lecture flashcard review break when notes omit diagrams?", "why_it_bites": "The study loop may fail exactly where visual material carries the concept."}',
'{"pressure": "What capability identifies which lecture notes are complete enough for flashcards?", "why_it_bites": "Without that capability, the stack can convert broken notes into confident mistakes."}',
'{"pressure": "What happens when a student studies a flashcard made from unfinished lecture notes?", "why_it_bites": "The first real review can reinforce the wrong material before the exam."}',
]
)
)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-2",
"idea": "A study tool that turns lecture notes into flashcards.",
"depth": 5,
"prior_cards": prior_cards,
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
self.assertEqual(payload["depth"], 5)
self.assertEqual(len(payload["cards"]), 4)
self.assertNotIn("next_step", payload)
def test_ui_engine_request_soft_accepts_repeat_after_retry_exhaustion(self) -> None:
repeated = (
'{"pressure": "Where does lecture flashcard review break when notes omit diagrams?", '
'"why_it_bites": "The study loop may fail where visual material carries the concept."}'
)
engine = IrisEngine(
FakeClient(
[
'{"pressure": "What hard exam rule makes diagram flashcards risky?", "why_it_bites": "A course constraint can punish confident but incomplete study material."}',
repeated,
repeated,
repeated,
repeated,
'{"pressure": "What capability identifies which lecture diagrams are complete enough for flashcards?", "why_it_bites": "Without that capability, broken notes can become confident mistakes."}',
'{"pressure": "What happens when a student reviews a flashcard from an unfinished lecture diagram?", "why_it_bites": "The first review can reinforce the wrong concept before the exam."}',
]
)
)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-2",
"idea": "Focus on diagrams in lecture notes.",
"depth": 2,
"iterations": [
{
"version": 1,
"idea": "A study tool that turns lecture notes into flashcards.",
},
{"version": 2, "idea": "Focus on diagrams in lecture notes."},
],
"prior_cards": [
{
"direction": "Limitations",
"pressure": "Where does lecture flashcard review break when notes omit diagrams?",
"why_it_bites": "The study loop may fail where visual material carries the concept.",
}
],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
self.assertEqual(len(payload["cards"]), 4)
self.assertEqual(
payload["cards"][1]["pressure"],
"Where does lecture flashcard review break when notes omit diagrams?",
)
def test_ui_engine_request_soft_accepts_current_iteration_after_retry_exhaustion(self) -> None:
missed_iteration = (
'{"pressure": "What hard aviation rule blocks real-time model weight updates during a live flight?", '
'"why_it_bites": "Live model changes can create unpredictable decisions before operators can verify them."}'
)
engine = IrisEngine(
FakeClient(
[
# Soft mode retries DIRECTION_SOFT_ATTEMPTS (2) times before
# accepting the best-effort card for direction 1.
*([missed_iteration] * DIRECTION_SOFT_ATTEMPTS),
'{"pressure": "Where does cheap compute break when vetted data is still too slow to arrive?", "why_it_bites": "The system can have processing power without trustworthy fresh inputs at the decision moment."}',
'{"pressure": "What capability must exist to verify vetted data before cheap compute changes the model?", "why_it_bites": "Without verification, fast processing can amplify bad data rather than improve decisions."}',
'{"pressure": "What happens when a pilot receives a cheap-compute update from a vetted data source mid-flight?", "why_it_bites": "The real cockpit moment can expose whether fresh information is trusted quickly enough to matter."}',
]
)
)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": (
"so we can think about cheap compute allows this "
"approach and well vetted data form amazing source"
),
"depth": 2,
"iterations": [
{
"version": 1,
"idea": (
"A flight decision model that uses fresh data "
"during rare weather events."
),
},
{
"version": 2,
"idea": (
"so we can think about cheap compute allows "
"this approach and well vetted data form "
"amazing source"
),
},
],
"prior_cards": [],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(payload["kind"], "pressures")
self.assertEqual(len(payload["cards"]), 4)
self.assertIn("aviation rule", payload["cards"][0]["pressure"])
self.assertIn("cheap compute", payload["cards"][1]["pressure"])
def test_ui_engine_request_soft_accepts_idea_grounding_after_retry_exhaustion(self) -> None:
missed_idea = (
'{"pressure": "What hard payroll rule blocks employee timesheet imports?", '
'"why_it_bites": "A compliance boundary can stop the workflow before value appears."}'
)
engine = IrisEngine(
FakeClient(
[
*([missed_idea] * DIRECTION_SOFT_ATTEMPTS),
'{"pressure": "Where does vetted weather data break when cheap compute receives it late?", "why_it_bites": "The model can be fast and still miss the operational moment."}',
'{"pressure": "What capability validates the weather source before cheap compute updates the flight model?", "why_it_bites": "Without validation, fast updates can amplify a bad signal."}',
'{"pressure": "What happens when a pilot receives a vetted weather update after choosing a route?", "why_it_bites": "The real decision moment can pass before the update matters."}',
]
)
)
payload = json.loads(
run_canvas_engine(
json.dumps(
{
"frame_id": "frame-1",
"idea": "Focus on vetted weather data plus cheap compute.",
"depth": 4,
"iterations": [
{
"version": 1,
"idea": "An aviation model for rare weather decisions.",
},
{
"version": 2,
"idea": "Focus on vetted weather data plus cheap compute.",
},
],
"prior_cards": [],
}
),
engine=engine,
)
)
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["cards"]), 4)
self.assertIn("payroll rule", payload["cards"][0]["pressure"])
def test_ui_render_includes_canvas_frame_and_center_card(self) -> None:
html = render_spiral_html(
SpiralView(
idea="A marketplace for renting tools between neighbors.",
rings=[
RingView(
depth=3,
pressure="What do neighbors do today?",
why_it_bites="Informal trust may already cover the moment.",
alternative="Informal agreements",
)
],
center=CenterView(
actor="neighbor",
situation="renting a power saw",
assumption_to_test="informal agreements are not enough",
next_step="Ask one neighbor to walk through this situation.",
),
status="complete",
)
)
self.assertIn("iris-board", html)
self.assertIn("iris-canvas-v2", html)
self.assertIn("iris-frame-primary", html)
self.assertIn("iris-card-center", html)
self.assertIn("IRIS", html)
self.assertIn("Pressure canvas", html)
self.assertIn("Ask one neighbor", html)
self.assertIn("informal agreements are not enough", html)
self.assertIn("status-complete", html)
self.assertNotIn("iris-electron", html)
def test_ui_render_shows_pending_ring_state(self) -> None:
html = render_spiral_html(
SpiralView(
idea="A marketplace for renting tools between neighbors.",
rings=[],
status="running",
message="Real Actor is next.",
pending_depth=2,
)
)
self.assertIn("Real Actor is next.", html)
self.assertIn("Real Actor forming", html)
self.assertIn("iris-card-ai is-pending", html)
self.assertIn("AI pressure", html)
self.assertNotIn("iris-electron", html)
def test_ui_render_shows_center_pending_state(self) -> None:
html = render_spiral_html(
SpiralView(
idea="A study tool that turns lecture notes into flashcards.",
rings=[
RingView(
depth=4,
pressure="What if the real problem is note triage?",
why_it_bites="The study failure may happen before cards are useful.",
)
],
status="running",
message="Center is forming.",
center_pending=True,
)
)
self.assertIn("Center is forming.", html)
self.assertIn("Next step pending", html)
self.assertIn("iris-card-center is-pending", html)
def test_ui_session_to_view_selects_latest_pressure_card(self) -> None:
session = SpatialSession(
idea="A marketplace for renting tools between neighbors.",
pressures=[
PressureResult(
"What happens when a borrowed tool breaks?",
"The trust failure appears before marketplace supply matters.",
"",
)
],
status="waiting",
message="R1 pressure card selected.",
selected_depth=1,
)
html = render_spiral_html(session_to_view(session))
self.assertIn("What happens when a borrowed tool breaks?", html)
self.assertIn("The trust failure appears", html)
self.assertIn("Reality Contact", html)
self.assertIn("is-selected", html)
self.assertIn("status-waiting", html)
self.assertNotIn("electron", html.lower())
def test_ui_render_escapes_user_content(self) -> None:
html = render_spiral_html(
SpiralView(
idea="",
rings=[],
)
)
self.assertIn("<script>", html)
self.assertNotIn("