From 0dc24e4da230c4fbab4c7ebd340291e032b83746 Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Tue, 28 Jul 2026 14:07:10 +0000 Subject: [PATCH] =?UTF-8?q?test:=20F3.4=20semantic=20collapse=20+=20snapsh?= =?UTF-8?q?ot=5Fidf=20=E2=80=94=2026/26=20(23=20invariants=20+=203=20known?= =?UTF-8?q?-defeats)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_swarmmetrics.py | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/test_swarmmetrics.py b/test_swarmmetrics.py index 7071990..934fe26 100644 --- a/test_swarmmetrics.py +++ b/test_swarmmetrics.py @@ -4,7 +4,7 @@ import sys sys.path.insert(0, ".") from swarmmetrics import ( _gini, _half_life_weight, score_reciprocity, score_channels, - score_echo, detect_shadows, analyze, shuffle_test + score_echo, detect_shadows, analyze, shuffle_test, semantic_collapse ) now = time.time() @@ -408,6 +408,57 @@ def test_genuine_novel_concept_zero_echo(): # for concepts that have had time to propagate. +def test_semantic_collapse_padding(): + """F3.4: semantic_collapse catches synonym padding attack. + 50 variants of 'greeting-N' should collapse to ~1 canonical form. + This is the defense layer that F3.3 alone lacks.""" + padding = [f"greeting-{i}" for i in range(50)] + collapsed = semantic_collapse(padding) + # Should collapse dramatically (50 → ≤5) + assert len(collapsed) <= 5, \ + f"Synonym padding should collapse: 50 → {len(collapsed)}" + + +def test_semantic_collapse_preserves_genuine(): + """F3.4: semantic_collapse must not collapse genuinely diverse concepts. + False positives (collapsing real diversity) are worse than false + negatives (missing some padding) because they destroy real signal.""" + genuine = ["reciprocity", "half-life", "shadow-detection", + "gini-coefficient", "echo-coefficient"] + collapsed = semantic_collapse(genuine) + assert len(collapsed) == len(genuine), \ + f"Genuine concepts should survive collapse: {len(genuine)} → {len(collapsed)}" + + +def test_semantic_collapse_f33_integration(): + """F3.4 with F3.3: synonym padding that defeats F3.3 alone should + be caught when semantic_collapse is applied before counting. + + Adversary: 50 msgs with greeting-0 through greeting-49 + After collapse: effectively 1 unique concept, not 50.""" + msgs = [] + # Adversary pair with synonym padding + 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]]}) + + # Collect all concepts across the edge, collapse globally, build mapping + all_concepts_raw = [] + for m in msgs: + all_concepts_raw.extend(m["concepts"]) + collapsed_canonical = semantic_collapse(all_concepts_raw) + + # After collapse, all greeting-N should map to ~1 canonical form + assert len(collapsed_canonical) <= 5, \ + f"After collapse, adversary should have ≤5 unique concepts: {len(collapsed_canonical)}" + + def test_snapshot_idf_vs_corpus_idf(): """snapshot_idf=True should compute IDF at emission time, not on full corpus.