from pathlib import Path from ui.components import PATH_CHIPS def test_trace_md_uses_hard_breaks(): import app md = app._trace_md(["● Routing… ✓ WASH", "● Selecting… ✓ 9 found"]) # each beat on its own rendered line: markdown hard break = two spaces + newline assert md == "● Routing… ✓ WASH \n● Selecting… ✓ 9 found" # single newline (which Markdown would collapse to a space) must NOT be the separator assert "WASH\n●" not in md def test_run_pipeline_streams_and_advances(monkeypatch): """run_pipeline (chips path) with 10-arg signature: switch to page 2, run route+select+bind(stub), end on page 3.""" import app from core.route import Step1Result from core.schemas import RouteResponse, BindResponse from core.select import SelectResult fake_route = Step1Result( route=RouteResponse(sectors=["WASH", "Food Security"], lens=[], type=[]), in_scope=["WASH", "Food Security"], out_of_scope=[], ) fake_select = SelectResult( selected_ids=("rcsi", "jmp_water_safely_managed"), indicator_defs={ "rcsi": {"label": "Reduced Coping Strategies Index", "cluster": "Food Security", "definition": "Coping behaviour."}, "jmp_water_safely_managed": {"label": "JMP Drinking Water — Safely Managed", "cluster": "WASH", "definition": "Safe water access."}, }, count_by_sector={"WASH": 1, "Food Security": 1}, ) fake_bind_rcsi = BindResponse( indicator_id="rcsi", variables=["coping_score"], measurable="MEASURABLE", reasons="Score directly computable.", result_ids=[], ) fake_bind_jmp = BindResponse( indicator_id="jmp_water_safely_managed", variables=["water_source"], measurable="PROXY", reasons="Source type captured but not full ladder.", result_ids=[], ) bind_map = {"rcsi": fake_bind_rcsi, "jmp_water_safely_managed": fake_bind_jmp} fake_spec_md_path = Path("/tmp/test_spec.md") fake_spec_md_path.write_text("## Analysis spec\n\nTest content.") monkeypatch.setattr(app, "resolve_route", lambda *a, **kw: fake_route) monkeypatch.setattr(app, "select_step", lambda sectors, **kw: fake_select) monkeypatch.setattr(app, "new_session", lambda *a, **kw: "test-sid") monkeypatch.setattr(app, "write_state", lambda sid, patch, **kw: None) monkeypatch.setattr(app, "ModalChatModel", lambda: object()) monkeypatch.setattr(app, "parse_kobo", lambda *a, **kw: Path("/tmp/kobo_cache.json")) monkeypatch.setattr(app, "kobo_summary", lambda cache_path: {"water_source": "Water source type", "coping_score": "Coping score"}) monkeypatch.setattr(app, "run_bind_step", lambda ind_id, ind_def, cache_path, summary_map, model: bind_map[ind_id]) monkeypatch.setattr(app, "load_state", lambda sid: {"route": {"sectors": ["WASH", "Food Security"], "lens": [], "type": []}}) monkeypatch.setattr(app, "assemble_spec_yaml", lambda *a, **kw: Path("/tmp/test_spec.yaml")) monkeypatch.setattr(app, "render_spec_md", lambda *a, **kw: fake_spec_md_path) def visible(obj): return obj["visible"] # New 11-arg signature: path, question, sectors, sectors_no_cat, lens, type, session_id, # kobo_file_path, demo_mode, survey_sheet, choices_sheet frames = list(app.run_pipeline( PATH_CHIPS, "", ["WASH", "Food Security"], [], [], [], "", "/tmp/kobo.json", False, "survey", "choices", )) # first frame switches to page 2 assert visible(frames[0][0]) is False assert visible(frames[0][1]) is True all_trace = " ".join(str(f[3]) for f in frames) assert "Routing" in all_trace assert "Selecting" in all_trace assert "Mapping" in all_trace # final frame advances to page 3 last = frames[-1] assert visible(last[2]) is True assert last[1]["visible"] is False # 7-tuple: (page1, page2, page3, trace_md, spec_md, session_id, spec_download) assert len(last) == 7 def test_run_pipeline_empty_sectors_no_page_switch(monkeypatch): """Empty sector list on chips path with 10-arg signature: yields error without switching to page 2.""" import app frames = list(app.run_pipeline( PATH_CHIPS, "", [], [], [], [], "", None, False, "survey", "choices", )) assert len(frames) == 1 assert "sector" in str(frames[0][3]).lower()