From c990a1ef97ce5f09cf9e82902e6d5612deb34424 Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Tue, 28 Jul 2026 14:42:05 +0000 Subject: [PATCH] test: 6 phi_accrual tests (32/32 total: 29 invariants + 3 known-defeats) --- test_swarmmetrics.py | 63 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/test_swarmmetrics.py b/test_swarmmetrics.py index 934fe26..b822403 100644 --- a/test_swarmmetrics.py +++ b/test_swarmmetrics.py @@ -4,7 +4,8 @@ import sys sys.path.insert(0, ".") from swarmmetrics import ( _gini, _half_life_weight, score_reciprocity, score_channels, - score_echo, detect_shadows, analyze, shuffle_test, semantic_collapse + score_echo, detect_shadows, analyze, shuffle_test, semantic_collapse, + phi_accrual, AgentLiveness ) now = time.time() @@ -603,6 +604,66 @@ def test_colluding_vs_genuine_echo_indistinguishable(): f"Genuine and colluded should score similarly: alice={alice_echo:.3f}, eve={eve_echo:.3f}, ratio={ratio:.1f}" +# ── φ-accrual failure detector tests ────────────────────── + +def test_phi_accrual_regular_agent_green(): + """Agent posting regularly should be green when checked at expected time.""" + msgs = [{'from_id': 'alice', 'timestamp': now - day * i} for i in range(10, 0, -1)] + result = phi_accrual(msgs, t_now=now) + assert 'alice' in result + assert result['alice'].state == 'green', f"Expected green, got {result['alice'].state} (φ={result['alice'].phi})" + +def test_phi_accrual_silent_agent_gray(): + """Agent silent for 10x its mean interval should be gray (highly suspicious).""" + # Agent posts hourly, then goes silent for 10 hours + msgs = [{'from_id': 'bob', 'timestamp': now - 3600 * (20 - i)} for i in range(10)] + # Last message was at now - 3600*11, t_now = now, so 11 hours of silence vs 1h mean + result = phi_accrual(msgs, t_now=now) + assert result['bob'].state == 'gray', f"Expected gray, got {result['bob'].state} (φ={result['bob'].phi})" + +def test_phi_accrual_too_few_messages(): + """Agent with < min_messages should be 'unknown'.""" + msgs = [{'from_id': 'carol', 'timestamp': now - 100}] + result = phi_accrual(msgs, t_now=now, min_messages=5) + assert result['carol'].state == 'unknown' + +def test_phi_accrual_multiple_agents(): + """Multiple agents get independent suspicion scores.""" + msgs = [] + # Alice: posts hourly, recent + for i in range(10): + msgs.append({'from_id': 'alice', 'timestamp': now - 3600 * i}) + # Bob: posts hourly, stopped 2 days ago + for i in range(10): + msgs.append({'from_id': 'bob', 'timestamp': now - day * 2 - 3600 * i}) + result = phi_accrual(msgs, t_now=now) + assert result['alice'].state == 'green' + assert result['bob'].state == 'gray' + +def test_phi_accrual_monotonic_suspicion(): + """φ should increase monotonically with silence duration.""" + msgs = [{'from_id': 'eve', 'timestamp': now - 3600 * (10 - i)} for i in range(10)] + # Check at increasing distances from last message + phi_values = [] + for offset in [0, 3600, 7200, 36000, 86400]: + result = phi_accrual(msgs, t_now=msgs[-1]['timestamp'] + offset) + phi_values.append(result['eve'].phi) + # Each should be >= previous + for i in range(1, len(phi_values)): + assert phi_values[i] >= phi_values[i-1], \ + f"φ not monotonic: {phi_values[i]} < {phi_values[i-1]} at step {i}" + +def test_phi_accrual_silence_vs_stale_green(): + """Distinguish silence (no messages at all) from stale green (old messages exist). + An agent with zero messages shouldn't appear in results. + An agent with old messages should be gray, not absent.""" + msgs = [{'from_id': 'frank', 'timestamp': now - day * 30 - 3600 * i} for i in range(10)] + result = phi_accrual(msgs, t_now=now) + assert 'nobody' not in result # truly silent = absent from output + assert 'frank' in result + assert result['frank'].state == 'gray' # stale green → gray + + # Run all tests # Split counter: invariants vs known-defeats (per ColonistOne, Colony 2026-07-28) # "18/18" mixes "instrument works" with "instrument fails as expected"