133 lines
6.2 KiB
Python
133 lines
6.2 KiB
Python
import copy
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SPEC = importlib.util.spec_from_file_location(
|
|
"validate_training_knowledge", ROOT / "tools" / "validate_training_knowledge.py"
|
|
)
|
|
MODULE = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
SPEC.loader.exec_module(MODULE)
|
|
|
|
|
|
class TrainingKnowledgeValidationTest(unittest.TestCase):
|
|
files = (
|
|
"body-zones-v1.json", "equipment-v1.json", "science-references-v1.json", "muscles-v1.json",
|
|
"joint-actions-v1.json", "movement-patterns-v1.json",
|
|
"exercise-knowledge-v1.json", "equipment-knowledge-v1.json",
|
|
"training-knowledge-audit-v1.json",
|
|
)
|
|
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.catalog = Path(self.temp.name)
|
|
for filename in self.files:
|
|
(self.catalog / filename).write_bytes((ROOT / "catalog" / filename).read_bytes())
|
|
|
|
def tearDown(self):
|
|
self.temp.cleanup()
|
|
|
|
def mutate(self, filename, operation):
|
|
path = self.catalog / filename
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
operation(value)
|
|
path.write_text(json.dumps(value, ensure_ascii=False), encoding="utf-8")
|
|
|
|
def assert_invalid(self):
|
|
with self.assertRaises(MODULE.ValidationError):
|
|
MODULE.validate(self.catalog)
|
|
|
|
def test_valid_and_deterministic_generation(self):
|
|
MODULE.validate(self.catalog)
|
|
first = self.catalog / "first.c"
|
|
second = self.catalog / "second.c"
|
|
command = [sys.executable, str(ROOT / "tools" / "generate_training_knowledge.py"),
|
|
str(self.catalog)]
|
|
subprocess.run(command + [str(first)], check=True)
|
|
subprocess.run(command + [str(second)], check=True)
|
|
self.assertEqual(first.read_bytes(), second.read_bytes())
|
|
audit = self.catalog / "regenerated-audit.json"
|
|
subprocess.run([sys.executable, str(ROOT / "tools" / "generate_training_knowledge_audit.py"),
|
|
str(self.catalog), str(audit)], check=True)
|
|
self.assertEqual(audit.read_bytes(), (self.catalog / "training-knowledge-audit-v1.json").read_bytes())
|
|
markdown = self.catalog / "audit.md"
|
|
subprocess.run([sys.executable, str(ROOT / "tools" / "generate_training_knowledge_audit.py"),
|
|
str(self.catalog), str(markdown), "--markdown"], check=True)
|
|
self.assertEqual(markdown.read_bytes(), (ROOT / "docs/domain/knowledge_audit.md").read_bytes())
|
|
|
|
def test_unknown_nested_keys_and_types_are_validation_errors(self):
|
|
mutations = [
|
|
lambda root: root["equipment"][0].__setitem__("unexpected_review_key", True),
|
|
lambda root: root["equipment"][0]["capabilities"][0].__setitem__("requirements", None),
|
|
lambda root: root["equipment"][0].__setitem__("requires_actual_exercise", 1),
|
|
]
|
|
original = (self.catalog / "equipment-knowledge-v1.json").read_bytes()
|
|
for mutation in mutations:
|
|
(self.catalog / "equipment-knowledge-v1.json").write_bytes(original)
|
|
self.mutate("equipment-knowledge-v1.json", mutation)
|
|
self.assert_invalid()
|
|
|
|
def test_additive_non_runtime_reference_is_valid(self):
|
|
def add(root):
|
|
row = copy.deepcopy(root["references"][-1])
|
|
row["ref_id"] = "zz_additive_provenance"
|
|
row["title"] = "Additional provenance record"
|
|
root["references"].append(row)
|
|
self.mutate("science-references-v1.json", add)
|
|
MODULE.validate(self.catalog)
|
|
|
|
def test_stale_generated_audit_is_rejected(self):
|
|
self.mutate("training-knowledge-audit-v1.json",
|
|
lambda root: root["rows"][0].__setitem__("rationale", "stale generated value"))
|
|
self.assert_invalid()
|
|
|
|
def test_duplicate_object_key_rejected(self):
|
|
path = self.catalog / "science-references-v1.json"
|
|
text = path.read_text(encoding="utf-8")
|
|
path.write_text(text.replace('"version": 1', '"version": 1, "version": 1', 1), encoding="utf-8")
|
|
self.assert_invalid()
|
|
|
|
def test_version_order_enum_role_and_cross_reference_mutations(self):
|
|
cases = [
|
|
("muscles-v1.json", lambda root: root.__setitem__("version", 2)),
|
|
("muscles-v1.json", lambda root: root["muscles"].reverse()),
|
|
("muscles-v1.json", lambda root: root["muscles"][0]["joint_action_ids"].reverse()),
|
|
("muscles-v1.json", lambda root: root["muscles"][0].__setitem__("confidence", "limited")),
|
|
("exercise-knowledge-v1.json", lambda root: root["exercises"][0]["interpretation"].
|
|
__setitem__("primary_muscle_ids", ["missing_muscle"])),
|
|
("exercise-knowledge-v1.json", lambda root: root["exercises"][0]["interpretation"].
|
|
__setitem__("secondary_muscle_ids", root["exercises"][0]["interpretation"]["primary_muscle_ids"])),
|
|
("equipment-knowledge-v1.json", lambda root: root["equipment"][0]["capabilities"][0].
|
|
__setitem__("exercise_ids", ["ex_00000000-0000-4000-8000-000000000000"])),
|
|
]
|
|
originals = {filename: (self.catalog / filename).read_bytes() for filename in self.files}
|
|
for filename, mutation in cases:
|
|
for restore, contents in originals.items():
|
|
(self.catalog / restore).write_bytes(contents)
|
|
self.mutate(filename, mutation)
|
|
self.assert_invalid()
|
|
|
|
def test_conditional_candidate_cannot_become_resolved_data(self):
|
|
def leak(root):
|
|
row = next(item for item in root["exercises"] if item["resolution_status"] == "conditional")
|
|
row["interpretation"] = copy.deepcopy(row["conditional_interpretation"])
|
|
self.mutate("exercise-knowledge-v1.json", leak)
|
|
self.assert_invalid()
|
|
|
|
def test_manufacturer_only_high_mapping_rejected(self):
|
|
def mutation(root):
|
|
root["equipment"][0]["confidence"] = "high"
|
|
root["equipment"][0]["evidence_type"] = "manufacturer_statement"
|
|
self.mutate("equipment-knowledge-v1.json", mutation)
|
|
self.assert_invalid()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|