Files
magnus919_agent-skills/community-guides/scripts/test_validate_guide.py
T
Magnus HedemarkandGitHub 11da723627 feat(skill): add community-guides with evaluated participatory workflows (#515)
* feat(skill): add community-guides method and eval fixtures

* fix(community-guides): bound intake and preserve source limits

* fix(community-guides): scale narrow revisions to requested artifacts

* fix(community-guides): verify worksheet references in host notes

* test(community-guides): strengthen substantive revision assertions

* Tighten source-only revision preflight and scope evals

* Add evidence-backed multilingual revision preflight

* Retain community-guides behavioral evidence and evaluation limits

* Fix isolated checker test loading for dependency validation
2026-09-15 13:02:39 -04:00

268 lines
10 KiB
Python

#!/usr/bin/env python3
"""Tests for the read-only community guide plan checker."""
from __future__ import annotations
import importlib.util
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).with_name("validate_guide.py")
SPEC = importlib.util.spec_from_file_location("community_guide_validator", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
validate_guide = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(validate_guide)
class ValidateGuideTests(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
self.root = Path(self.tempdir.name)
(self.root / "worksheets").mkdir()
(self.root / "worksheets" / "intro.md").write_text("intro", encoding="utf-8")
(self.root / "participant.md").write_text("participant", encoding="utf-8")
(self.root / "facilitator.md").write_text("facilitator", encoding="utf-8")
self.plan_path = self.root / "plan.json"
def tearDown(self) -> None:
self.tempdir.cleanup()
def plan(self) -> dict[str, object]:
return {
"schema_version": 1,
"title": "A useful guide",
"modules": [
{
"id": "intro",
"title": "Introduction",
"duration_minutes": 30,
"activities": [{"title": "Discuss", "minutes": 20}],
"worksheet_ids": ["welcome"],
}
],
"worksheets": [{"id": "welcome", "path": "worksheets/intro.md"}],
"materials": {"participant": "participant.md", "facilitator": "facilitator.md"},
"maintenance": {
"owner": "Community team",
"status": "confirmed",
"review_date": "2026-10-01",
},
"sources": [
{
"id": "source-1",
"locator": "https://example.test/source",
"checked_on": "2026-09-01",
"status": "verified",
}
],
}
def write_plan(self, plan: object) -> None:
self.plan_path.write_text(json.dumps(plan), encoding="utf-8")
def run_cli(self, path: Path | str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(SCRIPT), str(path)],
capture_output=True,
text=True,
check=False,
)
def test_valid_plan_and_non_readiness_warnings(self) -> None:
plan = self.plan()
plan["maintenance"] = { # type: ignore[index]
"owner": "Community team",
"status": "proposed",
"review_date": "2026-10-01",
}
plan["sources"] = [ # type: ignore[index]
{
"id": "source-1",
"locator": "provided by the author",
"checked_on": "2026-09-01",
"status": "unresolved",
}
]
self.write_plan(plan)
result = validate_guide.validate_file(self.plan_path)
self.assertEqual(result[1], 0)
self.assertTrue(result[0]["valid"])
self.assertEqual(len(result[0]["errors"]), 0)
self.assertEqual(len(result[0]["warnings"]), 2)
def test_required_fields_types_dates_and_duplicates(self) -> None:
plan = self.plan()
plan["schema_version"] = True # type: ignore[index]
plan["title"] = " " # type: ignore[index]
plan["modules"] = [ # type: ignore[index]
{
"id": "same",
"title": "One",
"duration_minutes": 0,
"activities": [{"title": "", "minutes": False}],
"worksheet_ids": [3],
},
{
"id": "same",
"title": "Two",
"duration_minutes": 10,
"activities": [],
"worksheet_ids": [],
},
]
plan["worksheets"] = [ # type: ignore[index]
{"id": "welcome", "path": "worksheets/intro.md"},
{"id": "welcome", "path": "worksheets/intro.md"},
]
plan["maintenance"] = { # type: ignore[index]
"owner": "owner",
"status": [],
"review_date": "2026-02-30",
}
plan["sources"][0]["status"] = [] # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
self.assertFalse(result["valid"])
joined = "\n".join(result["errors"])
self.assertIn("schema_version: must be the integer 1", joined)
self.assertIn("title: must be a nonblank string", joined)
self.assertIn("duplicate module id", joined)
self.assertIn("duplicate worksheet id", joined)
self.assertIn("positive integer", joined)
self.assertIn("must contain at least one activity", joined)
self.assertIn("valid calendar date", joined)
def test_timing_and_unresolved_worksheet_reference(self) -> None:
plan = self.plan()
plan["modules"][0]["duration_minutes"] = 10 # type: ignore[index]
plan["modules"][0]["worksheet_ids"] = ["missing"] # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
self.assertIn("exceeds duration_minutes", "\n".join(result["errors"]))
self.assertIn("does not resolve", "\n".join(result["errors"]))
def test_relative_file_refs_missing_and_traversal(self) -> None:
with tempfile.TemporaryDirectory() as external_dir:
outside = Path(external_dir) / "outside-material.md"
outside.write_text("outside", encoding="utf-8")
plan = self.plan()
plan["materials"]["participant"] = os.path.relpath( # type: ignore[index]
outside, self.plan_path.parent
)
plan["worksheets"][0]["path"] = "missing.md" # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
joined = "\n".join(result["errors"])
self.assertIn("'..' path components are not allowed", joined)
self.assertIn("referenced file does not exist", joined)
def test_parent_components_inside_base_and_backslashes_are_rejected(self) -> None:
plan = self.plan()
plan["materials"]["participant"] = "nested/../participant.md" # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
self.assertIn("'..' path components are not allowed", "\n".join(result["errors"]))
plan = self.plan()
plan["materials"]["facilitator"] = "facilitator\\.md" # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
self.assertIn("backslashes are not allowed", "\n".join(result["errors"]))
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
def test_symlink_escape_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as external_dir:
external = Path(external_dir) / "external-guide-material.md"
external.write_text("external", encoding="utf-8")
link = self.root / "escape.md"
try:
link.symlink_to(external)
plan = self.plan()
plan["materials"]["participant"] = "escape.md" # type: ignore[index]
self.write_plan(plan)
result, exit_code = validate_guide.validate_file(self.plan_path)
self.assertEqual(exit_code, 1)
self.assertIn("resolves outside the plan directory", "\n".join(result["errors"]))
finally:
link.unlink(missing_ok=True)
def test_cli_exit_codes_json_and_duplicate_keys(self) -> None:
self.write_plan(self.plan())
valid = self.run_cli(self.plan_path)
self.assertEqual(valid.returncode, 0)
self.assertEqual(valid.stderr, "")
self.assertTrue(json.loads(valid.stdout)["valid"])
invalid_path = self.root / "invalid.json"
invalid_path.write_text("{}", encoding="utf-8")
invalid = self.run_cli(invalid_path)
self.assertEqual(invalid.returncode, 1)
self.assertFalse(json.loads(invalid.stdout)["valid"])
self.assertEqual(invalid.stderr, "")
duplicate_path = self.root / "duplicate.json"
duplicate_path.write_text('{"schema_version":1,"schema_version":1}', encoding="utf-8")
duplicate = self.run_cli(duplicate_path)
self.assertEqual(duplicate.returncode, 2)
duplicate_result = json.loads(duplicate.stdout)
self.assertIn("duplicate JSON key", duplicate_result["errors"][0])
self.assertEqual(duplicate.stderr, "")
missing = self.run_cli(self.root / "does-not-exist.json")
self.assertEqual(missing.returncode, 2)
self.assertIn("cannot read plan", json.loads(missing.stdout)["errors"][0])
malformed = self.root / "malformed.json"
malformed.write_text("{", encoding="utf-8")
malformed_result = self.run_cli(malformed)
self.assertEqual(malformed_result.returncode, 2)
self.assertIn("invalid JSON", json.loads(malformed_result.stdout)["errors"][0])
def test_no_argument_is_json_input_failure(self) -> None:
result = subprocess.run(
[sys.executable, str(SCRIPT)], capture_output=True, text=True, check=False
)
self.assertEqual(result.returncode, 2)
self.assertEqual(result.stderr, "")
self.assertIn("exactly one", json.loads(result.stdout)["errors"][0])
def test_help_is_concise_and_succeeds(self) -> None:
result = subprocess.run(
[sys.executable, str(SCRIPT), "--help"],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stderr, "")
self.assertIn("Usage: python3 scripts/validate_guide.py PATH_TO_PLAN_JSON", result.stdout)
self.assertIn("Exit codes: 0 valid, 1 invalid plan, 2 input failure.", result.stdout)
if __name__ == "__main__":
unittest.main()