Ctrl K
ComunidadGuías…

tests/test_explain_same_file_tie.py

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

Archivo original ↗Preguntar a KiBot
"""`explain` / get_node refuse a name several nodes of ONE file carry, as `path` does.

The cross-file case was already refused; within one file, `_find_node`'s winning
tier was split by graph iteration order and one node presented as the answer.
"""
from __future__ import annotations

import json

import networkx as nx
import pytest

import graphify.__main__ as mainmod
from graphify.cli import EXIT_AMBIGUOUS
from graphify.serve import _resolve_single_node


_NODES = [
    ("src_x", "x.ts", "src/x.ts", "L1"),
    ("src_x_run", "run()", "src/x.ts", "L3"),
    ("src_x_alphaclient", "AlphaClient", "src/x.ts", "L5"),
    ("src_x_alphaclient_nonce", ".nonce()", "src/x.ts", "L6"),
    ("src_x_betaclient", "BetaClient", "src/x.ts", "L9"),
    ("src_x_betaclient_nonce", ".nonce()", "src/x.ts", "L10"),
    # Sourceless stubs: two files each import the same external interface.
    ("tests_a_php_entitymanagerinterface", "EntityManagerInterface", "", ""),
    ("tests_b_php_entitymanagerinterface", "EntityManagerInterface", "", ""),
]
_LINKS = [
    ("src_x", "src_x_run", "contains"),
    ("src_x_run", "src_x_alphaclient", "calls"),
    ("src_x_run", "src_x_betaclient", "calls"),
    ("src_x_alphaclient", "src_x_alphaclient_nonce", "method"),
    ("src_x_betaclient", "src_x_betaclient_nonce", "method"),
]


def _graph_data(reverse: bool = False) -> dict:
    nodes = [
        {"id": i, "label": lbl, "source_file": sf, "source_location": loc}
        for i, lbl, sf, loc in _NODES
    ]
    return {
        "directed": False, "multigraph": False, "graph": {},
        "nodes": list(reversed(nodes)) if reverse else nodes,
        "links": [
            {"source": a, "target": b, "relation": rel, "confidence": "EXTRACTED"}
            for a, b, rel in _LINKS
        ],
    }


def _write(tmp_path, reverse: bool = False):
    p = tmp_path / ("graph_rev.json" if reverse else "graph.json")
    p.write_text(json.dumps(_graph_data(reverse)))
    return p


def _explain(monkeypatch, capsys, graph_path, label):
    monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
    monkeypatch.setattr(mainmod.sys, "argv",
        ["graphify", "explain", label, "--graph", str(graph_path)])
    try:
        mainmod.main()
    except SystemExit as exc:
        return capsys.readouterr().out, exc.code
    return capsys.readouterr().out, None


@pytest.mark.parametrize("label", [".nonce()", "src/x.ts::.nonce()"])
def test_explain_refuses_same_file_twins(monkeypatch, tmp_path, capsys, label):
    out, code = _explain(monkeypatch, capsys, _write(tmp_path), label)
    assert f"Ambiguous: '{label}' matches 2 nodes in src/x.ts." in out
    assert "src_x_alphaclient_nonce" in out
    assert "src_x_betaclient_nonce" in out
    assert "Node: .nonce()" not in out
    assert code == EXIT_AMBIGUOUS


def test_explain_refuses_sourceless_stubs(monkeypatch, tmp_path, capsys):
    out, code = _explain(monkeypatch, capsys, _write(tmp_path), "EntityManagerInterface")
    assert out.startswith("Ambiguous: 'EntityManagerInterface' matches 2 nodes")
    assert "tests_a_php_entitymanagerinterface" in out
    assert "tests_b_php_entitymanagerinterface" in out
    assert "Node: EntityManagerInterface" not in out
    assert code == EXIT_AMBIGUOUS


def test_explain_refusal_does_not_depend_on_node_order(monkeypatch, tmp_path, capsys):
    forward, _ = _explain(monkeypatch, capsys, _write(tmp_path), ".nonce()")
    reverse, _ = _explain(monkeypatch, capsys, _write(tmp_path, reverse=True), ".nonce()")
    ids = lambda out: sorted(l.strip() for l in out.splitlines() if l.strip().startswith("id:"))
    assert ids(forward) == ids(reverse) == [
        "id: src_x_alphaclient_nonce", "id: src_x_betaclient_nonce",
    ]


def test_explain_node_id_still_names_one_twin(monkeypatch, tmp_path, capsys):
    out, code = _explain(monkeypatch, capsys, _write(tmp_path), "src_x_betaclient_nonce")
    assert "ID:        src_x_betaclient_nonce" in out
    assert "Ambiguous" not in out
    assert code is None


def test_explain_file_path_still_resolves_to_the_file_node(monkeypatch, tmp_path, capsys):
    """A file path matches the file node and its members: precedence, not a tie."""
    out, code = _explain(monkeypatch, capsys, _write(tmp_path), "src/x.ts")
    assert "ID:        src_x\n" in out
    assert "Ambiguous" not in out
    assert code is None


def test_get_node_resolver_refuses_same_file_twins():
    """get_node / get_neighbors share `_resolve_single_node` with the same refusal."""
    G = nx.node_link_graph({**_graph_data(), "directed": True}, edges="links")
    nid, err = _resolve_single_node(G, ".nonce()")
    assert nid is None
    assert err is not None and err.startswith("Ambiguous: '.nonce()' matches 2 nodes in src/x.ts.")
    nid, err = _resolve_single_node(G, "src_x_alphaclient_nonce")
    assert (nid, err) == ("src_x_alphaclient_nonce", None)