| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- # /// script
- # requires-python = ">=3.10"
- # dependencies = ["pytest>=8.0"]
- # ///
- """Tests for brain.py. Run: uv run -m pytest scripts/tests/test_brain.py"""
- import sys
- from pathlib import Path
- import pytest
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
- import brain # noqa: E402
- CSV = """category,technique_name,description,detail
- collaborative,Yes And Building,Build on every idea with "yes and" to keep momentum,
- wild,Quantum Superposition,Hold contradictory ideas as simultaneously true,techniques/quantum.md
- structured,SCAMPER Method,Run the idea through seven transformation lenses,
- wild,Anti-Solution,Brainstorm how to make the problem worse then invert,
- """
- DETAIL = "# Quantum Superposition\nFull multi-step instructions for the complex technique."
- @pytest.fixture
- def lib(tmp_path):
- csv_path = tmp_path / "brain-methods.csv"
- csv_path.write_text(CSV, encoding="utf-8")
- (tmp_path / "techniques").mkdir()
- (tmp_path / "techniques" / "quantum.md").write_text(DETAIL, encoding="utf-8")
- return csv_path
- def test_load_normalizes_detail(lib):
- rows = brain.load(lib)
- assert len(rows) == 4
- assert rows[0]["detail"] == ""
- assert rows[1]["detail"] == "techniques/quantum.md"
- def test_categories_counts_sorted(lib):
- assert brain.categories(brain.load(lib)) == [("collaborative", 1), ("structured", 1), ("wild", 2)]
- def test_filter_is_case_insensitive(lib):
- rows = brain.filter_cats(brain.load(lib), ["WILD"])
- assert {r["technique_name"] for r in rows} == {"Quantum Superposition", "Anti-Solution"}
- def test_filter_none_returns_all(lib):
- assert len(brain.filter_cats(brain.load(lib), None)) == 4
- def test_find_hits_and_misses(lib):
- found, missing = brain.find(brain.load(lib), ["scamper method", "Nope"])
- assert [r["technique_name"] for r in found] == ["SCAMPER Method"]
- assert missing == ["Nope"]
- def test_resolve_detail_present(lib):
- row = next(r for r in brain.load(lib) if r["detail"])
- assert "multi-step instructions" in brain.resolve_detail(row, lib.parent)
- def test_resolve_detail_absent_is_none(lib):
- row = next(r for r in brain.load(lib) if not r["detail"])
- assert brain.resolve_detail(row, lib.parent) is None
- def test_resolve_detail_missing_file_warns_not_fatal(lib, capsys):
- rows = brain.load(lib)
- rows[1]["detail"] = "techniques/gone.md"
- assert brain.resolve_detail(rows[1], lib.parent) is None
- assert "not found" in capsys.readouterr().err
- def test_show_inlines_detail(lib, capsys):
- assert brain.main(["--file", str(lib), "show", "Quantum Superposition"]) == 0
- out = capsys.readouterr().out
- assert "multi-step instructions" in out and "[wild]" in out
- def test_show_simple_has_no_detail(lib, capsys):
- brain.main(["--file", str(lib), "show", "SCAMPER Method"])
- out = capsys.readouterr().out
- assert "transformation lenses" in out
- def test_show_all_missing_returns_1(lib):
- assert brain.main(["--file", str(lib), "show", "Ghost"]) == 1
- def test_list_filtered_text(lib, capsys):
- brain.main(["--file", str(lib), "list", "--category", "structured"])
- out = capsys.readouterr().out.strip().splitlines()
- assert len(out) == 1 and out[0].startswith("structured\tSCAMPER Method\t")
- def test_list_bare_is_refused(lib, capsys):
- # the footgun: bare `list` must NOT dump the catalog into context
- assert brain.main(["--file", str(lib), "list"]) == 2
- captured = capsys.readouterr()
- assert captured.out == "" # nothing leaked to stdout
- assert "--category" in captured.err and "--all" in captured.err
- def test_list_all_dumps_everything(lib, capsys):
- assert brain.main(["--file", str(lib), "list", "--all"]) == 0
- out = capsys.readouterr().out.strip().splitlines()
- assert len(out) == 4 # the deliberate full-catalog escape hatch
- def test_json_output(lib, capsys):
- import json
- brain.main(["--file", str(lib), "--json", "categories"])
- data = json.loads(capsys.readouterr().out)
- assert {"category": "wild", "count": 2} in data
- def test_random_respects_n_and_category(lib, capsys):
- brain.main(["--file", str(lib), "random", "--category", "wild", "-n", "5"])
- lines = capsys.readouterr().out.strip().splitlines()
- assert len(lines) == 2 # only 2 wild exist, n capped
- assert all(line.startswith("wild\t") for line in lines)
- def test_random_negative_n_does_not_crash(lib, capsys):
- # a negative -n is clamped to 0, not passed to random.sample (which would raise)
- assert brain.main(["--file", str(lib), "random", "-n", "-1"]) == 0
- assert capsys.readouterr().out.strip() == ""
- def test_missing_file_returns_2(tmp_path):
- assert brain.main(["--file", str(tmp_path / "nope.csv"), "categories"]) == 2
- # --- html selection page ------------------------------------------------
- def test_html_requires_out(lib, capsys):
- # never dump the catalog to stdout — writing to a file is the whole point
- assert brain.main(["--file", str(lib), "html"]) == 2
- assert "--out" in capsys.readouterr().err
- def test_html_writes_selection_page(lib, tmp_path):
- out = tmp_path / "sel.html"
- assert brain.main(["--file", str(lib), "html", "--out", str(out)]) == 0
- doc = out.read_text(encoding="utf-8")
- assert doc.startswith("<!DOCTYPE html>")
- assert "BMad Method Brainstorming Selection" in doc
- for r in brain.load(lib):
- assert r["technique_name"] in doc # every technique is selectable
- assert ""yes and"" in doc # quotes in a description are escaped, not raw
- def test_html_creates_missing_parent(lib, tmp_path):
- out = tmp_path / "nested" / "deep" / "sel.html"
- assert brain.main(["--file", str(lib), "html", "--out", str(out)]) == 0
- assert out.is_file()
- # --- --extra overlay (customize.toml additional_techniques) -------------
- EXTRA = (
- '[{"category": "domain-specific", "technique_name": "Regulatory Inversion", '
- '"description": "Start from the compliance constraint and brainstorm what it unlocks."}, '
- '{"category": "wild", "technique_name": "Extra Wild One", "description": "An added wild method."}]'
- )
- @pytest.fixture
- def extra(tmp_path):
- p = tmp_path / "extra.json"
- p.write_text(EXTRA, encoding="utf-8")
- return p
- def test_extra_merges_into_categories(lib, extra, capsys):
- brain.main(["--file", str(lib), "--extra", str(extra), "categories"])
- out = capsys.readouterr().out
- assert "domain-specific\t1" in out # a brand-new category appears
- assert "wild\t3" in out # the extra wild one is counted alongside the shipped two
- def test_extra_appears_in_list_and_random(lib, extra, capsys):
- brain.main(["--file", str(lib), "--extra", str(extra), "list", "--category", "domain-specific"])
- assert "Regulatory Inversion" in capsys.readouterr().out
- def test_extra_is_first_class_in_html(lib, extra, tmp_path):
- out = tmp_path / "sel.html"
- assert brain.main(["--file", str(lib), "--extra", str(extra), "html", "--out", str(out)]) == 0
- doc = out.read_text(encoding="utf-8")
- # custom technique is selectable and its new category renders without crashing (fallback glyph/hue)
- assert "Regulatory Inversion" in doc
- assert "Domain Specific" in doc
- def test_extra_missing_file_returns_2(lib, tmp_path):
- assert brain.main(["--file", str(lib), "--extra", str(tmp_path / "nope.json"), "categories"]) == 2
- def test_unknown_category_style_uses_fallback_glyph():
- hue, glyph = brain.category_style("totally-made-up-category")
- assert hue.startswith("#") and len(hue) == 7 # valid derived hex
- assert glyph == brain._FALLBACK_GLYPH
- def test_shipped_selector_is_in_sync_with_catalog():
- # foolproofing: if someone edits brain-methods.csv they must regenerate the page.
- # Regenerate with: uv run brain.py html --out assets/brain-selector.html
- asset = brain.DEFAULT_FILE.parent / "brain-selector.html"
- assert asset.is_file(), "missing assets/brain-selector.html — generate it"
- expected = brain.html_doc(brain.load(brain.DEFAULT_FILE))
- assert asset.read_text(encoding="utf-8") == expected, (
- "assets/brain-selector.html is stale; regenerate: "
- "uv run brain.py html --out assets/brain-selector.html"
- )
|