from __future__ import annotations import json from types import SimpleNamespace from surfaces.cli.tests import interactive from surfaces.cli.tests.catalog import TestCatalog as Catalog from surfaces.cli.tests.catalog import TestCatalogItem as CatalogItem from surfaces.cli.tests.discover import load_test_catalog def test_choose_interactive_item_returns_single_match_without_extra_prompt(monkeypatch) -> None: catalog = Catalog( items=( CatalogItem( id="make:test-cov", kind="make_target", display_name="Coverage Suite", description="Run the coverage suite.", command=("make", "test-cov"), tags=("ci-safe",), ), ) ) selected_prompts: list[str] = [] monkeypatch.setattr(interactive, "_choose_category", lambda: "ci-safe") def _mock_select_item(items, *, prompt: str, allow_back: bool = False): _ = allow_back selected_prompts.append(prompt) return items[0] monkeypatch.setattr(interactive, "_select_item", _mock_select_item) item, auto_selected = interactive.choose_interactive_item(catalog) assert item.id == "make:test-cov" assert auto_selected is True assert selected_prompts == [] def test_choose_interactive_item_prompts_when_multiple_matches_exist(monkeypatch) -> None: catalog = load_test_catalog() selected_prompts: list[str] = [] selected_item_ids: list[list[str]] = [] monkeypatch.setattr(interactive, "_choose_category", lambda: "ci-safe") def _mock_select_item_or_all(items, *, prompt: str, allow_back: bool = False): _ = allow_back selected_prompts.append(prompt) selected_item_ids.append([item.id for item in items]) return items[0] monkeypatch.setattr(interactive, "_select_item_or_all", _mock_select_item_or_all) item, auto_selected = interactive.choose_interactive_item(catalog) assert item.id == selected_item_ids[0][0] assert auto_selected is False assert selected_prompts == ["Choose a test or suite:"] assert "make:test-cov" in selected_item_ids[0] def test_choose_interactive_item_retries_after_empty_filter(monkeypatch) -> None: catalog = Catalog( items=( CatalogItem( id="make:test-cov", kind="make_target", display_name="Coverage Suite", description="Run the coverage suite.", command=("make", "test-cov"), tags=("ci-safe",), ), ) ) category_choices = iter(["rca", "ci-safe"]) monkeypatch.setattr(interactive, "_choose_category", lambda: next(category_choices)) item, auto_selected = interactive.choose_interactive_item(catalog) assert item.id == "make:test-cov" assert auto_selected is True def test_choose_interactive_item_reselects_category_after_escape(monkeypatch) -> None: catalog = Catalog( items=( CatalogItem( id="make:test-cov", kind="make_target", display_name="Coverage Suite", description="Run the coverage suite.", command=("make", "test-cov"), tags=("ci-safe",), ), CatalogItem( id="make:test-full", kind="make_target", display_name="Full Suite", description="Run the full suite.", command=("make", "test-full"), tags=("ci-safe",), ), CatalogItem( id="make:demo", kind="make_target", display_name="Prefect ECS Demo", description="Run the demo.", command=("make", "demo"), tags=("demo",), ), ) ) category_choices = iter(["ci-safe", "demo"]) selected_prompts: list[str] = [] monkeypatch.setattr(interactive, "_choose_category", lambda: next(category_choices)) def _mock_select_item_or_all(items, *, prompt: str, allow_back: bool = False): _ = allow_back selected_prompts.append(prompt) if len(selected_prompts) == 1: raise interactive._GoBack return items[0] monkeypatch.setattr(interactive, "_select_item_or_all", _mock_select_item_or_all) item, _ = interactive.choose_interactive_item(catalog) assert item.id == "make:demo" assert selected_prompts == ["Choose a test or suite:"] def test_choose_interactive_item_returns_to_parent_list_after_escape(monkeypatch) -> None: suite = CatalogItem( id="suite:demo", kind="suite", display_name="Demo Suite", description="A grouped demo suite.", tags=("demo",), children=( CatalogItem( id="scenario:demo:first", kind="scenario", display_name="First scenario", description="First child.", command=("make", "demo"), tags=("demo",), ), CatalogItem( id="scenario:demo:second", kind="scenario", display_name="Second scenario", description="Second child.", command=("make", "demo"), tags=("demo",), ), ), ) leaf = CatalogItem( id="make:demo", kind="make_target", display_name="Standalone Demo", description="Run the demo.", command=("make", "demo"), tags=("demo",), ) catalog = Catalog(items=(suite, leaf)) selected_prompts: list[str] = [] monkeypatch.setattr(interactive, "_choose_category", lambda: "demo") def _mock_select_item(items, *, prompt: str, allow_back: bool = False): _ = (items, allow_back) selected_prompts.append(prompt) if prompt == "Choose a test or suite:" and len(selected_prompts) == 1: return suite if prompt == "Select a scenario from Demo Suite:": raise interactive._GoBack return leaf def _mock_select_item_or_all(items, *, prompt: str, allow_back: bool = False): return _mock_select_item(items, prompt=prompt, allow_back=allow_back) monkeypatch.setattr(interactive, "_select_item", _mock_select_item) monkeypatch.setattr(interactive, "_select_item_or_all", _mock_select_item_or_all) item, _ = interactive.choose_interactive_item(catalog) assert item.id == "make:demo" assert selected_prompts == [ "Choose a test or suite:", "Select a scenario from Demo Suite:", "Choose a test or suite:", ] def test_select_item_or_all_allows_literal_run_all_item_id(monkeypatch) -> None: item = CatalogItem( id="__run_all__", kind="make_target", display_name="Literal Run All Test", description="A test whose id matches the old sentinel string.", command=("make", "test-cov"), tags=("ci-safe",), ) def _mock_select_prompt(*_args, **_kwargs): return SimpleNamespace(ask=lambda: "__run_all__") monkeypatch.setattr(interactive, "_require_interactive_dependencies", lambda: None) monkeypatch.setattr( interactive, "_QuestionaryChoice", lambda *, title, value: {"title": title, "value": value}, ) monkeypatch.setattr(interactive, "_select_prompt", _mock_select_prompt) selection = interactive._select_item_or_all([item], prompt="Choose a test or suite:") assert selection is item def test_choose_interactive_item_expands_run_all_suites(monkeypatch) -> None: suite = CatalogItem( id="suite:demo", kind="suite", display_name="Demo Suite", description="A grouped demo suite.", tags=("demo",), children=( CatalogItem( id="scenario:demo:first", kind="scenario", display_name="First scenario", description="First child.", command=("make", "demo-first"), tags=("demo",), ), CatalogItem( id="scenario:demo:second", kind="scenario", display_name="Second scenario", description="Second child.", command=("make", "demo-second"), tags=("demo",), ), ), ) leaf = CatalogItem( id="make:demo", kind="make_target", display_name="Standalone Demo", description="Run the demo.", command=("make", "demo"), tags=("demo",), ) catalog = Catalog(items=(suite, leaf)) def _mock_select_item_or_all(items, *, prompt: str, allow_back: bool = False): _ = (prompt, allow_back) return items monkeypatch.setattr(interactive, "_choose_category", lambda: "demo") monkeypatch.setattr(interactive, "_select_item_or_all", _mock_select_item_or_all) selection, auto_selected = interactive.choose_interactive_item(catalog) assert auto_selected is False assert [item.id for item in selection] == [ "scenario:demo:first", "scenario:demo:second", "make:demo", ] def test_run_interactive_picker_returns_zero_on_escape(monkeypatch) -> None: catalog = load_test_catalog() monkeypatch.setattr(interactive, "_require_interactive_dependencies", lambda: None) monkeypatch.setattr(interactive.sys, "stdin", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr(interactive.sys, "stdout", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr( interactive, "choose_interactive_item", lambda _catalog: (_ for _ in ()).throw(KeyboardInterrupt()), ) assert interactive.run_interactive_picker(catalog) == 0 def test_run_interactive_picker_returns_to_selection_after_escape_from_confirm(monkeypatch) -> None: first = CatalogItem( id="make:test-cov", kind="make_target", display_name="Coverage Suite", description="Run coverage.", command=("make", "test-cov"), tags=("ci-safe",), ) second = CatalogItem( id="make:test-full", kind="make_target", display_name="Full Suite", description="Run full tests.", command=("make", "test-full"), tags=("ci-safe",), ) selections = iter([(first, False), (second, False)]) monkeypatch.setattr(interactive, "_require_interactive_dependencies", lambda: None) monkeypatch.setattr(interactive.sys, "stdin", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr(interactive.sys, "stdout", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr(interactive, "choose_interactive_item", lambda _catalog: next(selections)) confirm_calls = 0 def _mock_confirm(item): _ = item nonlocal confirm_calls confirm_calls += 1 if confirm_calls == 1: raise interactive._GoBack return True monkeypatch.setattr(interactive, "_confirm_run", _mock_confirm) monkeypatch.setattr( interactive, "run_catalog_item", lambda item: 7 if item.id == "make:test-full" else 1 ) assert interactive.run_interactive_picker(Catalog(items=(first, second))) == 7 def test_run_interactive_picker_writes_selection_for_background_mode(monkeypatch, tmp_path) -> None: item = CatalogItem( id="synthetic:001-replication-lag", kind="cli_command", display_name="001-replication-lag", description="Run synthetic scenario.", command=("opensre", "tests", "synthetic", "--scenario", "001-replication-lag"), tags=("synthetic",), ) selection_file = tmp_path / "selection.json" monkeypatch.setenv("OPENSRE_TEST_PICKER_SELECTION_FILE", str(selection_file)) monkeypatch.setattr(interactive, "_require_interactive_dependencies", lambda: None) monkeypatch.setattr(interactive.sys, "stdin", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr(interactive.sys, "stdout", SimpleNamespace(isatty=lambda: True)) monkeypatch.setattr(interactive, "choose_interactive_item", lambda _catalog: (item, False)) monkeypatch.setattr(interactive, "_confirm_run", lambda _item: True) monkeypatch.setattr( interactive, "run_catalog_item", lambda _item: (_ for _ in ()).throw(AssertionError("should not run inline")), ) assert interactive.run_interactive_picker(Catalog(items=(item,))) == 0 assert json.loads(selection_file.read_text(encoding="utf-8")) == [ { "id": "synthetic:001-replication-lag", "display_name": "001-replication-lag", "command": ["opensre", "tests", "synthetic", "--scenario", "001-replication-lag"], "command_display": "opensre tests synthetic --scenario 001-replication-lag", } ] def test_run_catalog_items_skips_non_runnable_items() -> None: runnable = CatalogItem( id="make:test-cov", kind="make_target", display_name="Coverage Suite", description="Run coverage.", command=("make", "test-cov"), tags=("ci-safe",), ) suite = CatalogItem( id="suite:ci-safe", kind="suite", display_name="Grouped CI Tests", description="A suite without a direct command.", tags=("ci-safe",), children=(runnable,), ) assert interactive.run_catalog_items([suite, runnable], dry_run=True) == 0 def test_confirm_run_prints_openclaw_preflight_messages(monkeypatch, capsys) -> None: item = CatalogItem( id="rca:openclaw_gateway_crashed", kind="rca_file", display_name="OpenClaw Gateway Crashed", description="Run a bundled markdown RCA alert fixture.", command=("make", "test-rca", "FILE=openclaw_gateway_crashed"), tags=("rca", "fixture", "openclaw"), ) monkeypatch.setattr( interactive, "get_preflight_messages", lambda _item: ("OpenClaw preflight: unavailable.", "Fix: verify openclaw."), ) monkeypatch.setattr(interactive, "_QuestionaryChoice", lambda *, title, value: (title, value)) monkeypatch.setattr( interactive, "_select_prompt", lambda *_args, **_kwargs: SimpleNamespace(ask=lambda: True), ) assert interactive._confirm_run(item) is True output = capsys.readouterr().out assert "OpenClaw preflight: unavailable." in output assert "Fix: verify openclaw." in output