|
| 1 | +"""Comprehensive regression test suite for SHA (Silence) operator. |
| 2 | +
|
| 3 | +This module implements a complete regression test suite for the Silence (SHA) |
| 4 | +operator following TNFR structural theory as specified in TNFR.pdf §2.3.10. |
| 5 | +
|
| 6 | +Test Coverage: |
| 7 | +- A. Structural Effects: νf reduction, EPI preservation, ΔNFR freezing |
| 8 | +- B. Preconditions: Validation of minimum νf and existing EPI requirements |
| 9 | +- C. Canonical Sequences: IL→SHA, SHA→AL, SHA→NAV, OZ→SHA |
| 10 | +- D. Metrics: SHA-specific metrics validation |
| 11 | +- E. Integration: Multi-node effects, complex sequences |
| 12 | +- F. Nodal Equation: Validation of ∂EPI/∂t = νf · ΔNFR(t) |
| 13 | +- G. Full Lifecycle: Complete activation-silence-reactivation cycle |
| 14 | +
|
| 15 | +Theoretical Foundation: |
| 16 | +- SHA reduces νf → 0 (structural pause) |
| 17 | +- EPI remains invariant (preservation) |
| 18 | +- ΔNFR maintained but frozen (no reorganization pressure) |
| 19 | +- Latency state tracking for memory consolidation |
| 20 | +""" |
| 21 | + |
| 22 | +from __future__ import annotations |
| 23 | + |
| 24 | +import pytest |
| 25 | +import warnings |
| 26 | + |
| 27 | +from tnfr.constants import DNFR_PRIMARY, EPI_PRIMARY, VF_PRIMARY |
| 28 | +from tnfr.structural import create_nfr, run_sequence |
| 29 | +from tnfr.operators.definitions import ( |
| 30 | + Silence, |
| 31 | + Emission, |
| 32 | + Reception, |
| 33 | + Coherence, |
| 34 | + Dissonance, |
| 35 | + Resonance, |
| 36 | + Coupling, |
| 37 | + Transition, |
| 38 | +) |
| 39 | +from tnfr.operators.preconditions import OperatorPreconditionError |
| 40 | +from tnfr.alias import set_attr |
| 41 | +from tnfr.constants.aliases import ALIAS_EPI, ALIAS_VF |
| 42 | + |
| 43 | + |
| 44 | +class TestSHAStructuralEffects: |
| 45 | + """Test A: Structural effects of SHA operator.""" |
| 46 | + |
| 47 | + def test_sha_reduces_vf_to_minimum(self): |
| 48 | + """Test 1: SHA must reduce νf to value close to zero. |
| 49 | + |
| 50 | + Validates nodal equation: If νf ≈ 0, then ∂EPI/∂t ≈ 0 (independent of ΔNFR). |
| 51 | + Sequence must start with generator (AL) per R1 grammar rule. |
| 52 | + """ |
| 53 | + G, node = create_nfr("active", epi=0.60, vf=1.50) |
| 54 | + initial_vf = G.nodes[node][VF_PRIMARY] |
| 55 | + |
| 56 | + # Apply sequence: AL (generator start) → SHA |
| 57 | + run_sequence(G, node, [Emission(), Silence()]) |
| 58 | + |
| 59 | + final_vf = G.nodes[node][VF_PRIMARY] |
| 60 | + min_threshold = G.graph.get("SHA_MIN_VF", 0.01) |
| 61 | + |
| 62 | + # Validations |
| 63 | + assert final_vf < initial_vf, "SHA must reduce νf" |
| 64 | + assert final_vf <= min_threshold * 2, f"νf must be close to minimum: {final_vf}" |
| 65 | + assert final_vf >= 0.0, "νf cannot be negative" |
| 66 | + |
| 67 | + def test_sha_preserves_epi_exactly(self): |
| 68 | + """Test 2: SHA must maintain EPI invariant with minimal tolerance.""" |
| 69 | + G, node = create_nfr("memory", epi=0.73, vf=1.20) |
| 70 | + initial_epi = G.nodes[node][EPI_PRIMARY] |
| 71 | + |
| 72 | + # Apply SHA |
| 73 | + run_sequence(G, node, [Silence()]) |
| 74 | + |
| 75 | + final_epi = G.nodes[node][EPI_PRIMARY] |
| 76 | + tolerance = 1e-3 # Allow small numerical tolerance |
| 77 | + |
| 78 | + assert abs(final_epi - initial_epi) < tolerance, ( |
| 79 | + f"EPI must be preserved: ΔEPI = {abs(final_epi - initial_epi)}" |
| 80 | + ) |
| 81 | + |
| 82 | + def test_sha_freezes_dnfr(self): |
| 83 | + """Test 3: SHA does not modify ΔNFR - state is frozen. |
| 84 | + |
| 85 | + ΔNFR can remain high but with νf ≈ 0, it does not affect EPI. |
| 86 | + """ |
| 87 | + G, node = create_nfr("frozen", epi=0.50, vf=1.00) |
| 88 | + G.nodes[node][DNFR_PRIMARY] = 0.15 # High reorganization pressure |
| 89 | + initial_dnfr = G.nodes[node][DNFR_PRIMARY] |
| 90 | + |
| 91 | + # Apply SHA |
| 92 | + run_sequence(G, node, [Silence()]) |
| 93 | + |
| 94 | + final_dnfr = G.nodes[node][DNFR_PRIMARY] |
| 95 | + |
| 96 | + # ΔNFR can remain high, but SHA should not actively change it |
| 97 | + # The key is that with νf ≈ 0, ΔNFR does not affect EPI |
| 98 | + assert abs(final_dnfr - initial_dnfr) < 0.05, ( |
| 99 | + "SHA should not actively modify ΔNFR" |
| 100 | + ) |
| 101 | + |
| 102 | + |
| 103 | +class TestSHAPreconditions: |
| 104 | + """Test B: Precondition validation for SHA operator.""" |
| 105 | + |
| 106 | + def test_sha_precondition_vf_minimum(self): |
| 107 | + """Test 4: SHA must fail if νf already at minimum.""" |
| 108 | + G, node = create_nfr("already_silent", epi=0.40, vf=0.005) |
| 109 | + G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True |
| 110 | + |
| 111 | + with pytest.raises(OperatorPreconditionError, match="already minimal"): |
| 112 | + run_sequence(G, node, [Silence()]) |
| 113 | + |
| 114 | + def test_sha_requires_existing_epi(self): |
| 115 | + """Test 5: SHA should warn if EPI ≈ 0 (no structure to preserve).""" |
| 116 | + G, node = create_nfr("empty", epi=0.0, vf=1.0) |
| 117 | + G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True |
| 118 | + |
| 119 | + # SHA on empty structure should issue warning |
| 120 | + # This is more of a semantic warning than hard failure |
| 121 | + with pytest.warns(UserWarning, match="no structure|empty|zero"): |
| 122 | + run_sequence(G, node, [Silence()]) |
| 123 | + |
| 124 | + |
| 125 | +class TestSHACanonicalSequences: |
| 126 | + """Test C: Canonical operator sequences involving SHA.""" |
| 127 | + |
| 128 | + def test_sha_after_coherence_preserves_stability(self): |
| 129 | + """Test 6: IL → SHA (stabilize then preserve) - canonical memory pattern.""" |
| 130 | + G, node = create_nfr("learning", epi=0.45, vf=1.10) |
| 131 | + G.nodes[node][DNFR_PRIMARY] = 0.20 # High initial pressure |
| 132 | + |
| 133 | + # IL reduces ΔNFR, stabilizes |
| 134 | + run_sequence(G, node, [Coherence()]) |
| 135 | + post_il_dnfr = G.nodes[node][DNFR_PRIMARY] |
| 136 | + post_il_epi = G.nodes[node][EPI_PRIMARY] |
| 137 | + |
| 138 | + # SHA preserves the stabilized state |
| 139 | + run_sequence(G, node, [Silence()]) |
| 140 | + |
| 141 | + assert G.nodes[node][VF_PRIMARY] < 0.1, "SHA reduces νf" |
| 142 | + assert abs(G.nodes[node][EPI_PRIMARY] - post_il_epi) < 0.05, "EPI preserved" |
| 143 | + |
| 144 | + def test_sha_to_emission_reactivation(self): |
| 145 | + """Test 7: SHA → NAV → AL (reactivation from silence) - structurally coherent awakening. |
| 146 | + |
| 147 | + TNFR Physics: Cannot jump zero → high (SHA → AL) directly. |
| 148 | + Must transition through medium frequency: SHA → NAV → AL (zero → medium → high). |
| 149 | + This respects structural continuity and prevents singularities. |
| 150 | + """ |
| 151 | + G, node = create_nfr("sleeping", epi=0.55, vf=1.00) |
| 152 | + |
| 153 | + # Phase 1: Prepare and enter silence |
| 154 | + run_sequence(G, node, [Emission(), Coherence(), Silence()]) |
| 155 | + assert G.nodes[node][VF_PRIMARY] < 0.1, "Node in silence" |
| 156 | + silent_epi = G.nodes[node][EPI_PRIMARY] |
| 157 | + |
| 158 | + # Phase 2: Reactivate through medium frequency (NAV) then high (AL) |
| 159 | + run_sequence(G, node, [Transition(), Emission()]) |
| 160 | + |
| 161 | + # Validate coherent reactivation |
| 162 | + assert G.nodes[node][VF_PRIMARY] > 0.5, "Node reactivated" |
| 163 | + assert G.nodes[node][EPI_PRIMARY] >= silent_epi - 0.15, "EPI maintains structural identity" |
| 164 | + |
| 165 | + def test_sha_to_transition_controlled_change(self): |
| 166 | + """Test 8: SHA → NAV (controlled transition from silence).""" |
| 167 | + G, node = create_nfr("dormant", epi=0.48, vf=0.95) |
| 168 | + |
| 169 | + # SHA: Preserve structure |
| 170 | + run_sequence(G, node, [Silence()]) |
| 171 | + preserved_epi = G.nodes[node][EPI_PRIMARY] |
| 172 | + |
| 173 | + # NAV: Transition from silence |
| 174 | + run_sequence(G, node, [Transition()]) |
| 175 | + |
| 176 | + # Validate controlled transition without collapse |
| 177 | + assert G.nodes[node][VF_PRIMARY] > 0.1, "Node reactivating" |
| 178 | + assert abs(G.nodes[node][EPI_PRIMARY] - preserved_epi) < 0.2, ( |
| 179 | + "EPI transitions controlledly without collapse" |
| 180 | + ) |
| 181 | + |
| 182 | + def test_oz_to_sha_containment(self): |
| 183 | + """Test 9: OZ → SHA (dissonance contained) - therapeutic pause. |
| 184 | + |
| 185 | + Clinical use case: Trauma containment, conflict deferred. |
| 186 | + """ |
| 187 | + G, node = create_nfr("trauma", epi=0.40, vf=1.00) |
| 188 | + G.nodes[node][DNFR_PRIMARY] = 0.05 |
| 189 | + |
| 190 | + # OZ: Introduce dissonance |
| 191 | + run_sequence(G, node, [Dissonance()]) |
| 192 | + post_oz_dnfr = G.nodes[node][DNFR_PRIMARY] |
| 193 | + assert post_oz_dnfr > 0.10, "Dissonance increases ΔNFR" |
| 194 | + |
| 195 | + # SHA: Contain dissonance (protective pause) |
| 196 | + run_sequence(G, node, [Silence()]) |
| 197 | + |
| 198 | + # Validate containment |
| 199 | + assert G.nodes[node][VF_PRIMARY] < 0.1, "Node paused" |
| 200 | + # ΔNFR remains high but frozen |
| 201 | + assert G.nodes[node][DNFR_PRIMARY] > 0.10, "Dissonance contained (not resolved)" |
| 202 | + |
| 203 | + |
| 204 | +class TestSHAMetrics: |
| 205 | + """Test D: SHA-specific metrics collection.""" |
| 206 | + |
| 207 | + def test_sha_metrics_preservation(self): |
| 208 | + """Test 10: Validate that silence_metrics captures preservation correctly.""" |
| 209 | + G, node = create_nfr("test", epi=0.60, vf=1.00) |
| 210 | + G.graph["COLLECT_OPERATOR_METRICS"] = True |
| 211 | + |
| 212 | + run_sequence(G, node, [Silence()]) |
| 213 | + |
| 214 | + # Check if metrics were collected |
| 215 | + if "operator_metrics" in G.graph: |
| 216 | + metrics = G.graph["operator_metrics"][-1] |
| 217 | + |
| 218 | + assert metrics["operator"] == "Silence", "Operator name recorded" |
| 219 | + assert metrics["glyph"] == "SHA", "Glyph recorded" |
| 220 | + |
| 221 | + # Check for SHA-specific metric keys |
| 222 | + assert "vf_reduction" in metrics or "vf_final" in metrics, ( |
| 223 | + "νf reduction metric present" |
| 224 | + ) |
| 225 | + |
| 226 | + |
| 227 | +class TestSHAIntegration: |
| 228 | + """Test E: Integration and network effects.""" |
| 229 | + |
| 230 | + def test_sha_does_not_affect_neighbors(self): |
| 231 | + """Test 11: SHA is local operation - no direct propagation to neighbors.""" |
| 232 | + G, n1 = create_nfr("node1", epi=0.50, vf=1.00) |
| 233 | + |
| 234 | + # Add second node manually |
| 235 | + _, n2 = create_nfr("node2", epi=0.50, vf=1.00) |
| 236 | + # Import n2's attributes into G |
| 237 | + G.add_node(n2) |
| 238 | + set_attr(G.nodes[n2], ALIAS_EPI, 0.50) |
| 239 | + set_attr(G.nodes[n2], ALIAS_VF, 1.00) |
| 240 | + G.add_edge(n1, n2) # Connect nodes |
| 241 | + |
| 242 | + initial_n2_vf = G.nodes[n2][VF_PRIMARY] |
| 243 | + |
| 244 | + # SHA on n1 |
| 245 | + run_sequence(G, n1, [Silence()]) |
| 246 | + |
| 247 | + # n2 must remain active |
| 248 | + assert G.nodes[n1][VF_PRIMARY] < 0.1, "n1 in silence" |
| 249 | + assert G.nodes[n2][VF_PRIMARY] >= initial_n2_vf * 0.9, ( |
| 250 | + "n2 remains active (SHA is local)" |
| 251 | + ) |
| 252 | + |
| 253 | + def test_sha_after_complex_sequence(self): |
| 254 | + """Test 12: SHA as closure of complex sequence. |
| 255 | + |
| 256 | + Sequence: AL → IL → RA → UM → SHA |
| 257 | + """ |
| 258 | + G, node = create_nfr("complex", epi=0.30, vf=0.80) |
| 259 | + |
| 260 | + sequence = [ |
| 261 | + Emission(), # AL: Activate |
| 262 | + Coherence(), # IL: Stabilize |
| 263 | + Resonance(), # RA: Propagate |
| 264 | + Coupling(), # UM: Couple |
| 265 | + Silence() # SHA: Close and preserve |
| 266 | + ] |
| 267 | + |
| 268 | + initial_epi = G.nodes[node][EPI_PRIMARY] |
| 269 | + run_sequence(G, node, sequence) |
| 270 | + |
| 271 | + # Validate final state |
| 272 | + assert G.nodes[node][VF_PRIMARY] < 0.1, "Sequence closed with silence" |
| 273 | + assert G.nodes[node][EPI_PRIMARY] >= initial_epi, ( |
| 274 | + "EPI evolved during sequence" |
| 275 | + ) |
| 276 | + |
| 277 | + |
| 278 | +class TestSHANodalEquation: |
| 279 | + """Test F: Nodal equation validation for SHA.""" |
| 280 | + |
| 281 | + def test_sha_nodal_equation_validation(self): |
| 282 | + """Test 13: Validate SHA respects nodal equation: ∂EPI/∂t = νf · ΔNFR(t). |
| 283 | + |
| 284 | + If νf → 0, then |∂EPI/∂t| → 0 |
| 285 | + """ |
| 286 | + G, node = create_nfr("validate", epi=0.65, vf=1.30) |
| 287 | + G.nodes[node][DNFR_PRIMARY] = 0.25 # High pressure |
| 288 | + |
| 289 | + epi_before = G.nodes[node][EPI_PRIMARY] |
| 290 | + |
| 291 | + # SHA should work even with high ΔNFR |
| 292 | + run_sequence(G, node, [Silence()]) |
| 293 | + |
| 294 | + epi_after = G.nodes[node][EPI_PRIMARY] |
| 295 | + vf_after = G.nodes[node][VF_PRIMARY] |
| 296 | + |
| 297 | + # ∂EPI/∂t ≈ νf_after · ΔNFR ≈ 0 (because νf ≈ 0) |
| 298 | + delta_epi = abs(epi_after - epi_before) |
| 299 | + |
| 300 | + # With νf ≈ 0, EPI change should be minimal regardless of ΔNFR |
| 301 | + assert delta_epi < 0.1, ( |
| 302 | + f"Nodal equation respected: ΔEPI = {delta_epi} should be small with νf ≈ 0" |
| 303 | + ) |
| 304 | + |
| 305 | + |
| 306 | +class TestSHAFullLifecycle: |
| 307 | + """Test G: Complete lifecycle including SHA.""" |
| 308 | + |
| 309 | + def test_sha_full_cycle_activation_silence_reactivation(self): |
| 310 | + """Test 14: Complete cycle: AL → IL → SHA → NAV → AL. |
| 311 | + |
| 312 | + Simulates: learning → consolidation → memory → recall → use |
| 313 | + """ |
| 314 | + G, node = create_nfr("lifecycle", epi=0.25, vf=0.90) |
| 315 | + |
| 316 | + # Phase 1: Activation and stabilization (learning) |
| 317 | + run_sequence(G, node, [Emission(), Coherence()]) |
| 318 | + post_learning_epi = G.nodes[node][EPI_PRIMARY] |
| 319 | + assert post_learning_epi > 0.25, "Learning increments EPI" |
| 320 | + |
| 321 | + # Phase 2: Consolidation in silence (memory formation) |
| 322 | + run_sequence(G, node, [Silence()]) |
| 323 | + assert G.nodes[node][VF_PRIMARY] < 0.1, "Memory consolidated" |
| 324 | + memory_epi = G.nodes[node][EPI_PRIMARY] |
| 325 | + |
| 326 | + # Phase 3: Transition and reactivation (recall) |
| 327 | + run_sequence(G, node, [Transition(), Emission()]) |
| 328 | + |
| 329 | + # Validate memory preservation and reactivation |
| 330 | + assert abs(G.nodes[node][EPI_PRIMARY] - memory_epi) < 0.2, ( |
| 331 | + "Structural identity preserved through silence cycle" |
| 332 | + ) |
| 333 | + assert G.nodes[node][VF_PRIMARY] > 0.5, "Node active again" |
| 334 | + |
| 335 | + |
| 336 | +class TestSHALatencyStateTracking: |
| 337 | + """Additional tests for SHA latency state attributes.""" |
| 338 | + |
| 339 | + def test_sha_sets_latency_attributes(self): |
| 340 | + """Validate SHA sets latency state tracking attributes.""" |
| 341 | + G, node = create_nfr("latency_test", epi=0.50, vf=1.00) |
| 342 | + |
| 343 | + run_sequence(G, node, [Silence()]) |
| 344 | + |
| 345 | + # Check latency attributes |
| 346 | + assert G.nodes[node].get("latent") == True, "Latent flag set" |
| 347 | + assert "latency_start_time" in G.nodes[node], "Start time recorded" |
| 348 | + assert "preserved_epi" in G.nodes[node], "EPI preserved" |
| 349 | + assert G.nodes[node]["preserved_epi"] == pytest.approx(0.50, abs=0.01), ( |
| 350 | + "Preserved EPI matches initial" |
| 351 | + ) |
| 352 | + assert "silence_duration" in G.nodes[node], "Duration tracker initialized" |
| 353 | + |
| 354 | + def test_sha_preserved_epi_matches_current(self): |
| 355 | + """Validate preserved_epi attribute matches actual EPI at silence entry.""" |
| 356 | + G, node = create_nfr("preserve_test", epi=0.73, vf=1.10) |
| 357 | + |
| 358 | + # Apply some operators first |
| 359 | + run_sequence(G, node, [Emission(), Coherence()]) |
| 360 | + epi_before_silence = G.nodes[node][EPI_PRIMARY] |
| 361 | + |
| 362 | + # Apply SHA |
| 363 | + run_sequence(G, node, [Silence()]) |
| 364 | + |
| 365 | + preserved_epi = G.nodes[node].get("preserved_epi", 0.0) |
| 366 | + assert abs(preserved_epi - epi_before_silence) < 0.01, ( |
| 367 | + "Preserved EPI must match EPI at silence entry" |
| 368 | + ) |
0 commit comments