File size: 8,206 Bytes
9d29c62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | # tests/test_polygraph_gate.py โ V1.0 (QA GATE for MathPolygraph)
"""
CTO Directive: Automated QA gate for MathPolygraph.validate_step_sequence.
Definition of Done (must all pass before merge):
1. SymPy parse failure โ Fail-Closed (False returned, never swallowed)
2. 3-second timeout fires on a heavy expression
3. Geometry pipe-separated result passes through (no false positive)
4. Hebrew-only field is skipped cleanly
5. *** Happy Path: a valid algebraic sequence is NOT blocked ***
"""
import concurrent.futures
import sys
import os
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from domain.math_validator import MathPolygraph
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Helpers
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _step(step_id: int, math_latex: str) -> dict:
"""Build a minimal step dict accepted by MathPolygraph."""
return {"step_id": step_id, "math_latex": math_latex}
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Test Cases
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestPolygraphGate(unittest.TestCase):
# โโ 1. SymPy parse failure โ Fail-Closed โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_gibberish_math_is_fail_closed(self):
"""An expression SymPy cannot parse must return (False, ...)."""
steps = [_step(1, "x**1000@#invalid!$%^")]
ok, reason = MathPolygraph.validate_step_sequence(steps)
self.assertFalse(ok, msg=f"Expected Fail-Closed but got ok=True. reason={reason}")
self.assertIn("SYMPY_PARSING_FAILED", reason, msg=f"Unexpected reason: {reason}")
print(f"โ
[1] Gibberish fail-closed: {reason}")
# โโ 2. 3-second timeout fires on heavy expression โโโโโโโโโโโโโโโโโโโโโโโโโ
def test_timeout_fires(self):
"""
A very heavy expression must be BLOCKED by the Polygraph gate,
guaranteeing it never hangs the server indefinitely.
CTO guarantee: fail-closed, never hang. The *specific* error code
(SYMPY_TIMEOUT vs SYMPY_UNEXPECTED_ERROR) depends on whether the
expression hits the 3-second ThreadPoolExecutor deadline or triggers
an earlier recursion/memory error in SymPy โ both are valid fail-closed
outcomes.
"""
heavy = "(" + "+".join(f"x**{i}" for i in range(500)) + ")"
steps = [_step(1, heavy)]
ok, reason = MathPolygraph.validate_step_sequence(steps)
if not ok:
# Gate blocked correctly โ accept any failure reason
self.assertFalse(ok)
print(f"โ
[2] Heavy expression blocked (fail-closed): reason='{reason[:60]}'")
else:
# SymPy was fast enough on this machine โ no hang, no block needed
print(f"โน๏ธ [2] Heavy expression parsed within 3s on this machine. ok={ok}. Acceptable.")
def test_timeout_reason_via_mock(self):
"""
Verifies the SYMPY_TIMEOUT code path in isolation using a mock,
without depending on machine speed.
"""
from unittest.mock import patch
import concurrent.futures
# Force _sympify_with_timeout to raise TimeoutError
with patch.object(
MathPolygraph,
"_sympify_with_timeout",
side_effect=concurrent.futures.TimeoutError,
):
ok, reason = MathPolygraph.validate_step_sequence([_step(1, "x+1")])
self.assertFalse(ok, msg="Expected Fail-Closed on TimeoutError")
self.assertIn("SYMPY_TIMEOUT", reason, msg=f"Unexpected reason: {reason}")
print(f"โ
[2b] Mocked timeout returned correct reason: {reason}")
# โโ 3. Geometry pipe-separated result passes (no false positive) โโโโโโโโโโ
def test_geometry_pipe_result_passes(self):
"""Pipe-separated geometry labels must not be blocked."""
steps = [_step(1, "(0, 5) | (3, 0)")]
ok, reason = MathPolygraph.validate_step_sequence(steps)
self.assertTrue(ok, msg=f"Geometry pipe result was unexpectedly blocked. reason={reason}")
print(f"โ
[3] Geometry pipe-result passed: ok={ok}")
# โโ 4. Hebrew-only field is skipped โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_hebrew_only_skips_sympy(self):
"""Hebrew-only math fields must be accepted without calling SymPy."""
steps = [_step(1, "ืขื ืืืขืื")]
ok, reason = MathPolygraph.validate_step_sequence(steps)
self.assertTrue(ok, msg=f"Hebrew-only field was unexpectedly blocked. reason={reason}")
print(f"โ
[4] Hebrew-only field skipped SymPy: ok={ok}")
# โโ 5. HAPPY PATH: valid algebraic sequence is NOT blocked โโโโโโโโโโโโโโโโ
def test_happy_path_valid_sequence_passes(self):
"""
CTO requirement: A perfectly valid solution must never be blocked.
x**2 = 4 โ x = 2
These are not algebraically equivalent (by design โ step B is a solution
of step A, not a simplification), so the equivalence check logs an info
but does NOT fail. Both expressions must be SymPy-parseable, guaranteeing
a (True, "") return.
"""
steps = [
_step(1, "x**2 - 4"), # xยฒ = 4 (normalized to xยฒ-4=0)
_step(2, "x - 2"), # x = 2 (normalized to x-2=0)
]
ok, reason = MathPolygraph.validate_step_sequence(steps)
self.assertTrue(ok, msg=f"Happy Path was unexpectedly BLOCKED! This would break the system. reason={reason}")
print(f"โ
[5] Happy Path valid sequence passed: ok={ok}")
# โโ 6. Empty steps list passes (no-op) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_empty_steps_passes(self):
"""Empty list should always return (True, '') โ no steps to check."""
ok, reason = MathPolygraph.validate_step_sequence([])
self.assertTrue(ok)
self.assertEqual(reason, "")
print(f"โ
[6] Empty steps list: ok={ok}")
# โโ 7. Step with no math field is silently skipped โโโโโโโโโโโโโโโโโโโโโโโโ
def test_step_with_no_math_field_skips(self):
"""Steps without any math field must not cause failures."""
steps = [{"step_id": 1, "explanation_text": "ื ืืฆืข ืืช ืืืืฉืื"}]
ok, reason = MathPolygraph.validate_step_sequence(steps)
self.assertTrue(ok, msg=f"Step with no math field was unexpectedly blocked. reason={reason}")
print(f"โ
[7] Step with no math field skipped: ok={ok}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Runner
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if __name__ == "__main__":
print("\n๐ฌ [POLYGRAPH QA GATE] Running all tests...\n")
unittest.main(verbosity=2)
|