Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 28s
Smoke Test / smoke (pull_request) Failing after 23s
Validate Config / YAML Lint (pull_request) Failing after 21s
Validate Config / JSON Validate (pull_request) Successful in 21s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 1m23s
Validate Config / Shell Script Lint (pull_request) Failing after 50s
Validate Config / Cron Syntax Check (pull_request) Successful in 11s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 26s
Validate Config / Playbook Schema Validation (pull_request) Successful in 32s
PR Checklist / pr-checklist (pull_request) Failing after 11m13s
Architecture Lint / Lint Repository (pull_request) Has been cancelled
Validate Config / Python Test Suite (pull_request) Has been cancelled
- Added TestThreeJsPatterns class with 14 tests - Tests cover: pattern existence, severity inference, vision prompt - Updated pattern count assertion (14+ patterns now) - Updated demo test (6 glitches: 4 original + 2 Three.js)
381 lines
15 KiB
Python
381 lines
15 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,
|
|
THREEJS_CATEGORIES,
|
|
build_vision_prompt,
|
|
get_pattern_by_category,
|
|
get_patterns_by_severity,
|
|
get_threejs_patterns,
|
|
)
|
|
|
|
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), 14) # 10 generic + 6 Three.js
|
|
|
|
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)
|
|
# Three.js patterns should be included
|
|
self.assertIn("Shader Compilation Failure", prompt)
|
|
self.assertIn("Bloom Overflow", 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), 6) # 4 original + 2 Three.js
|
|
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 TestThreeJsPatterns(unittest.TestCase):
|
|
"""Tests for Three.js-specific glitch patterns (timmy-config#543)."""
|
|
|
|
def test_get_threejs_patterns_returns_only_threejs(self):
|
|
"""get_threejs_patterns() should return only Three.js categories."""
|
|
patterns = get_threejs_patterns()
|
|
self.assertEqual(len(patterns), 6)
|
|
for p in patterns:
|
|
self.assertIn(p.category, THREEJS_CATEGORIES)
|
|
|
|
def test_threejs_patterns_have_required_fields(self):
|
|
"""All Three.js patterns must have valid fields."""
|
|
for p in get_threejs_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)
|
|
|
|
def test_shader_failure_is_critical(self):
|
|
"""Shader compilation failure should be CRITICAL severity."""
|
|
p = get_pattern_by_category(GlitchCategory.SHADER_FAILURE)
|
|
self.assertIsNotNone(p)
|
|
self.assertEqual(p.severity, GlitchSeverity.CRITICAL)
|
|
|
|
def test_texture_placeholder_is_critical(self):
|
|
"""Texture placeholder (1x1 white) should be CRITICAL severity."""
|
|
p = get_pattern_by_category(GlitchCategory.TEXTURE_PLACEHOLDER)
|
|
self.assertIsNotNone(p)
|
|
self.assertEqual(p.severity, GlitchSeverity.CRITICAL)
|
|
|
|
def test_infer_severity_shader_failure(self):
|
|
"""Shader failure should infer critical/high."""
|
|
self.assertEqual(_infer_severity("shader_failure", 0.8), "critical")
|
|
self.assertEqual(_infer_severity("shader_failure", 0.5), "high")
|
|
|
|
def test_infer_severity_texture_placeholder(self):
|
|
"""Texture placeholder should infer critical/high."""
|
|
self.assertEqual(_infer_severity("texture_placeholder", 0.8), "critical")
|
|
self.assertEqual(_infer_severity("texture_placeholder", 0.5), "high")
|
|
|
|
def test_infer_severity_uv_mapping(self):
|
|
"""UV mapping error should infer high/medium."""
|
|
self.assertEqual(_infer_severity("uv_mapping_error", 0.8), "high")
|
|
self.assertEqual(_infer_severity("uv_mapping_error", 0.5), "medium")
|
|
|
|
def test_infer_severity_frustum_culling(self):
|
|
"""Frustum culling should infer medium/low."""
|
|
self.assertEqual(_infer_severity("frustum_culling", 0.7), "medium")
|
|
self.assertEqual(_infer_severity("frustum_culling", 0.4), "low")
|
|
|
|
def test_infer_severity_shadow_map(self):
|
|
"""Shadow map artifact should infer medium/low."""
|
|
self.assertEqual(_infer_severity("shadow_map_artifact", 0.7), "medium")
|
|
self.assertEqual(_infer_severity("shadow_map_artifact", 0.4), "low")
|
|
|
|
def test_infer_severity_bloom_overflow(self):
|
|
"""Bloom overflow should infer medium/low (default path)."""
|
|
self.assertEqual(_infer_severity("bloom_overflow", 0.7), "medium")
|
|
self.assertEqual(_infer_severity("bloom_overflow", 0.4), "low")
|
|
|
|
def test_threejs_patterns_in_vision_prompt(self):
|
|
"""Three.js patterns should appear in the composite vision prompt."""
|
|
prompt = build_vision_prompt()
|
|
self.assertIn("shader_failure", prompt)
|
|
self.assertIn("texture_placeholder", prompt)
|
|
self.assertIn("uv_mapping_error", prompt)
|
|
self.assertIn("frustum_culling", prompt)
|
|
self.assertIn("shadow_map_artifact", prompt)
|
|
self.assertIn("bloom_overflow", prompt)
|
|
|
|
def test_threejs_subset_prompt(self):
|
|
"""Building prompt from Three.js-only patterns should work."""
|
|
threejs = get_threejs_patterns()
|
|
prompt = build_vision_prompt(threejs)
|
|
self.assertIn("Shader Compilation Failure", prompt)
|
|
self.assertNotIn("Floating Object", prompt) # generic, not Three.js
|
|
|
|
def test_report_metadata_version(self):
|
|
"""Report metadata should reference both issues."""
|
|
report = run_demo()
|
|
self.assertEqual(report.metadata["detector_version"], "0.2.0")
|
|
self.assertIn("543", report.metadata["reference"])
|
|
|
|
|
|
|
|
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))
|
|
|
|
def test_patterns_cover_threejs_themes(self):
|
|
"""Patterns should cover Three.js-specific glitch themes (#543)."""
|
|
category_values = {p.category.value for p in MATRIX_GLITCH_PATTERNS}
|
|
threejs_expected = {"shader_failure", "texture_placeholder", "uv_mapping_error",
|
|
"frustum_culling", "shadow_map_artifact", "bloom_overflow"}
|
|
self.assertTrue(threejs_expected.issubset(category_values))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|