Nazanin Andalibi

74 papers A* 12A 4B 3Misc 1Journal 36Unranked 18
YearRankTypeTitle / Venue / Authors
2025 conf
FAccT
Oliver L. Haimson, Samuel Reiji Mayworm, Alexis Shore Ingber, Nazanin Andalibi
2025 J jnl
Proc. ACM Hum. Comput. Interact.
Cassidy Pyle, Nazanin Andalibi
2025 A conf
Conference on Designing Interactive Systems
Cassidy Pyle, Nicole B. Ellison, Nazanin Andalibi
2025 conf
FAccT
Alexis Shore Ingber, Oliver L. Haimson, Nazanin Andalibi
2025 J jnl
Interactions
Nazanin Andalibi
2025 conf
FAccT
Alexis Shore Ingber, Nazanin Andalibi
2025 J jnl
Proc. ACM Hum. Comput. Interact.
Nadia Karizat, Nora McDonald, Nazanin Andalibi
2025 A* conf
CHI
Nazanin Andalibi, Alexis Shore Ingber
2025 conf
FAccT
Alexis Shore Ingber, Nazanin Andalibi
2025 conf
FAccT
Shreya Chowdhary, Alexis Shore Ingber, Nazanin Andalibi
2025 J jnl
Proc. ACM Hum. Comput. Interact.
Nadia Karizat, Nazanin Andalibi
2025 J jnl
ACM Trans. Comput. Hum. Interact.
Nora McDonald, Alan F. Luo, Phoebe Moh, Michelle L. Mazurek, Nazanin Andalibi
2025 J jnl
CoRR
Rada Mihalcea, Nazanin Andalibi, David Jensen, Matthew Turk, Pamela J. Wisniewski, Holly A. Yanco
2024 J jnl
Proc. ACM Hum. Comput. Interact.
Cassidy Pyle, Ben Zefeng Zhang, Oliver L. Haimson, Nazanin Andalibi
2024 J jnl
Proc. ACM Hum. Comput. Interact.
Kat Roemmich, Shanley Corvite, Cassidy Pyle, Nadia Karizat, Nazanin Andalibi
2024 conf
CSCW Companion
Vishal Sharma, Neha Kumar, Pejman Mirza-Babaei, Aakash Gautam, Cindy Lin, Volker Wulf, Nazanin Andalibi
2024 J jnl
Proc. ACM Hum. Comput. Interact.
Nadia Karizat, Alexandra H. Vinson, Shobita Parthasarathy, Nazanin Andalibi
2024 conf
FAccT
Nari Johnson, Sanika Moharana, Christina N. Harrington, Nazanin Andalibi, Hoda Heidari, Motahhare Eslami
2024 J jnl
CoRR
Nari Johnson, Sanika Moharana, Christina N. Harrington, Nazanin Andalibi, Hoda Heidari, Motahhare Eslami
2024 J jnl
ACM Trans. Comput. Hum. Interact.
Kristen Barta, Nazanin Andalibi
2024 J jnl
Proc. ACM Hum. Comput. Interact.
Cassidy Pyle, Kat Roemmich, Nazanin Andalibi
2024 conf
CSCW Companion
Nazanin Andalibi, Luke Stark, Daniel McDuff, Rosalind W. Picard, Jonathan Gratch, Noura Howell
2023 J jnl
ACM Trans. Comput. Hum. Interact.
Nora McDonald, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Nadia Karizat, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Karen L. Boyd, Nazanin Andalibi
2023 A* conf
CHI
Nazanin Andalibi, Cassidy Pyle, Kristen Barta, Lu Xian, Abigail Z. Jacobs, Mark S. Ackerman
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Shanley Corvite, Kat Roemmich, Tillie Ilana Rosenberg, Nazanin Andalibi
2023 A* conf
CHI
Kat Roemmich, Florian Schaub, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Kristen Barta, Katelyn Wolberg, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Cassidy Pyle, Nicole B. Ellison, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Kristen Barta, Cassidy Pyle, Nazanin Andalibi
2023 conf
CSCW Companion
Casey Randazzo, Carol F. Scott, Rosanna Bellini, Tawfiq Ammari, Michael Ann DeVito, Bryan C. Semaan, Nazanin Andalibi
2023 J jnl
Proc. ACM Hum. Comput. Interact.
Kat Roemmich, Tillie Rosenberg, Serena Fan, Nazanin Andalibi
2022 J jnl
Proc. ACM Hum. Comput. Interact.
Gabriel Grill, Nazanin Andalibi
2022 J jnl
ACM Trans. Comput. Hum. Interact.
Nazanin Andalibi, Ashley Lacombe-Duncan, Lee Roosevelt, Kylie Wojciechowski, Cameron Giniel
2022 J jnl
Cyberpsychology Behav. Soc. Netw.
Katherine J. Gold, Martha E. Boggs, Melissa A. Plegue, Nazanin Andalibi
2022 conf
CHI Extended Abstracts
Jessica L. Feuston, Arpita Bhattacharya, Nazanin Andalibi, Elizabeth A. Ankrah, Sheena Erete, Mark Handel, Wendy Moncur, Sarah Vieweg, Jed R. Brubaker
2022 J jnl
Proc. ACM Hum. Comput. Interact.
Ben Zefeng Zhang, Tianxiao Liu, Shanley Corvite, Nazanin Andalibi, Oliver L. Haimson
2021 J jnl
Proc. ACM Hum. Comput. Interact.
Nadia Karizat, Daniel Delmonaco, Motahhare Eslami, Nazanin Andalibi
2021 J jnl
Proc. ACM Hum. Comput. Interact.
Kristen Barta, Nazanin Andalibi
2021 J jnl
Proc. ACM Hum. Comput. Interact.
Kat Roemmich, Nazanin Andalibi
2021 A* conf
CHI
Cassidy Pyle, Lee Roosevelt, Ashley Lacombe-Duncan, Nazanin Andalibi
2021 J jnl
Proc. ACM Hum. Comput. Interact.
Nazanin Andalibi, Patricia Garcia
2021 J jnl
New Media Soc.
Nazanin Andalibi
2020 J jnl
ACM Trans. Comput. Hum. Interact.
Nazanin Andalibi
2020 A* conf
CHI
Nazanin Andalibi, Justin Buss
2019 J jnl
Proc. ACM Hum. Comput. Interact.
Danielle M. Lottridge, Nazanin Andalibi, Joy Kim, Jofish Kaye
2019 conf
CHI Extended Abstracts
Stevie Chancellor, Nazanin Andalibi, Lindsay Blackwell, David Nemer, Wendy Moncur
2019 A* conf
CHI
Nazanin Andalibi
2018 A* conf
CHI
Nazanin Andalibi, Andrea Forte
2018 conf
ASIST
Blake W. Hawkins, Nazanin Andalibi, Oliver L. Haimson
2018 B conf
GROUP
Andrea Forte, Nazanin Andalibi, Tim Gorichanaz, Meen Chul Kim, Thomas H. Park, Aaron Halfaker
2018 J jnl
New Media Soc.
Oliver L. Haimson, Nazanin Andalibi, Munmun De Choudhury, Gillian R. Hayes
2018 J jnl
ACM Trans. Comput. Hum. Interact.
Nazanin Andalibi, Andrea Forte
2018 J jnl
ACM Trans. Comput. Hum. Interact.
Nazanin Andalibi, Oliver L. Haimson, Munmun De Choudhury, Andrea Forte
2018 J jnl
Proc. ACM Hum. Comput. Interact.
Nazanin Andalibi, Margaret E. Morris, Andrea Forte
2017 A* conf
CHI
Frank Bentley, Nediyana Daskalova, Nazanin Andalibi
2017 A* conf
CHI
Xiao Ma, Nazanin Andalibi, Louise Barkhuus, Mor Naaman
2017 conf
CHI Extended Abstracts
Scott P. Robertson, Nazanin Andalibi, Nicholas Diakopoulos, Andrea Forte, Misa Maruyama, Nova Ahmed, Hrvoje Benko, Susan M. Dray, Geraldine Fitzpatrick, Rubaiat Habib, Björn Hartmann, Karen Holtzblatt, Elizabeth Rosenzweig, Preethi Srinivas, Doug Zytko
2017 A* conf
CHI
Eugenia Ha Rim Rho, Oliver L. Haimson, Nazanin Andalibi, Melissa Mazmanian, Gillian R. Hayes
2017 conf
CHI Extended Abstracts
Nediyana Daskalova, Frank R. Bentley, Nazanin Andalibi
2017 J jnl
Proc. ACM Hum. Comput. Interact.
Nazanin Andalibi, Frank Bentley, Katie Quehl
2017 A conf
CSCW
Andrea Forte, Nazanin Andalibi, Rachel Greenstadt
2017 conf
CHI Extended Abstracts
Nazanin Andalibi
2017 A conf
CSCW
Nazanin Andalibi, Pinar Öztürk, Andrea Forte
2016 A conf
CSCW
Jessica Annette Pater, Oliver L. Haimson, Nazanin Andalibi, Elizabeth D. Mynatt
2016 B conf
GROUP
Casey Fiesler, Pamela J. Wisniewski, Jessica Pater, Nazanin Andalibi
2016 Misc conf
NordiCHI
Daniel Herron, Nazanin Andalibi, Oliver L. Haimson, Wendy Moncur, Elise van den Hoven
2016 B conf
GROUP
Nazanin Andalibi
2016 conf
PervasiveHealth
Gabriela Marcu, Nadia Dowshen, Shuvadittya Saha, Ressa Reneth Sarreal, Nazanin Andalibi
2016 A* conf
CHI
Nazanin Andalibi, Oliver L. Haimson, Munmun De Choudhury, Andrea Forte
2015 conf
CSCW Companion
Nazanin Andalibi, Pinar Öztürk, Andrea Forte
2014 A* conf
CHI
Andrea Forte, Nazanin Andalibi, Thomas H. Park, Heather Willever-Farr
2010 conf
ICUMT
Hamid R. Darabi, Mo Mansouri, Nazanin Andalibi, Eileen Para
tests/unit/test_cfg_features.py
← Index tests/unit/test_cfg_features.py python
"""
Unit tests for cfg_features.py — all new CFG feature computations.

These tests use plain Python data structures (index-based adjacency lists)
and require no Binary Ninja dependency.
"""
import pytest

from redb.extractors.decompiler.bninja.analysis.cfg_features import (
    bfs_order,
    bfs_max_depth,
    count_back_edges,
    compute_topology_hash,
    compute_md_index_topdown,
    compute_md_index_bottomup,
    compute_prime_product,
    build_block_features,
    compute_cfg_feature_tlsh,
    compute_wl_minhash,
    pack_adjacency,
    LLIL_OP_CATEGORIES,
    CAT_ARITHMETIC,
    CAT_LOGIC,
    CAT_CALL,
    CAT_MEMORY,
    NUM_WL_MINHASH_PERMS,
)


# ===================================================================
# Helper: common graph topologies
# ===================================================================

def _linear_chain(n):
    """0 -> 1 -> 2 -> ... -> (n-1)"""
    return [[i + 1] if i < n - 1 else [] for i in range(n)]


def _diamond():
    """
    0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
    (classic if/else diamond)
    """
    return [[1, 2], [3], [3], []]


def _predecessors_from_successors(successors, n):
    preds = [[] for _ in range(n)]
    for src, targets in enumerate(successors):
        for tgt in targets:
            preds[tgt].append(src)
    return preds


# ===================================================================
# TestBfsOrder
# ===================================================================

class TestBfsOrder:
    def test_empty_graph(self):
        assert bfs_order([], 0) == []

    def test_single_node(self):
        assert bfs_order([[]], 1) == [0]

    def test_linear_chain(self):
        succs = _linear_chain(4)
        assert bfs_order(succs, 4) == [0, 1, 2, 3]

    def test_diamond(self):
        succs = _diamond()
        order = bfs_order(succs, 4)
        assert order[0] == 0
        assert order[-1] == 3
        assert set(order) == {0, 1, 2, 3}

    def test_unreachable_nodes(self):
        # 0 -> 1, node 2 is unreachable
        succs = [[1], [], []]
        order = bfs_order(succs, 3)
        assert order[:2] == [0, 1]
        assert 2 in order  # unreachable appended

    def test_all_nodes_visited(self):
        succs = _diamond()
        order = bfs_order(succs, 4)
        assert len(order) == 4


# ===================================================================
# TestBfsMaxDepth
# ===================================================================

class TestBfsMaxDepth:
    def test_empty_graph(self):
        assert bfs_max_depth([], 0) == 0

    def test_single_block(self):
        assert bfs_max_depth([[]], 1) == 0

    def test_linear_chain(self):
        succs = _linear_chain(5)
        assert bfs_max_depth(succs, 5) == 4

    def test_diamond(self):
        succs = _diamond()
        assert bfs_max_depth(succs, 4) == 2

    def test_wide_graph(self):
        # 0 -> 1, 0 -> 2, 0 -> 3 (all at depth 1)
        succs = [[1, 2, 3], [], [], []]
        assert bfs_max_depth(succs, 4) == 1


# ===================================================================
# TestCountBackEdges
# ===================================================================

class TestCountBackEdges:
    def test_empty_graph(self):
        assert count_back_edges([], 0) == 0

    def test_no_loops(self):
        succs = _linear_chain(3)
        assert count_back_edges(succs, 3) == 0

    def test_single_loop(self):
        # 0 -> 1 -> 2 -> 0 (one back edge: 2->0)
        succs = [[1], [2], [0]]
        assert count_back_edges(succs, 3) == 1

    def test_nested_loops(self):
        # 0 -> 1 -> 2 -> 1 (inner), 2 -> 3 -> 0 (outer)
        succs = [[1], [2], [1, 3], [0]]
        assert count_back_edges(succs, 4) == 2

    def test_self_loop(self):
        # 0 -> 0 (self-loop)
        succs = [[0]]
        assert count_back_edges(succs, 1) == 1

    def test_diamond_no_loops(self):
        succs = _diamond()
        assert count_back_edges(succs, 4) == 0

    def test_single_node_no_loop(self):
        succs = [[]]
        assert count_back_edges(succs, 1) == 0


# ===================================================================
# TestTopologyHash
# ===================================================================

class TestTopologyHash:
    def test_same_graph_same_hash(self):
        succs = _diamond()
        bfs = bfs_order(succs, 4)
        h1 = compute_topology_hash(succs, bfs, 4)
        h2 = compute_topology_hash(succs, bfs, 4)
        assert h1 == h2

    def test_different_graphs_different_hash(self):
        succs1 = _linear_chain(3)
        bfs1 = bfs_order(succs1, 3)
        h1 = compute_topology_hash(succs1, bfs1, 3)

        succs2 = _diamond()
        bfs2 = bfs_order(succs2, 4)
        h2 = compute_topology_hash(succs2, bfs2, 4)

        assert h1 != h2

    def test_returns_16_bytes(self):
        succs = _diamond()
        bfs = bfs_order(succs, 4)
        h = compute_topology_hash(succs, bfs, 4)
        assert isinstance(h, bytes)
        assert len(h) == 16

    def test_isomorphic_graphs_same_hash(self):
        # Graph A: 0->1, 0->2, 1->3, 2->3 (diamond with successors [1,2])
        succs_a = [[1, 2], [3], [3], []]
        # Graph B: same structure but successors listed as [2,1]
        # BFS from 0 will visit them in different order, but after remapping
        # the canonical form should be identical for isomorphic graphs
        succs_b = [[2, 1], [3], [3], []]

        bfs_a = bfs_order(succs_a, 4)
        bfs_b = bfs_order(succs_b, 4)

        h_a = compute_topology_hash(succs_a, bfs_a, 4)
        h_b = compute_topology_hash(succs_b, bfs_b, 4)
        assert h_a == h_b

    def test_empty_graph(self):
        h = compute_topology_hash([], [], 0)
        assert h == b'\x00' * 16

    def test_single_node(self):
        succs = [[]]
        bfs = bfs_order(succs, 1)
        h = compute_topology_hash(succs, bfs, 1)
        assert isinstance(h, bytes)
        assert len(h) == 16


# ===================================================================
# TestMdIndex
# ===================================================================

class TestMdIndex:
    def test_single_block_topdown(self):
        succs = [[]]
        preds = [[]]
        bfs = [0]
        result = compute_md_index_topdown(succs, preds, bfs)
        assert isinstance(result, int)
        assert result > 0

    def test_single_block_bottomup(self):
        succs = [[]]
        preds = [[]]
        result = compute_md_index_bottomup(succs, preds, 1)
        assert isinstance(result, int)
        assert result > 0

    def test_linear_chain_topdown_vs_bottomup(self):
        succs = _linear_chain(4)
        preds = _predecessors_from_successors(succs, 4)
        bfs = bfs_order(succs, 4)
        td = compute_md_index_topdown(succs, preds, bfs)
        bu = compute_md_index_bottomup(succs, preds, 4)
        # Top-down and bottom-up should be different for a linear chain
        # (entry has in_deg=0, exit has out_deg=0, so the sequences differ)
        assert td != bu

    def test_deterministic(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bfs = bfs_order(succs, 4)
        td1 = compute_md_index_topdown(succs, preds, bfs)
        td2 = compute_md_index_topdown(succs, preds, bfs)
        assert td1 == td2

    def test_different_graphs_different_index(self):
        succs1 = _linear_chain(3)
        preds1 = _predecessors_from_successors(succs1, 3)
        bfs1 = bfs_order(succs1, 3)
        td1 = compute_md_index_topdown(succs1, preds1, bfs1)

        succs2 = _diamond()
        preds2 = _predecessors_from_successors(succs2, 4)
        bfs2 = bfs_order(succs2, 4)
        td2 = compute_md_index_topdown(succs2, preds2, bfs2)

        assert td1 != td2

    def test_topdown_empty(self):
        assert compute_md_index_topdown([], [], []) == 0

    def test_bottomup_empty(self):
        assert compute_md_index_bottomup([], [], 0) == 0


# ===================================================================
# TestPrimeProduct
# ===================================================================

class TestPrimeProduct:
    def test_empty(self):
        assert compute_prime_product([]) == 0

    def test_known_sequence(self):
        # Use actual LLIL enum values from conftest_binja_stubs:
        # LLIL_NOP=0 -> prime 1, LLIL_LOAD=4 -> prime 5
        from redb.extractors.decompiler.bninja.analysis.cfg_features import LLIL_OP_PRIMES
        nop_val = 0   # LLIL_NOP
        load_val = 4  # LLIL_LOAD
        expected = LLIL_OP_PRIMES.get(nop_val, 1) * LLIL_OP_PRIMES.get(load_val, 1)
        result = compute_prime_product([nop_val, load_val])
        assert result == expected

    def test_order_independence(self):
        # LLIL_LOAD=4, LLIL_STORE=5, LLIL_ADD=13
        ops_a = [4, 5, 13]
        ops_b = [13, 4, 5]
        assert compute_prime_product(ops_a) == compute_prime_product(ops_b)

    def test_unknown_ops_map_to_1(self):
        # Unknown ops get prime 1, so they don't change the product
        result_known = compute_prime_product([4])  # LLIL_LOAD -> 5
        result_with_unknown = compute_prime_product([4, 9999])  # LOAD * unknown(1)
        assert result_known == result_with_unknown

    def test_mod_2_64(self):
        # Product should be mod 2^64
        result = compute_prime_product([4] * 1000)  # LLIL_LOAD
        assert 0 <= result < 2**64

    def test_single_op(self):
        # LLIL_STORE=5 -> prime 7
        assert compute_prime_product([5]) == 7


# ===================================================================
# TestBuildBlockFeatures
# ===================================================================

class TestBuildBlockFeatures:
    def test_empty_llil(self):
        succs = [[1], []]
        features = build_block_features([[], []], succs, 2)
        assert len(features) == 2
        # All zeros except successor_count
        assert features[0] == [0, 0, 0, 0, 0, 0, 0, 1]  # 1 successor
        assert features[1] == [0, 0, 0, 0, 0, 0, 0, 0]  # 0 successors

    def test_correct_categorization(self):
        # Set up categories for testing
        import redb.extractors.decompiler.bninja.analysis.cfg_features as cf
        old_cats = cf.LLIL_OP_CATEGORIES.copy()
        cf.LLIL_OP_CATEGORIES.update({
            100: CAT_ARITHMETIC,
            101: CAT_ARITHMETIC,
            200: CAT_LOGIC,
            300: CAT_CALL,
            400: CAT_MEMORY,
        })
        try:
            block_ops = [[100, 101, 200, 300, 400]]
            succs = [[]]
            features = build_block_features(block_ops, succs, 1)
            assert features[0][0] == 5   # instr_count
            assert features[0][1] == 2   # arithmetic
            assert features[0][2] == 1   # logic
            assert features[0][4] == 1   # call
            assert features[0][6] == 1   # memory
        finally:
            cf.LLIL_OP_CATEGORIES.clear()
            cf.LLIL_OP_CATEGORIES.update(old_cats)

    def test_cap_at_65535(self):
        # More than 65535 ops in one block
        huge_ops = [0] * 70000  # NOP x 70000
        succs = [[]]
        features = build_block_features([huge_ops], succs, 1)
        assert features[0][0] == 65535  # capped

    def test_missing_block_ops(self):
        # block_llil_ops shorter than n
        succs = [[1], [2], []]
        features = build_block_features([[1, 2]], succs, 3)
        assert len(features) == 3
        # Block 1 and 2 get empty ops since block_llil_ops only has 1 entry
        assert features[1] == [0, 0, 0, 0, 0, 0, 0, 1]
        assert features[2] == [0, 0, 0, 0, 0, 0, 0, 0]


# ===================================================================
# TestCfgFeatureTlsh
# ===================================================================

class TestCfgFeatureTlsh:
    def test_too_few_blocks_returns_none(self):
        # 5 blocks = 5 * 9 bytes = 45 < 50
        bb_features = [[10, 1, 0, 2, 0, 1, 1, 2]] * 5
        bfs = list(range(5))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert result is None

    def test_uniform_data_returns_none(self):
        # 7 identical blocks — TLSH returns TNULL for low-entropy input
        bb_features = [[10, 1, 0, 2, 0, 1, 1, 2]] * 7
        bfs = list(range(7))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert result is None

    def test_varied_data_returns_string(self):
        # 20 blocks with varied features — enough entropy for TLSH
        bb_features = [
            [i * 7 + 3, (i * 13) % 50, (i * 17) % 30, (i * 23) % 40,
             (i * 11) % 20, (i * 7) % 25, (i * 19) % 35, (i * 3) % 10]
            for i in range(20)
        ]
        bfs = list(range(20))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert isinstance(result, str)
        assert len(result) > 0
        assert result.startswith("T1")


# ===================================================================
# TestWlMinhash
# ===================================================================

class TestWlMinhash:
    def test_empty_function(self):
        result = compute_wl_minhash([], [], [], 0)
        assert result == [255] * NUM_WL_MINHASH_PERMS

    def test_returns_128_elements(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bb_feats = [[5, 1, 0, 2, 0, 1, 1, 2]] * 4
        result = compute_wl_minhash(succs, preds, bb_feats, 4)
        assert len(result) == 128

    def test_all_uint8(self):
        succs = _linear_chain(3)
        preds = _predecessors_from_successors(succs, 3)
        bb_feats = [[3, 1, 0, 1, 0, 0, 1, 1]] * 3
        result = compute_wl_minhash(succs, preds, bb_feats, 3)
        assert all(0 <= v <= 255 for v in result)

    def test_identical_graphs_same_signature(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bb_feats = [[5, 1, 0, 2, 0, 1, 1, 2]] * 4
        sig1 = compute_wl_minhash(succs, preds, bb_feats, 4)
        sig2 = compute_wl_minhash(succs, preds, bb_feats, 4)
        assert sig1 == sig2

    def test_different_graphs_different_signatures(self):
        # Graph 1: linear chain
        succs1 = _linear_chain(4)
        preds1 = _predecessors_from_successors(succs1, 4)
        bb_feats1 = [[5, 1, 0, 2, 0, 1, 1, i] for i in range(4)]
        sig1 = compute_wl_minhash(succs1, preds1, bb_feats1, 4)

        # Graph 2: diamond
        succs2 = _diamond()
        preds2 = _predecessors_from_successors(succs2, 4)
        bb_feats2 = [[10, 3, 2, 1, 0, 0, 0, i] for i in range(4)]
        sig2 = compute_wl_minhash(succs2, preds2, bb_feats2, 4)

        assert sig1 != sig2

    def test_single_node(self):
        succs = [[]]
        preds = [[]]
        bb_feats = [[1, 0, 0, 0, 0, 0, 0, 0]]
        result = compute_wl_minhash(succs, preds, bb_feats, 1)
        assert len(result) == 128


# ===================================================================
# TestPackAdjacency
# ===================================================================

class TestPackAdjacency:
    def test_empty(self):
        assert pack_adjacency([]) == []

    def test_single_edge(self):
        succs = [[1], []]
        edges = pack_adjacency(succs)
        assert len(edges) == 1
        assert edges[0] == (0 << 16) | 1

    def test_correct_packing(self):
        succs = _diamond()
        edges = pack_adjacency(succs)
        assert len(edges) == 4
        # 0->1, 0->2, 1->3, 2->3
        expected = {
            (0 << 16) | 1,
            (0 << 16) | 2,
            (1 << 16) | 3,
            (2 << 16) | 3,
        }
        assert set(edges) == expected

    def test_roundtrip(self):
        """Unpack edges and verify source/target pairs."""
        succs = [[1, 2], [3], [3], []]
        edges = pack_adjacency(succs)
        unpacked = [(e >> 16, e & 0xFFFF) for e in edges]
        expected = [(0, 1), (0, 2), (1, 3), (2, 3)]
        assert sorted(unpacked) == sorted(expected)

    def test_large_index_filtered(self):
        # Create a successor list where index >= 65536
        succs = [[] for _ in range(65537)]
        succs[0] = [65536]  # target is exactly 65536 — should be filtered
        edges = pack_adjacency(succs)
        assert len(edges) == 0

    def test_max_valid_index(self):
        # Index 65535 is the maximum valid
        succs = [[] for _ in range(65536)]
        succs[0] = [65535]
        edges = pack_adjacency(succs)
        assert len(edges) == 1
        assert edges[0] == (0 << 16) | 65535