378 lines
13 KiB
Python
Executable File
378 lines
13 KiB
Python
Executable File
"""
|
|
Unit tests for XIC v2 control flow (IF, MATCH, LOOP).
|
|
Tests predicate evaluation, control operations, and guardrails.
|
|
"""
|
|
import pytest
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from xic_ops import XICContext, op_IF, op_MATCH, op_LOOP, op_PUSH_GLYPH_CONTEXT, op_CALL_GLYPH
|
|
from glyphos.control.predicate import eval_predicate
|
|
from glyphos.symbolic_pipeline import SymbolicPipelineResult, FusedSymbol, GlyphResonanceMap, GlyphResonanceMetrics
|
|
|
|
|
|
def create_fake_pipeline_result(global_score: float = 0.9, glyph_ids: list = None):
|
|
"""Helper to create a fake symbolic pipeline result for testing."""
|
|
if glyph_ids is None:
|
|
glyph_ids = ["glyph://a", "glyph://b"]
|
|
|
|
# Create fake resonance map
|
|
resonance_map = GlyphResonanceMap(global_resonance_score=global_score)
|
|
for glyph_id in glyph_ids:
|
|
resonance_map.resonances[glyph_id] = GlyphResonanceMetrics(
|
|
weight=0.8,
|
|
lineage_score=0.7,
|
|
contributor_score=0.75,
|
|
frequency_score=0.8,
|
|
grammar_score=0.85,
|
|
)
|
|
|
|
fused = FusedSymbol(
|
|
summary="Test output",
|
|
glyph_ids=glyph_ids,
|
|
resonance_map=resonance_map,
|
|
)
|
|
|
|
return SymbolicPipelineResult(
|
|
steps=[],
|
|
output_text="Test output",
|
|
fused_symbol=fused,
|
|
)
|
|
|
|
|
|
class TestPredicateEvaluator:
|
|
"""Test safe predicate evaluation."""
|
|
|
|
def test_simple_comparison(self):
|
|
"""Test simple numeric comparison."""
|
|
fused = {"global_resonance_score": 0.9}
|
|
result = eval_predicate("fused.global_resonance_score > 0.8", fused)
|
|
assert result is True
|
|
|
|
def test_comparison_false(self):
|
|
"""Test comparison that evaluates to False."""
|
|
fused = {"global_resonance_score": 0.5}
|
|
result = eval_predicate("fused.global_resonance_score > 0.8", fused)
|
|
assert result is False
|
|
|
|
def test_boolean_and(self):
|
|
"""Test AND operator."""
|
|
fused = {"global_resonance_score": 0.9, "count": 3}
|
|
result = eval_predicate(
|
|
"fused.global_resonance_score > 0.8 and fused.count > 2",
|
|
fused
|
|
)
|
|
assert result is True
|
|
|
|
def test_boolean_or(self):
|
|
"""Test OR operator."""
|
|
fused = {"global_resonance_score": 0.5}
|
|
result = eval_predicate(
|
|
"fused.global_resonance_score > 0.8 or fused.global_resonance_score > 0.4",
|
|
fused
|
|
)
|
|
assert result is True
|
|
|
|
def test_dominant_contains(self):
|
|
"""Test dominant_contains helper function."""
|
|
dominant = [("glyph://entropy", 0.95), ("glyph://compression", 0.8)]
|
|
result = eval_predicate(
|
|
"dominant_contains('glyph://entropy')",
|
|
{},
|
|
dominant
|
|
)
|
|
assert result is True
|
|
|
|
def test_dominant_contains_not_found(self):
|
|
"""Test dominant_contains when glyph not present."""
|
|
dominant = [("glyph://entropy", 0.95)]
|
|
result = eval_predicate(
|
|
"dominant_contains('glyph://other')",
|
|
{},
|
|
dominant
|
|
)
|
|
assert result is False
|
|
|
|
def test_unsafe_import(self):
|
|
"""Test that import statements are blocked."""
|
|
with pytest.raises(ValueError):
|
|
eval_predicate("__import__('os')")
|
|
|
|
def test_syntax_error(self):
|
|
"""Test handling of syntax errors."""
|
|
with pytest.raises(ValueError, match="Invalid predicate syntax"):
|
|
eval_predicate("fused.score >")
|
|
|
|
|
|
class TestIFOperation:
|
|
"""Test IF control flow operation."""
|
|
|
|
def test_if_then_true(self):
|
|
"""Test IF with true condition and then branch."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.9)
|
|
|
|
op_IF(ctx, "fused.global_resonance_score > 0.8", "then_chain", "else_chain")
|
|
|
|
assert ctx.pop_next_chain() == "then_chain"
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_if_else_true(self):
|
|
"""Test IF with false condition and else branch."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.5)
|
|
|
|
op_IF(ctx, "fused.global_resonance_score > 0.8", "then_chain", "else_chain")
|
|
|
|
assert ctx.pop_next_chain() == "else_chain"
|
|
|
|
def test_if_no_else(self):
|
|
"""Test IF without else branch."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.5)
|
|
|
|
op_IF(ctx, "fused.global_resonance_score > 0.8", "then_chain")
|
|
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_if_logs_control_step(self):
|
|
"""Test that IF logs control steps for observability."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.9)
|
|
|
|
op_IF(ctx, "fused.global_resonance_score > 0.8", "then_chain")
|
|
|
|
control_steps = ctx._state.get("control_steps", [])
|
|
assert len(control_steps) > 0
|
|
assert control_steps[0]["type"] == "if"
|
|
assert control_steps[0]["result"] is True
|
|
|
|
def test_if_with_dominant_contains(self):
|
|
"""Test IF using dominant_contains helper."""
|
|
ctx = XICContext()
|
|
result = create_fake_pipeline_result(glyph_ids=["glyph://entropy", "glyph://compression"])
|
|
ctx._state["last_symbolic_pipeline"] = result
|
|
|
|
op_IF(ctx, "dominant_contains('glyph://entropy')", "found_chain")
|
|
|
|
assert ctx.pop_next_chain() == "found_chain"
|
|
|
|
|
|
class TestMATCHOperation:
|
|
"""Test MATCH control flow operation."""
|
|
|
|
def test_match_glyph_ids_found(self):
|
|
"""Test MATCH when pattern is found in glyph_ids."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(
|
|
glyph_ids=["glyph://a", "glyph://b", "glyph://c"]
|
|
)
|
|
|
|
op_MATCH(ctx, "fused.glyph_ids", "glyph://b", "found_chain")
|
|
|
|
assert ctx.pop_next_chain() == "found_chain"
|
|
|
|
def test_match_glyph_ids_not_found(self):
|
|
"""Test MATCH when pattern is not in glyph_ids."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(
|
|
glyph_ids=["glyph://a", "glyph://b"]
|
|
)
|
|
|
|
op_MATCH(ctx, "fused.glyph_ids", "glyph://x", "found_chain")
|
|
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_match_logs_step(self):
|
|
"""Test that MATCH logs symbolic steps."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result()
|
|
|
|
op_MATCH(ctx, "fused.glyph_ids", "glyph://a", "chain")
|
|
|
|
steps = ctx._state.get("symbolic_steps", [])
|
|
assert any(s["kind"] == "control_match" for s in steps)
|
|
|
|
|
|
class TestLOOPOperation:
|
|
"""Test LOOP control flow operation."""
|
|
|
|
def test_loop_iterations(self):
|
|
"""Test LOOP schedules multiple iterations."""
|
|
ctx = XICContext()
|
|
ctx.params["max_loop_iterations"] = 3
|
|
# Create a pipeline that will be true for the loop condition
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=1.0)
|
|
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.5", "body_chain", 3)
|
|
|
|
# Should have enqueued 3 iterations
|
|
chains = []
|
|
while True:
|
|
c = ctx.pop_next_chain()
|
|
if c is None:
|
|
break
|
|
chains.append(c)
|
|
|
|
assert chains == ["body_chain", "body_chain", "body_chain"]
|
|
|
|
def test_loop_predicate_false(self):
|
|
"""Test LOOP stops when predicate is false."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.3)
|
|
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.8", "body_chain", 10)
|
|
|
|
# Should not enqueue anything
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_loop_max_iterations_guardrail(self):
|
|
"""Test LOOP guardrail triggers at max iterations."""
|
|
ctx = XICContext()
|
|
ctx.params["max_loop_iterations"] = 2
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=1.0)
|
|
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.5", "body_chain", 2)
|
|
|
|
# Should trigger guardrail
|
|
guardrails = ctx._state.get("guardrails", [])
|
|
assert "max_loop_iterations_exceeded" in guardrails
|
|
|
|
def test_loop_max_total_steps_guardrail(self):
|
|
"""Test LOOP respects max_total_steps guardrail."""
|
|
ctx = XICContext()
|
|
ctx.params["max_total_steps"] = 5
|
|
ctx._state["total_steps"] = 5 # Already at limit
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=1.0)
|
|
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.5", "body_chain", 10)
|
|
|
|
# Should trigger guardrail immediately
|
|
guardrails = ctx._state.get("guardrails", [])
|
|
assert "max_total_steps_exceeded" in guardrails
|
|
|
|
# Should not enqueue any chains
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_loop_emits_steps(self):
|
|
"""Test LOOP emits symbolic steps for each iteration."""
|
|
ctx = XICContext()
|
|
ctx.params["max_loop_iterations"] = 2
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=1.0)
|
|
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.5", "body_chain", 2)
|
|
|
|
steps = ctx._state.get("symbolic_steps", [])
|
|
loop_steps = [s for s in steps if s["kind"] == "control_loop"]
|
|
assert len(loop_steps) >= 2
|
|
|
|
|
|
class TestQueueHelpers:
|
|
"""Test XICContext queue helper methods."""
|
|
|
|
def test_enqueue_chain(self):
|
|
"""Test enqueue_chain adds to queue."""
|
|
ctx = XICContext()
|
|
ctx.enqueue_chain("chain1")
|
|
assert ctx.pop_next_chain() == "chain1"
|
|
|
|
def test_fifo_order(self):
|
|
"""Test chains dequeue in FIFO order."""
|
|
ctx = XICContext()
|
|
ctx.enqueue_chain("chain1")
|
|
ctx.enqueue_chain("chain2")
|
|
ctx.enqueue_chain("chain3")
|
|
|
|
assert ctx.pop_next_chain() == "chain1"
|
|
assert ctx.pop_next_chain() == "chain2"
|
|
assert ctx.pop_next_chain() == "chain3"
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_jump_to(self):
|
|
"""Test jump_to clears and replaces queue."""
|
|
ctx = XICContext()
|
|
ctx.enqueue_chain("chain1")
|
|
ctx.enqueue_chain("chain2")
|
|
ctx.jump_to("chain3")
|
|
|
|
# Should only have chain3
|
|
assert ctx.pop_next_chain() == "chain3"
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
def test_pop_empty_queue(self):
|
|
"""Test pop_next_chain on empty queue returns None."""
|
|
ctx = XICContext()
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests combining multiple operations."""
|
|
|
|
def test_if_with_call_glyph(self):
|
|
"""Test IF depends on CALL_GLYPH result."""
|
|
ctx = XICContext()
|
|
ctx.symbolic_mode = True
|
|
|
|
# Simulate CALL_GLYPH execution (would set last_symbolic_pipeline)
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=0.95)
|
|
|
|
# IF should work with the result
|
|
op_IF(ctx, "fused.global_resonance_score > 0.9", "high_resonance")
|
|
|
|
assert ctx.pop_next_chain() == "high_resonance"
|
|
|
|
def test_guardrail_enforcement(self):
|
|
"""Test that guardrails stop execution."""
|
|
ctx = XICContext()
|
|
ctx._state["guardrails"] = ["max_total_steps_exceeded"]
|
|
ctx.params["max_total_steps"] = 10
|
|
ctx._state["total_steps"] = 10
|
|
|
|
# LOOP should respect the guardrail
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result(global_score=1.0)
|
|
op_LOOP(ctx, "fused.global_resonance_score > 0.5", "body", 100)
|
|
|
|
# No chains should be enqueued due to guardrail
|
|
assert ctx.pop_next_chain() is None
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test error handling in control flow."""
|
|
|
|
def test_if_without_pipeline(self):
|
|
"""Test IF gracefully handles missing pipeline."""
|
|
ctx = XICContext()
|
|
# No pipeline set
|
|
op_IF(ctx, "fused.global_resonance_score > 0.5", "then")
|
|
|
|
# Should complete without error, but not enqueue (predicate fails safely)
|
|
assert True # Successfully didn't crash
|
|
|
|
def test_malformed_predicate(self):
|
|
"""Test that malformed predicates are caught."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result()
|
|
|
|
# Invalid syntax
|
|
op_IF(ctx, "fused.score >", "then")
|
|
|
|
# Should handle error gracefully (may not enqueue)
|
|
assert True # Successfully didn't crash
|
|
|
|
def test_loop_with_bad_predicate(self):
|
|
"""Test LOOP handles predicate evaluation errors."""
|
|
ctx = XICContext()
|
|
ctx._state["last_symbolic_pipeline"] = create_fake_pipeline_result()
|
|
|
|
op_LOOP(ctx, "fused.nonexistent > 0.5", "body", 2)
|
|
|
|
# Should handle error gracefully
|
|
assert True # Successfully didn't crash
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "--tb=short"])
|