feat(mindmodel): add CLI wrapper, edge-case tests, and manifest schema tests

main
Sven Geboers 1 month ago
parent ed289ff582
commit d1faf2b3e4
  1. 56
      scripts/validate_mindmodel.py
  2. 29
      tests/mindmodel/test_manifest_parse.py
  3. 32
      tests/mindmodel/test_manifest_schema.py
  4. 52
      tests/scripts/test_validate_cli.py
  5. 56
      tests/validators/test_validator_edgecases.py

@ -0,0 +1,56 @@
"""Command-line wrapper around src.validators.mindmodel_validator.validate_manifest
This tiny CLI loads a manifest and writes a structured JSON report to stdout
and optionally to a file path. It is report-only: it never raises an error or
changes exit code based on findings.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any
def _write_report(report: dict[str, Any], path: Path | None) -> None:
text = json.dumps(report, indent=2, ensure_ascii=False)
print(text)
if path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser("validate_mindmodel")
parser.add_argument("manifest", nargs="?", help="path to manifest file")
parser.add_argument("--manifest", dest="manifest_opt", help="path to manifest file")
parser.add_argument("--report", help="optional output report path")
args = parser.parse_args(argv)
manifest = args.manifest_opt or args.manifest
if not manifest:
parser.error("manifest path is required (positional or --manifest)")
# import here to keep CLI tiny when unused
try:
from src.validators.mindmodel_validator import validate_manifest
except Exception as e: # pragma: no cover - defensive
print(f"Failed to import validator: {e}")
return 0
try:
report = validate_manifest(manifest, report_only=True)
except Exception as e: # never fail the process
report = {"error": str(e)}
report_path = Path(args.report) if args.report else None
_write_report(report, report_path)
# always exit zero for report-only operation
return 0
if __name__ == "__main__":
raise SystemExit(main())

@ -0,0 +1,29 @@
import re
from pathlib import Path
try:
import yaml # type: ignore
except Exception:
yaml = None
def test_manifest_loads():
"""Ensure the .mindmodel/manifest.yaml can be read and contains a 'files' list."""
p = Path(".mindmodel/manifest.yaml")
assert p.exists(), ".mindmodel/manifest.yaml must exist"
text = p.read_text(encoding="utf-8")
if yaml is not None:
data = yaml.safe_load(text)
assert isinstance(data, dict), "manifest should parse to a mapping"
assert "files" in data, "top-level 'files' key missing"
assert isinstance(data["files"], list), "'files' should be a list"
assert len(data["files"]) >= 1, "'files' must have at least one entry"
else:
# Fallback simple checks if PyYAML is not available in the environment.
assert re.search(r"^\s*files:\s*$", text, re.M), (
"manifest must contain top-level 'files:'"
)
assert re.search(r"^\s*-\s+path:\s+", text, re.M), (
"manifest must contain at least one '- path:' entry"
)

@ -0,0 +1,32 @@
from pathlib import Path
from src.validators.types import parse_manifest
def test_manifest_schema_parses_into_types():
"""Ensure the .mindmodel/manifest.yaml parses via parse_manifest and
yields a manifest-like object with a files list where each entry has a
`path` key.
The test relies on parse_manifest to use its PyYAML fallback when
PyYAML is not available in the test environment.
"""
p = Path(".mindmodel/manifest.yaml")
assert p.exists(), ".mindmodel/manifest.yaml must exist"
manifest = parse_manifest(str(p))
# Accept either a plain mapping or the Manifest dataclass returned by
# parse_manifest. Normalize to the files list for assertions.
if isinstance(manifest, dict):
files = manifest.get("files", [])
else:
# Manifest dataclass has .files attribute
files = getattr(manifest, "files", [])
assert isinstance(files, list), "manifest.files must be a list"
assert files, "manifest must contain at least one file entry"
for entry in files:
assert isinstance(entry, dict), "each file entry should be a mapping"
assert "path" in entry, f"file entry missing 'path': {entry}"

@ -0,0 +1,52 @@
import json
import subprocess
import sys
from pathlib import Path
def test_cli_runs(tmp_path):
manifest = Path(".mindmodel/manifest.yaml")
assert manifest.exists(), "expected .mindmodel/manifest.yaml to exist in repo"
report_path = tmp_path / "report.json"
# Try module mode first, fallback to direct script invocation
cmds = [
[
sys.executable,
"-m",
"scripts.validate_mindmodel",
str(manifest),
"--report",
str(report_path),
],
[
sys.executable,
"scripts/validate_mindmodel.py",
str(manifest),
"--report",
str(report_path),
],
]
result = None
for cmd in cmds:
try:
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
# if process ran (any exit code), break and use this result
break
except FileNotFoundError:
continue
assert result is not None, "Failed to run script (no suitable invocation)"
# CLI should exit with 0 (report-only)
assert result.returncode == 0, (
f"CLI exited non-zero: {result.returncode}\nstderr: {result.stderr}"
)
assert report_path.exists(), f"Report file was not created at {report_path}"
data = json.loads(report_path.read_text(encoding="utf-8"))
# top-level keys expected from validator
for key in ("missing_files", "truncated_evidence", "potential_secrets"):
assert key in data, f"Report JSON missing key: {key}"

@ -0,0 +1,56 @@
import os
from pathlib import Path
from src.validators.mindmodel_validator import validate_manifest
def test_missing_files_reported(tmp_path):
# create two paths that do not exist
p1 = str(tmp_path / "missing_one.txt")
p2 = str(tmp_path / "missing_two.txt")
manifest = f"""
files:
- path: {p1}
- path: {p2}
"""
mpath = tmp_path / "manifest_missing.yaml"
mpath.write_text(manifest, encoding="utf-8")
report = validate_manifest(str(mpath))
assert "missing_files" in report
# both missing paths should be reported
assert p1 in report["missing_files"]
assert p2 in report["missing_files"]
def test_truncated_evidence_and_secrets_reported(tmp_path):
# entry with truncated evidence (ends with ...)
trunc_path = str(tmp_path / "trunc.txt")
trunc_evidence = "This output was cut off..."
# entry with potential secret (contains PASSWORD)
secret_path = str(tmp_path / "secret.txt")
secret_evidence = "Found PASSWORD=sekret123 in the logs"
manifest = f"""
files:
- path: {trunc_path}
evidence_excerpt: "{trunc_evidence}"
- path: {secret_path}
evidence_excerpt: "{secret_evidence}"
"""
mpath = tmp_path / "manifest_edgecases.yaml"
mpath.write_text(manifest, encoding="utf-8")
report = validate_manifest(str(mpath))
# truncated evidence should report the trunc_path
assert "truncated_evidence" in report
assert any(item.get("path") == trunc_path for item in report["truncated_evidence"])
# potential secrets should report the secret_path
assert "potential_secrets" in report
assert any(item.get("path") == secret_path for item in report["potential_secrets"])
Loading…
Cancel
Save