Ctrl K
ComunidadGuías…

tests/test_affected_seed_reference_forms.py

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

Archivo original ↗Preguntar a KiBot
"""#4792 — `affected` and `query --seed` rejected the reference forms `explain`
already resolved, and `affected` exited 0 on an ambiguous or unmatched seed.

`explain` resolves a `path::Symbol` reference (#3485) and a dotted Python name
(`pkg.mod.func`, #4366) to a single node. `affected` and `query --seed` answered
"No unique node match" for the very same string, so following the README's own
advice ("use the precise ID returned by the graph") forced a caller through
`explain` first merely to obtain an id. `affected` also exited 0 when the query
was ambiguous or matched nothing, so a script could not branch on the outcome
the README documents (3 = ambiguous, 4 = no match).

Regression shape: every assertion below fails on v8, where `resolve_seed`
returns None for the qualified forms and the CLI always exits 0.
"""
from __future__ import annotations

import json

import networkx as nx
import pytest
from networkx.readwrite import json_graph

import graphify.__main__ as mainmod
from graphify.affected import resolve_seed, resolve_seed_outcome


def _two_same_named_functions() -> nx.DiGraph:
    """Two `state` functions in different modules, plus one caller.

    The layout from the issue: a path-qualified seed must single one out, while
    the bare name is genuinely ambiguous and must stay unresolved.
    """
    g = nx.DiGraph()
    g.add_node("pkg", label="pkg", source_file="pkg/__init__.py", source_location="L1")
    g.add_node(
        "pkg_auth_mfa_state",
        label="state()",
        norm_label="state()",
        source_file="pkg/auth/mfa.py",
        source_location="L1",
    )
    g.add_node(
        "pkg_other_flow_state",
        label="state()",
        norm_label="state()",
        source_file="pkg/other/flow.py",
        source_location="L1",
    )
    g.add_node(
        "pkg_user_use",
        label="use()",
        norm_label="use()",
        source_file="pkg/user.py",
        source_location="L3",
    )
    g.add_edge("pkg_user_use", "pkg_auth_mfa_state", relation="calls",
               source_file="pkg/user.py", source_location="L4")
    return g


def test_resolve_seed_accepts_path_scoped_symbol():
    """`path::Symbol` resolves as it does in `explain` (#3485)."""
    g = _two_same_named_functions()
    assert resolve_seed(g, "pkg/auth/mfa.py::state") == "pkg_auth_mfa_state"


def test_resolve_seed_accepts_dotted_python_name():
    """`pkg.mod.func` resolves as it does in `explain` (#4366)."""
    g = _two_same_named_functions()
    assert resolve_seed(g, "pkg.auth.mfa.state") == "pkg_auth_mfa_state"


def test_resolve_seed_bare_ambiguous_name_still_returns_none():
    """A bare name shared by two modules stays unresolved — it must not guess."""
    g = _two_same_named_functions()
    assert resolve_seed(g, "state") is None


def test_resolve_seed_outcome_reports_rivals_for_ambiguous_name():
    """The ambiguity is reported so the CLI can exit 3 rather than 0."""
    g = _two_same_named_functions()
    seed, rivals = resolve_seed_outcome(g, "state")
    assert seed is None
    assert set(rivals) == {"pkg_auth_mfa_state", "pkg_other_flow_state"}


def test_resolve_seed_outcome_reports_no_rivals_for_a_miss():
    """A query that matches nothing is a miss (exit 4), not an ambiguity (exit 3)."""
    g = _two_same_named_functions()
    seed, rivals = resolve_seed_outcome(g, "nothing_like_this")
    assert seed is None
    assert rivals == []


def _write_graph(tmp_path, graph: nx.Graph) -> str:
    from graphify.paths import GRAPHIFY_OUT_NAME

    out_dir = tmp_path / "repo" / GRAPHIFY_OUT_NAME
    out_dir.mkdir(parents=True)
    gp = out_dir / "graph.json"
    gp.write_text(json.dumps(json_graph.node_link_data(graph, edges="links")), encoding="utf-8")
    return str(gp)


def _run_affected(monkeypatch, graph_path: str, query: str) -> int:
    """Run `graphify affected <query>` and return its exit code (0 when it returns)."""
    monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
    monkeypatch.setattr(
        mainmod.sys, "argv", ["graphify", "affected", query, "--graph", graph_path]
    )
    try:
        mainmod.main()
    except SystemExit as exc:  # only the failure paths exit explicitly
        return int(exc.code or 0)
    return 0


def test_affected_cli_resolves_path_scoped_symbol(tmp_path, monkeypatch, capsys):
    """The exact repro from the issue: affected now resolves what explain resolves."""
    graph_path = _write_graph(tmp_path, _two_same_named_functions())

    code = _run_affected(monkeypatch, graph_path, "pkg/auth/mfa.py::state")

    out = capsys.readouterr().out
    assert code == 0
    assert "Affected nodes for state()" in out
    assert "use()" in out


def test_affected_cli_exits_3_on_ambiguous_seed(tmp_path, monkeypatch, capsys):
    """README documents exit 3 for an ambiguous name; affected used to exit 0."""
    from graphify.cli import EXIT_AMBIGUOUS

    graph_path = _write_graph(tmp_path, _two_same_named_functions())

    code = _run_affected(monkeypatch, graph_path, "state")

    err = capsys.readouterr().err
    assert code == EXIT_AMBIGUOUS
    assert err.startswith("Ambiguous:")
    # Both candidates must be named so the caller can pick one.
    assert "pkg/auth/mfa.py" in err
    assert "pkg/other/flow.py" in err


def test_affected_cli_exits_4_on_unmatched_seed(tmp_path, monkeypatch, capsys):
    """README documents exit 4 for no matching node; affected used to exit 0."""
    from graphify.cli import EXIT_NOT_FOUND

    graph_path = _write_graph(tmp_path, _two_same_named_functions())

    code = _run_affected(monkeypatch, graph_path, "nothing_like_this")

    err = capsys.readouterr().err
    assert code == EXIT_NOT_FOUND
    assert "No unique node match" in err