"""Drift + behaviour guard for the Windows/no-make quickstart runner. ``tasks.py`` + ``make.bat`` exist so the README quickstart runs on native Windows without GNU make (ticket #000055). Two thin entry points are a known DRY cost; this test pins them: - the supported-target set is frozen here, so adding/removing a quickstart target without updating this contract fails loudly; - venv-layout + Python-launcher detection is verified for *both* ``os.name`` values (the host only exercises one at runtime); - the make-style ``KEY=VALUE`` parser is verified for every shell quoting form (POSIX strips quotes; cmd/PowerShell do not). No network, no subprocess, no venv — pure import + function checks. """ from __future__ import annotations import importlib.util from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[1] def _load(): """Fresh import of tasks.py with cleared module-level CLI state.""" spec = importlib.util.spec_from_file_location("arborist_tasks", REPO / "tasks.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) mod._CLI_VARS.clear() mod._POSITIONALS.clear() return mod # The frozen contract. Mirrors the README Quickstart + "After the answer" # + Setup blocks and the Makefile quickstart subset. Changing the runner's # surface MUST change this set in the same commit. EXPECTED_TARGETS = { "bootstrap", "fetch-cur", "ingest-cur-attached", "distill-shards-parallel", "distill-shards-tfidf-parallel", "query", "query-dry", "inspect", "falsify", "burn", "burn-kindergarten", "bootstrap-crawler", "crawl-ingest", "recrawl-check", "stats", "stats-shards", "verify", "search", "test", "clean", "clean-db", "clean-data", } def test_supported_target_set_is_frozen(): mod = _load() assert set(mod.TARGETS) == EXPECTED_TARGETS # Every target is callable and self-documents (help relies on __doc__). for name, fn in mod.TARGETS.items(): assert callable(fn), name assert (fn.__doc__ or "").strip(), f"{name} has no help docstring" def test_help_lists_every_target(capsys): mod = _load() mod.main(["tasks.py", "help"]) out = capsys.readouterr().out for name in EXPECTED_TARGETS | {"help"}: assert f" {name}" in out, name def test_unknown_target_exits_2(capsys): mod = _load() with pytest.raises(SystemExit) as exc: mod.main(["tasks.py", "definitely-not-a-target"]) assert exc.value.code == 2 captured = capsys.readouterr() assert "unknown target" in captured.err # diagnostics on stderr assert "show this help" in captured.out # help still printed @pytest.mark.parametrize( "is_windows,bin_name,arborist_name,py_name", [ (False, "bin", "arborist", "python"), (True, "Scripts", "arborist.exe", "python.exe"), ], ) def test_venv_layout_per_platform(monkeypatch, is_windows, bin_name, arborist_name, py_name): mod = _load() monkeypatch.setattr(mod, "IS_WINDOWS", is_windows) assert mod.venv_bin_dir().name == bin_name assert mod.arborist_exe().name == arborist_name assert mod.venv_python().name == py_name assert mod.venv_exe("pytest").name == ("pytest.exe" if is_windows else "pytest") def test_windows_prefers_py_launcher(monkeypatch): mod = _load() monkeypatch.setattr(mod, "IS_WINDOWS", True) monkeypatch.delenv("ARBORIST_PYTHON", raising=False) monkeypatch.setattr(mod.shutil, "which", lambda name: f"/x/{name}" if name == "py" else None) assert mod.base_python_cmd() == ["py", "-3"] def test_posix_uses_python3(monkeypatch): mod = _load() monkeypatch.setattr(mod, "IS_WINDOWS", False) monkeypatch.delenv("ARBORIST_PYTHON", raising=False) monkeypatch.setattr(mod.shutil, "which", lambda name: f"/usr/bin/{name}") assert mod.base_python_cmd() == ["python3"] def test_arborist_python_override(monkeypatch): mod = _load() monkeypatch.setenv("ARBORIST_PYTHON", "py -3.12") assert mod.base_python_cmd() == ["py", "-3.12"] @pytest.mark.parametrize( "argv,expect_q", [ # POSIX: the shell already stripped the quotes. (["query", "Q=What is X?"], "What is X?"), # cmd: KEY="value with spaces" — quotes forwarded literally. (["query", 'Q="What is X?"'], "What is X?"), # cmd-friendly whole-token form. (["query", '"Q=What is X?"'], "What is X?"), ], ) def test_make_style_value_parsing(argv, expect_q): mod = _load() # Stop before executing the target — just parse args. mod.TARGETS["query"] = lambda: None mod.main(["tasks.py", *argv]) assert mod._CLI_VARS["Q"] == expect_q def test_positional_question_and_flags(): mod = _load() mod.TARGETS["query"] = lambda: None mod.main(["tasks.py", "query", "What is X?", "JSON=1", "ANSWER_MODE=quote"]) assert mod._POSITIONALS == ["What is X?"] assert mod._CLI_VARS["JSON"] == "1" and mod.truthy("JSON") assert mod._CLI_VARS["ANSWER_MODE"] == "quote" assert not mod.truthy("BURN") # unset → falsey, like make def test_crawl_shard_name_matches_makefile_sed(): mod = _load() mod._CLI_VARS["URL"] = "https://russell.ballestrini.net/blog/x?y=1" # Makefile: sed 's,^https?://([^/]+).*,\1,' | tr '.' '_' assert mod._crawl_shard_path().name == "crawl_russell_ballestrini_net.db" def test_shim_and_attributes_present(): assert (REPO / "make.bat").is_file() text = (REPO / "make.bat").read_text() assert "tasks.py" in text and "py -3" in text ga = (REPO / ".gitattributes").read_text() assert "*.bat text eol=crlf" in ga