3155518305
8 new importable-function tests in tests/test_aggregate_directives.py: - test_importable_function_returns_clean_body - test_importable_function_no_meta_md_pollution - test_importable_function_max_chars_truncates_with_suffix - test_importable_function_max_chars_zero_is_unlimited - test_importable_function_max_chars_above_size_is_unlimited - test_importable_function_missing_preset_raises - test_importable_function_missing_v1_in_preset_raises - test_importable_function_accepts_relative_preset_path 7 new MCP-server tests in tests/test_mcp_aggregate_directives.py: - test_mcp_server_exposes_aggregate_directives_spec - test_mcp_server_impl_default_preset - test_mcp_server_impl_explicit_preset_path - test_mcp_server_impl_max_chars_truncates - test_mcp_server_impl_missing_preset_returns_error_string - test_mcp_server_impl_zero_max_chars_is_unlimited - test_mcp_server_list_tools_includes_aggregate_directives Tests load modules via importlib.util.spec_from_file_location so the scripts/mcp_server.py main guard is not triggered during testing. sandbox-policy compliance: all write paths use tmp_path. 22 tests pass (15 aggregate_directives + 7 MCP); 7 pre-existing CLI tests still pass byte-identical.
173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
"""Tests for scripts/aggregate_directives.py.
|
|
|
|
These tests exercise the script via subprocess to verify the contract:
|
|
- reads v1.md files only (never meta.md)
|
|
- emits clean bodies separated by '====' header
|
|
- exits 1 on missing files
|
|
- emits to stdout by default; -o writes to file
|
|
|
|
The importable aggregate_directives() function tests cover the API surface used
|
|
by the MCP server (scripts/mcp_server.py:aggregate_directives tool).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SCRIPT = REPO_ROOT / "scripts" / "aggregate_directives.py"
|
|
PRESET = REPO_ROOT / "conductor" / "directives" / "presets" / "current_baseline.md"
|
|
|
|
|
|
def _run(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess:
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *args],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
cwd=cwd or REPO_ROOT,
|
|
)
|
|
|
|
|
|
def test_runs_against_current_baseline() -> None:
|
|
result = _run([str(PRESET)])
|
|
assert result.returncode == 0, "script should succeed; stderr=" + result.stderr
|
|
body = result.stdout
|
|
assert "atomic_per_task_commits" in body
|
|
assert "ban_dict_any" in body
|
|
assert "knowledge_harvest_pattern" in body
|
|
assert body.count("========================================") >= 51 * 2
|
|
|
|
|
|
def test_meta_md_pollution_fix() -> None:
|
|
result = _run([str(PRESET)])
|
|
body = result.stdout
|
|
assert "**Why this iteration:**" not in body
|
|
assert "**Lifted:**" not in body
|
|
|
|
|
|
def test_missing_file_exits_nonzero(tmp_path: Path) -> None:
|
|
bad = tmp_path / "bad_preset.md"
|
|
bad.write_text("- missing_thing: conductor/directives/missing_thing/v1.md\n", encoding="utf-8")
|
|
result = _run([str(bad)])
|
|
assert result.returncode != 0
|
|
assert "missing_thing" in result.stderr
|
|
|
|
|
|
def test_output_flag_writes_to_file(tmp_path: Path) -> None:
|
|
out = tmp_path / "out.md"
|
|
result = _run([str(PRESET), "-o", str(out)])
|
|
assert result.returncode == 0
|
|
assert out.exists()
|
|
content = out.read_text(encoding="utf-8")
|
|
assert "ban_dict_any" in content
|
|
assert content.count("========================================") >= 51 * 2
|
|
|
|
|
|
def test_no_meta_md_reads() -> None:
|
|
result = _run([str(PRESET)])
|
|
body = result.stdout
|
|
assert "Provenance" not in body
|
|
assert "## v1" not in body
|
|
assert "# ban_dict_any" not in body
|
|
|
|
|
|
DIRECTIVES_DIR = REPO_ROOT / "conductor" / "directives"
|
|
|
|
|
|
def _iter_v1_files() -> list[Path]:
|
|
return sorted(DIRECTIVES_DIR.glob("*/v1.md"))
|
|
|
|
|
|
def test_every_v1_has_top_level_heading() -> None:
|
|
v1_files = _iter_v1_files()
|
|
assert len(v1_files) >= 66, "expected at least 66 directive v1.md files; found " + str(len(v1_files))
|
|
missing: list[str] = []
|
|
for path in v1_files:
|
|
first_line = path.read_text(encoding="utf-8").splitlines()[0] if path.read_text(encoding="utf-8") else ""
|
|
if not first_line.startswith("# "):
|
|
missing.append(path.parent.name)
|
|
assert not missing, "v1.md files missing a top-level '# ' rule-statement heading: " + ", ".join(missing)
|
|
|
|
|
|
def test_v1_heading_is_not_meta_provenance_format() -> None:
|
|
for path in _iter_v1_files():
|
|
first_line = path.read_text(encoding="utf-8").splitlines()[0]
|
|
assert not first_line.strip().endswith("- v1"), path.parent.name + " uses the meta.md provenance heading format in v1.md body"
|
|
|
|
|
|
def _import_aggregate_directives_module():
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("aggregate_directives_under_test", SCRIPT)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError("failed to build import spec for " + str(SCRIPT))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def test_importable_function_returns_clean_body() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
body = mod.aggregate_directives(str(PRESET))
|
|
assert "atomic_per_task_commits" in body
|
|
assert "ban_dict_any" in body
|
|
assert "knowledge_harvest_pattern" in body
|
|
assert body.count("========================================") >= 51 * 2
|
|
|
|
|
|
def test_importable_function_no_meta_md_pollution() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
body = mod.aggregate_directives(str(PRESET))
|
|
assert "**Why this iteration:**" not in body
|
|
assert "**Source:**" not in body
|
|
assert "**Lifted:**" not in body
|
|
assert "## v1" not in body
|
|
assert "Provenance" not in body
|
|
|
|
|
|
def test_importable_function_max_chars_truncates_with_suffix() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
body = mod.aggregate_directives(str(PRESET), max_chars=2000)
|
|
assert len(body) > 2000, "truncated body must include suffix text exceeding the cap"
|
|
assert "truncated" in body.lower()
|
|
|
|
|
|
def test_importable_function_max_chars_zero_is_unlimited() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
full = mod.aggregate_directives(str(PRESET))
|
|
same = mod.aggregate_directives(str(PRESET), max_chars=0)
|
|
assert same == full
|
|
|
|
|
|
def test_importable_function_max_chars_above_size_is_unlimited() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
full = mod.aggregate_directives(str(PRESET))
|
|
capped = mod.aggregate_directives(str(PRESET), max_chars=len(full) + 5000)
|
|
assert capped == full
|
|
|
|
|
|
def test_importable_function_missing_preset_raises() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
bad = REPO_ROOT / "conductor" / "directives" / "presets" / "_nonexistent_preset_xyz.md"
|
|
with pytest.raises(FileNotFoundError) as exc_info:
|
|
mod.aggregate_directives(str(bad))
|
|
assert "preset file not found" in str(exc_info.value)
|
|
|
|
|
|
def test_importable_function_missing_v1_in_preset_raises(tmp_path: Path) -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
bad = tmp_path / "bad_preset.md"
|
|
bad.write_text("- missing_v1_xyz: conductor/directives/missing_v1_xyz/v1.md\n", encoding="utf-8")
|
|
with pytest.raises(FileNotFoundError) as exc_info:
|
|
mod.aggregate_directives(str(bad))
|
|
assert "missing_v1_xyz" in str(exc_info.value)
|
|
|
|
|
|
def test_importable_function_accepts_relative_preset_path() -> None:
|
|
mod = _import_aggregate_directives_module()
|
|
rel = "conductor/directives/presets/current_baseline.md"
|
|
body = mod.aggregate_directives(rel, project_root=str(REPO_ROOT))
|
|
assert "ban_dict_any" in body |