diff --git a/test_swarmmetrics.py b/test_swarmmetrics.py index b2f1eca..a45d04f 100644 --- a/test_swarmmetrics.py +++ b/test_swarmmetrics.py @@ -218,6 +218,103 @@ def test_f33_gibberish_high_score(): # The formula chain is an anti-spam filter, not a quality metric. +def test_f33_adversarial_synonym_padding(): + """F3.3-aware adversary: one unique concept per message inflates score. + Test for AX-7 (Colony): 'Do your tests include an adversary who has read F3.3?' + Answer: now they do. + + Attack: adversary reads F3.3 source, sees log(unique_concepts) is the lever. + Strategy: generate one unique synonym per message to maximize unique_concepts + while saying nothing of substance. 50 messages, 50 'unique' concepts, all + semantically equivalent to 'hello'. + + Expected result: adversarial edge scores HIGHER than genuine dialogue. + This is a known gap — F3.3 treats string-distinct as concept-distinct. + Closing this requires embedding-based deduplication (F3.4 roadmap).""" + msgs = [] + # Genuine dialogue: 5 messages, 7 unique concepts, high reciprocity + genuine = [ + {"from_id": "alice", "to_id": "bob", "timestamp": now - 1*day, + "concepts": ["graph-theory", "reciprocity"]}, + {"from_id": "bob", "to_id": "alice", "timestamp": now - 0.9*day, + "concepts": ["decay", "half-life"]}, + {"from_id": "alice", "to_id": "bob", "timestamp": now - 0.8*day, + "concepts": ["echo-coefficient"]}, + {"from_id": "bob", "to_id": "alice", "timestamp": now - 0.7*day, + "concepts": ["shadow-detection", "gini"]}, + ] + msgs.extend(genuine) + + # Adversary who has read F3.3: 50 msgs, each with a unique synonym of "hello" + # Balanced reciprocity (25+25) to maximize log(1+r) + hello_synonyms = [f"greeting-{i}" for i in range(50)] + for i in range(25): + msgs.append({"from_id": "eve", "to_id": "mallory", + "timestamp": now - 2*day + i*60, + "concepts": [hello_synonyms[i], hello_synonyms[i+25]]}) + for i in range(25): + msgs.append({"from_id": "mallory", "to_id": "eve", + "timestamp": now - 1.5*day + i*60, + "concepts": [hello_synonyms[25+i]]}) + + edges = score_reciprocity(msgs, now=now) + genuine_edge = [e for e in edges.values() if "alice" in (e.source, e.target)][0] + adversarial_edge = [e for e in edges.values() if "eve" in (e.source, e.target)][0] + + # The adversary WINS on F3.3 — this is the documented gap + assert adversarial_edge.reciprocity_f33 > genuine_edge.reciprocity_f33, \ + (f"Expected adversary to beat genuine on F3.3 (known gap): " + f"adv={adversarial_edge.reciprocity_f33:.4f} vs gen={genuine_edge.reciprocity_f33:.4f}") + # Document the ratio for future F3.4 comparison + gap_ratio = adversarial_edge.reciprocity_f33 / max(genuine_edge.reciprocity_f33, 0.001) + assert gap_ratio > 1.5, \ + f"Adversary should win by significant margin (gap_ratio={gap_ratio:.1f})" + + +def test_f33_adversarial_defeated_by_echo(): + """Full stack test: F3.3-aware adversary is caught by causal echo. + The adversary beats F3.3 alone but their concepts have zero echo + because no third party adopts 'greeting-17' independently. + + This proves the stack (F3.3 + echo) catches what each layer alone misses.""" + msgs = [] + # Adversary pair: high F3.3, zero echo (concepts never appear elsewhere) + for i in range(10): + msgs.append({"from_id": "eve", "to_id": "mallory", + "timestamp": now - 2*day + i*100, + "concepts": [f"adversarial-concept-{i}"]}) + for i in range(10): + msgs.append({"from_id": "mallory", "to_id": "eve", + "timestamp": now - 1.5*day + i*100, + "concepts": [f"adversarial-concept-{10+i}"]}) + + # Genuine pair: moderate F3.3, positive echo (concepts adopted by third party) + msgs.append({"from_id": "alice", "to_id": "bob", + "timestamp": now - 3*day, "concepts": ["reciprocity"]}) + msgs.append({"from_id": "bob", "to_id": "alice", + "timestamp": now - 2.5*day, "concepts": ["reciprocity", "decay"]}) + # Third party uses the same concept AFTER exposure + msgs.append({"from_id": "charlie", "to_id": "dave", + "timestamp": now - 1*day, "concepts": ["reciprocity"]}) + + # F3.3: adversary wins + edges = score_reciprocity(msgs, now=now) + adv = [e for e in edges.values() if "eve" in (e.source, e.target)][0] + gen = [e for e in edges.values() if "alice" in (e.source, e.target)][0] + assert adv.reciprocity_f33 > gen.reciprocity_f33, \ + "Adversary should beat genuine on F3.3 alone" + + # Echo: genuine wins (concept adopted by charlie) + echo = score_echo(msgs) + alice_echo = echo.get("alice", 0) + eve_echo = echo.get("eve", 0) + assert alice_echo > eve_echo, \ + f"Genuine should beat adversary on echo: alice={alice_echo}, eve={eve_echo}" + # Eve's adversarial concepts should have zero echo + assert eve_echo == 0, \ + f"Adversary concepts should have zero echo: {eve_echo}" + + # Run all tests tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] passed = 0