Some checks failed
Architecture Lint / Linter Tests (push) Has been cancelled
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Python Test Suite (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Merge PR #535
282 lines
11 KiB
Python
282 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for Matrix 3D Glitch Detector (timmy-config#491).
|
|
|
|
Covers: glitch_patterns, matrix_glitch_detector core logic.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
# Ensure bin/ is importable
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bin"))
|
|
|
|
from glitch_patterns import (
|
|
GlitchCategory,
|
|
GlitchPattern,
|
|
GlitchSeverity,
|
|
MATRIX_GLITCH_PATTERNS,
|
|
build_vision_prompt,
|
|
get_pattern_by_category,
|
|
get_patterns_by_severity,
|
|
)
|
|
|
|
from matrix_glitch_detector import (
|
|
DetectedGlitch,
|
|
ScanResult,
|
|
_infer_severity,
|
|
_parse_vision_response,
|
|
build_report,
|
|
generate_scan_angles,
|
|
run_demo,
|
|
)
|
|
|
|
|
|
class TestGlitchPatterns(unittest.TestCase):
|
|
"""Tests for glitch_patterns module."""
|
|
|
|
def test_pattern_count(self):
|
|
"""Verify we have a reasonable number of defined patterns."""
|
|
self.assertGreaterEqual(len(MATRIX_GLITCH_PATTERNS), 8)
|
|
|
|
def test_all_patterns_have_required_fields(self):
|
|
"""Every pattern must have category, name, description, severity, prompts."""
|
|
for p in MATRIX_GLITCH_PATTERNS:
|
|
self.assertIsInstance(p.category, GlitchCategory)
|
|
self.assertTrue(p.name)
|
|
self.assertTrue(p.description)
|
|
self.assertIsInstance(p.severity, GlitchSeverity)
|
|
self.assertGreater(len(p.detection_prompts), 0)
|
|
self.assertGreater(len(p.visual_indicators), 0)
|
|
self.assertGreater(p.confidence_threshold, 0)
|
|
self.assertLessEqual(p.confidence_threshold, 1.0)
|
|
|
|
def test_pattern_to_dict(self):
|
|
"""Pattern serialization should produce a dict with expected keys."""
|
|
p = MATRIX_GLITCH_PATTERNS[0]
|
|
d = p.to_dict()
|
|
self.assertIn("category", d)
|
|
self.assertIn("name", d)
|
|
self.assertIn("severity", d)
|
|
self.assertEqual(d["category"], p.category.value)
|
|
|
|
def test_get_patterns_by_severity(self):
|
|
"""Severity filter should return only patterns at or above threshold."""
|
|
high_patterns = get_patterns_by_severity(GlitchSeverity.HIGH)
|
|
self.assertTrue(all(p.severity.value in ("high", "critical") for p in high_patterns))
|
|
self.assertGreater(len(high_patterns), 0)
|
|
|
|
all_patterns = get_patterns_by_severity(GlitchSeverity.INFO)
|
|
self.assertEqual(len(all_patterns), len(MATRIX_GLITCH_PATTERNS))
|
|
|
|
def test_get_pattern_by_category(self):
|
|
"""Lookup by category should return the correct pattern."""
|
|
p = get_pattern_by_category(GlitchCategory.FLOATING_ASSETS)
|
|
self.assertIsNotNone(p)
|
|
self.assertEqual(p.category, GlitchCategory.FLOATING_ASSETS)
|
|
|
|
missing = get_pattern_by_category("nonexistent_category_value")
|
|
self.assertIsNone(missing)
|
|
|
|
def test_build_vision_prompt(self):
|
|
"""Vision prompt should contain pattern names and be non-trivial."""
|
|
prompt = build_vision_prompt()
|
|
self.assertGreater(len(prompt), 200)
|
|
self.assertIn("Floating Object", prompt)
|
|
self.assertIn("Z-Fighting", prompt)
|
|
self.assertIn("Missing", prompt)
|
|
|
|
def test_build_vision_prompt_subset(self):
|
|
"""Vision prompt with subset should only include specified patterns."""
|
|
subset = MATRIX_GLITCH_PATTERNS[:3]
|
|
prompt = build_vision_prompt(subset)
|
|
self.assertIn(subset[0].name, prompt)
|
|
self.assertNotIn(MATRIX_GLITCH_PATTERNS[-1].name, prompt)
|
|
|
|
|
|
class TestGlitchDetector(unittest.TestCase):
|
|
"""Tests for matrix_glitch_detector module."""
|
|
|
|
def test_generate_scan_angles_default(self):
|
|
"""Default 4 angles should return front, right, back, left."""
|
|
angles = generate_scan_angles(4)
|
|
self.assertEqual(len(angles), 4)
|
|
labels = [a["label"] for a in angles]
|
|
self.assertIn("front", labels)
|
|
self.assertIn("right", labels)
|
|
self.assertIn("back", labels)
|
|
self.assertIn("left", labels)
|
|
|
|
def test_generate_scan_angles_many(self):
|
|
"""Requesting more angles than base should still return correct count."""
|
|
angles = generate_scan_angles(12)
|
|
self.assertEqual(len(angles), 12)
|
|
# Should still have the standard ones
|
|
labels = [a["label"] for a in angles]
|
|
self.assertIn("front", labels)
|
|
|
|
def test_generate_scan_angles_few(self):
|
|
"""Requesting fewer angles should return fewer."""
|
|
angles = generate_scan_angles(2)
|
|
self.assertEqual(len(angles), 2)
|
|
|
|
def test_detected_glitch_dataclass(self):
|
|
"""DetectedGlitch should serialize cleanly."""
|
|
g = DetectedGlitch(
|
|
id="test001",
|
|
category="floating_assets",
|
|
name="Test Glitch",
|
|
description="A test glitch",
|
|
severity="high",
|
|
confidence=0.85,
|
|
location_x=50.0,
|
|
location_y=30.0,
|
|
screenshot_index=0,
|
|
screenshot_angle="front",
|
|
)
|
|
self.assertEqual(g.id, "test001")
|
|
self.assertTrue(g.timestamp) # Auto-generated
|
|
|
|
def test_infer_severity_critical(self):
|
|
"""Missing textures should infer critical/high severity."""
|
|
sev = _infer_severity("missing_textures", 0.9)
|
|
self.assertEqual(sev, "critical")
|
|
sev_low = _infer_severity("missing_textures", 0.5)
|
|
self.assertEqual(sev_low, "high")
|
|
|
|
def test_infer_severity_floating(self):
|
|
"""Floating assets should infer high/medium severity."""
|
|
sev = _infer_severity("floating_assets", 0.8)
|
|
self.assertEqual(sev, "high")
|
|
sev_low = _infer_severity("floating_assets", 0.5)
|
|
self.assertEqual(sev_low, "medium")
|
|
|
|
def test_infer_severity_default(self):
|
|
"""Unknown categories should default to medium/low."""
|
|
sev = _infer_severity("unknown_thing", 0.7)
|
|
self.assertEqual(sev, "medium")
|
|
sev_low = _infer_severity("unknown_thing", 0.3)
|
|
self.assertEqual(sev_low, "low")
|
|
|
|
def test_parse_vision_response_json_array(self):
|
|
"""Should parse a JSON array response."""
|
|
response = json.dumps([
|
|
{
|
|
"category": "floating_assets",
|
|
"name": "Float Test",
|
|
"description": "Chair floating",
|
|
"confidence": 0.9,
|
|
"severity": "high",
|
|
"location_x": 40,
|
|
"location_y": 60,
|
|
}
|
|
])
|
|
glitches = _parse_vision_response(response, 0, "front")
|
|
self.assertEqual(len(glitches), 1)
|
|
self.assertEqual(glitches[0].category, "floating_assets")
|
|
self.assertAlmostEqual(glitches[0].confidence, 0.9)
|
|
|
|
def test_parse_vision_response_wrapped(self):
|
|
"""Should parse a response with 'glitches' wrapper key."""
|
|
response = json.dumps({
|
|
"glitches": [
|
|
{
|
|
"category": "z_fighting",
|
|
"name": "Shimmer",
|
|
"confidence": 0.6,
|
|
}
|
|
]
|
|
})
|
|
glitches = _parse_vision_response(response, 1, "right")
|
|
self.assertEqual(len(glitches), 1)
|
|
self.assertEqual(glitches[0].category, "z_fighting")
|
|
|
|
def test_parse_vision_response_empty(self):
|
|
"""Should return empty list for non-JSON text."""
|
|
glitches = _parse_vision_response("No glitches found.", 0, "front")
|
|
self.assertEqual(len(glitches), 0)
|
|
|
|
def test_parse_vision_response_code_block(self):
|
|
"""Should extract JSON from markdown code blocks."""
|
|
response = '```json\n[{"category": "clipping", "name": "Clip", "confidence": 0.7}]\n```'
|
|
glitches = _parse_vision_response(response, 0, "front")
|
|
self.assertEqual(len(glitches), 1)
|
|
|
|
def test_build_report(self):
|
|
"""Report should have correct summary statistics."""
|
|
angles = generate_scan_angles(4)
|
|
screenshots = [Path(f"/tmp/ss_{i}.png") for i in range(4)]
|
|
glitches = [
|
|
DetectedGlitch(
|
|
id="a", category="floating_assets", name="Float",
|
|
description="", severity="high", confidence=0.8,
|
|
screenshot_index=0, screenshot_angle="front",
|
|
),
|
|
DetectedGlitch(
|
|
id="b", category="missing_textures", name="Missing",
|
|
description="", severity="critical", confidence=0.95,
|
|
screenshot_index=1, screenshot_angle="right",
|
|
),
|
|
]
|
|
report = build_report("https://test.com", angles, screenshots, glitches)
|
|
|
|
self.assertEqual(report.total_screenshots, 4)
|
|
self.assertEqual(len(report.glitches), 2)
|
|
self.assertEqual(report.summary["total_glitches"], 2)
|
|
self.assertEqual(report.summary["by_severity"]["critical"], 1)
|
|
self.assertEqual(report.summary["by_severity"]["high"], 1)
|
|
self.assertEqual(report.summary["by_category"]["floating_assets"], 1)
|
|
self.assertEqual(report.metadata["reference"], "timmy-config#491")
|
|
|
|
def test_build_report_json_roundtrip(self):
|
|
"""Report JSON should parse back correctly."""
|
|
angles = generate_scan_angles(2)
|
|
screenshots = [Path(f"/tmp/ss_{i}.png") for i in range(2)]
|
|
report = build_report("https://test.com", angles, screenshots, [])
|
|
json_str = report.to_json()
|
|
parsed = json.loads(json_str)
|
|
self.assertEqual(parsed["url"], "https://test.com")
|
|
self.assertEqual(parsed["total_screenshots"], 2)
|
|
|
|
def test_run_demo(self):
|
|
"""Demo mode should produce a report with simulated glitches."""
|
|
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
|
output_path = Path(f.name)
|
|
|
|
try:
|
|
report = run_demo(output_path)
|
|
self.assertEqual(len(report.glitches), 4)
|
|
self.assertGreater(report.summary["total_glitches"], 0)
|
|
self.assertTrue(output_path.exists())
|
|
|
|
# Verify the saved JSON is valid
|
|
saved = json.loads(output_path.read_text())
|
|
self.assertIn("scan_id", saved)
|
|
self.assertIn("glitches", saved)
|
|
finally:
|
|
output_path.unlink(missing_ok=True)
|
|
|
|
|
|
class TestIntegration(unittest.TestCase):
|
|
"""Integration-level tests."""
|
|
|
|
def test_full_pipeline_demo(self):
|
|
"""End-to-end demo pipeline should complete without errors."""
|
|
report = run_demo()
|
|
self.assertIsNotNone(report.scan_id)
|
|
self.assertTrue(report.timestamp)
|
|
self.assertGreater(report.total_screenshots, 0)
|
|
|
|
def test_patterns_cover_matrix_themes(self):
|
|
"""Patterns should cover the main Matrix glitch themes."""
|
|
category_values = {p.category.value for p in MATRIX_GLITCH_PATTERNS}
|
|
expected = {"floating_assets", "z_fighting", "missing_textures", "clipping", "broken_normals"}
|
|
self.assertTrue(expected.issubset(category_values))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|