Ctrl K
ComunidadGuías…

tests/test_alias_folding.py

Rama v8 · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""LLM field aliases: node file/path/filepath -> source_file, edge relationship -> relation."""
from __future__ import annotations

import io
from contextlib import redirect_stderr

from graphify.build import (
    _fold_confidence,
    _fold_edge_aliases,
    _fold_node_aliases,
    build_from_json,
)


def test_edge_relationship_folds_to_relation():
    edge = {"source": "a", "target": "b", "relationship": "calls"}
    _fold_edge_aliases(edge)
    assert edge["relation"] == "calls"
    assert "relationship" not in edge


def test_edge_relation_wins_over_relationship():
    edge = {"source": "a", "target": "b", "relation": "imports", "relationship": "calls"}
    _fold_edge_aliases(edge)
    assert edge["relation"] == "imports"


def test_node_file_folds_to_source_file():
    node = {"id": "a", "label": "A", "file": "foo.ts"}
    _fold_node_aliases(node)
    assert node["source_file"] == "foo.ts"
    assert "file" not in node


def test_node_path_folds_to_source_file():
    node = {"id": "a", "label": "A", "path": "bar.py"}
    _fold_node_aliases(node)
    assert node["source_file"] == "bar.py"
    assert "path" not in node


def test_node_filepath_folds_to_source_file():
    node = {"id": "a", "label": "A", "filepath": "baz.go"}
    _fold_node_aliases(node)
    assert node["source_file"] == "baz.go"


def test_node_src_folds_to_source_file():
    node = {"id": "a", "label": "A", "src": "src/engine.py"}
    _fold_node_aliases(node)
    assert node["source_file"] == "src/engine.py"
    assert "src" not in node


def test_node_source_file_wins_over_alias():
    node = {"id": "a", "label": "A", "source_file": "real.py", "file": "other.py"}
    _fold_node_aliases(node)
    assert node["source_file"] == "real.py"


def test_numeric_confidence_folds_to_inferred_and_backfills_score():
    edge = {"source": "a", "target": "b", "confidence": 0.85}
    _fold_edge_aliases(edge)
    assert edge["confidence"] == "INFERRED"
    assert edge["confidence_score"] == 0.85


def test_numeric_confidence_does_not_override_an_existing_score():
    edge = {"source": "a", "target": "b", "confidence": 0.85, "confidence_score": 0.5}
    _fold_edge_aliases(edge)
    assert edge["confidence"] == "INFERRED"
    assert edge["confidence_score"] == 0.5


def test_model_drifted_string_confidence_folds_to_inferred():
    # "high"/"confirmed"/"medium-high"/"medium-low" are model drift from the
    # LLM semantic pass, not the documented EXTRACTED/INFERRED/AMBIGUOUS enum
    # (#4495): none of them parse as a float, so confidence_score is left
    # alone and only the enum field itself is repaired.
    for drifted in ("high", "confirmed", "medium-high", "medium-low"):
        edge = {"source": "a", "target": "b", "confidence": drifted}
        _fold_edge_aliases(edge)
        assert edge["confidence"] == "INFERRED", drifted
        assert "confidence_score" not in edge, drifted


def test_numeric_looking_string_confidence_backfills_score():
    """A string that DOES parse as a float still backfills confidence_score,
    the same as a genuinely numeric confidence (#4495)."""
    edge = {"source": "a", "target": "b", "confidence": "0.85"}
    _fold_edge_aliases(edge)
    assert edge["confidence"] == "INFERRED"
    assert edge["confidence_score"] == 0.85


def test_lowercase_confidence_is_case_corrected_not_treated_as_drift():
    """A case variant of a real enum value (#4495) IS the enum value, just
    mis-cased -- it must be corrected to its exact spelling, not treated as
    an unknown word that backfills confidence_score from a failed float
    parse of a string that was never meant to be a number."""
    edge = {"source": "a", "target": "b", "confidence": "inferred"}
    _fold_edge_aliases(edge)
    assert edge["confidence"] == "INFERRED"
    assert "confidence_score" not in edge


def test_valid_enum_confidences_are_left_untouched():
    for valid in ("EXTRACTED", "INFERRED", "AMBIGUOUS"):
        edge = {"source": "a", "target": "b", "confidence": valid}
        _fold_edge_aliases(edge)
        assert edge["confidence"] == valid
        assert "confidence_score" not in edge


def test_hyperedge_confidence_folds_the_same_way():
    """#4495: a hyperedge carries the same confidence/confidence_score pair
    an edge does, and had no fold of its own."""
    he = {"id": "h1", "nodes": ["a", "b"], "confidence": "high"}
    _fold_confidence(he)
    assert he["confidence"] == "INFERRED"

    he_numeric = {"id": "h2", "nodes": ["a", "b"], "confidence": 0.75}
    _fold_confidence(he_numeric)
    assert he_numeric["confidence"] == "INFERRED"
    assert he_numeric["confidence_score"] == 0.75


def test_build_from_json_normalizes_drifted_confidence_without_warning():
    """#4495 end to end: a graph carrying the exact warning text from the
    issue must build silently, with the edge and hyperedge confidence both
    repaired to the enum."""
    extraction = {
        "nodes": [
            {"id": "a", "label": "A", "file_type": "document", "source_file": "a.md"},
            {"id": "b", "label": "B", "file_type": "document", "source_file": "b.md"},
        ],
        "edges": [
            {
                "source": "a", "target": "b", "relation": "references",
                "confidence": "confirmed", "source_file": "a.md",
            }
        ],
        "hyperedges": [
            {"id": "h1", "nodes": ["a", "b"], "relation": "form", "confidence": "high"}
        ],
    }
    err = io.StringIO()
    with redirect_stderr(err):
        G = build_from_json(extraction)
    assert "invalid confidence" not in err.getvalue()
    _, _, data = next(iter(G.edges(data=True)))
    assert data["confidence"] == "INFERRED"
    assert G.graph["hyperedges"][0]["confidence"] == "INFERRED"


def test_build_from_json_normalizes_without_validation_warnings():
    extraction = {
        "nodes": [
            {"id": "a", "label": "A", "file_type": "code", "file": "foo.ts"},
            {"id": "b", "label": "B", "file_type": "code", "path": "bar.py"},
        ],
        "edges": [
            {
                "source": "a",
                "target": "b",
                "relationship": "calls",
                "confidence": "EXTRACTED",
                "source_file": "foo.ts",
            }
        ],
    }
    err = io.StringIO()
    with redirect_stderr(err):
        G = build_from_json(extraction)
    out = err.getvalue()
    assert "missing required field" not in out
    assert G.nodes["a"]["source_file"] == "foo.ts"
    assert G.nodes["b"]["source_file"] == "bar.py"
    assert G.number_of_edges() == 1
    _, _, data = next(iter(G.edges(data=True)))
    assert data["relation"] == "calls"


def test_node_confidence_folds_the_same_way():
    """#4529: a node carrying a confidence/confidence_score pair gets the same
    fold edges and hyperedges already have, instead of passing through the
    build verbatim."""
    drifted = {"id": "a", "label": "A", "confidence": "high"}
    _fold_node_aliases(drifted)
    assert drifted["confidence"] == "INFERRED"
    assert "confidence_score" not in drifted

    mis_cased = {"id": "b", "label": "B", "confidence": "extracted"}
    _fold_node_aliases(mis_cased)
    assert mis_cased["confidence"] == "EXTRACTED"
    assert "confidence_score" not in mis_cased

    numeric = {"id": "c", "label": "C", "confidence": 0.9}
    _fold_node_aliases(numeric)
    assert numeric["confidence"] == "INFERRED"
    assert numeric["confidence_score"] == 0.9


def test_node_without_confidence_is_left_untouched():
    """No behavior change for graphs whose nodes carry no confidence (#4529)."""
    node = {"id": "a", "label": "A", "source_file": "a.py"}
    _fold_node_aliases(node)
    assert "confidence" not in node
    assert "confidence_score" not in node


def test_build_from_json_normalizes_node_confidence():
    """#4529 end to end: a drifted node confidence reaches the built graph
    folded onto the enum, the same as an edge's."""
    extraction = {
        "nodes": [
            {
                "id": "a", "label": "A", "file_type": "document",
                "source_file": "a.md", "confidence": "extracted",
            },
            {
                "id": "b", "label": "B", "file_type": "document",
                "source_file": "b.md", "confidence": 0.7,
            },
        ],
        "edges": [],
    }
    G = build_from_json(extraction)
    assert G.nodes["a"]["confidence"] == "EXTRACTED"
    assert G.nodes["b"]["confidence"] == "INFERRED"
    assert G.nodes["b"]["confidence_score"] == 0.7