arborist/tests/test_tasks_runner.py
russell@unturf.com 2c98fc964e
feat: cross-language Q&A (Operation Sandwich) + Windows quickstart — all default-OFF
Three workstreams, full suite 2482 passed, experimental paths default-OFF.

#000055 — Windows quickstart without make
  tasks.py (pure-stdlib runner) + make.bat shim + .gitattributes;
  README Windows section rewritten. Quickstart needs only Python
  3.10+ (no make/bzip2/curl/bash). Mirrors the Makefile quickstart
  subset; drift-pinned by tests/test_tasks_runner.py.

#000001 §7 Phase 0 — deterministic cross-language guard
  arborist/qa/crosslang.py: non-English signal (¿/¡/non-ASCII) + an
  es function-word stoppack. Fail-closed to UNGROUNDED before
  retrieval/LLM (mirrors the quantifier reject-DAG) when no content
  token survives, else strips es stopwords from the retrieval query
  only. English path byte-identical by construction. Default OFF
  (crosslang_guard_enabled). Measured: the anarcocapitalismo field
  case 10.4s -> 1.6s.

#000056 — Operation Sandwich (cross-language grounding)
  arborist/qa/mt/: opus-mt es/fr/ru<->en, lazy per-pair memoised
  singleton (fixes the 88%-engine-error concurrency defect),
  manifest-pinned, [mt] extra; entity_mask wrapper. Sandwich =
  translate query in (retrieval + LLM prompt) -> English answer ->
  UNTOUCHED verifier grounds English-vs-English -> translate the
  verified answer out as display-only (banner-labelled, zero
  grounding). question_hash + verifier_policy_hash invariant; MT
  engine identity binds into RetrievalPlan, not governance. CLI
  --crosslang-translate / make XLANG_MT=1. Default OFF; entity_mask
  default OFF (measured net-negative at bench scale). Fan-out bench
  (bench/*.py): Spanish ~0% -> 71% grounded vs the real no-support
  baseline; the round-trip predictor was tried and refuted; the
  entity-mask lever failed at scale (corpus-title anchoring untried).

CLAUDE.md: cross-language bright-line convention + module map.
Pre-existing modified diagram files are intentionally excluded.
2026-05-18 12:12:23 -04:00

172 lines
5.6 KiB
Python

"""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