Files
graphify/tests/test_lookup_exit_codes.py
Allionix ddc24c76d8 fix(cli): give lookups distinct exit codes for ambiguous and missing names
explain and query exited 0 when no node matched, the same status as an
answer, and path exited 1 for an ambiguous endpoint, a missing one and a
missing graph file alike. A script or an agent could not tell a refusal
from an answer without parsing the text.

explain, path and query now exit 3 when a name is ambiguous (the
candidates are listed) and 4 when no node matches. Messages and output
streams are unchanged; "no path found" between two existing nodes is
still an answer. The codes are documented in --help and the README.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 13dc2400e1)
2026-10-11 20:49:42 +01:00

129 lines
4.9 KiB
Python

"""`explain`, `path` and `query` tell a script apart an answer, an ambiguous name
and a name that matches nothing, through the exit code alone.
`explain` used to exit 0 on a miss, the same as a successful answer, and the
ambiguity and miss refusals of `path` shared exit 1 with every other error.
"""
from __future__ import annotations
import json
import pytest
import graphify.__main__ as mainmod
from graphify.cli import EXIT_AMBIGUOUS, EXIT_NOT_FOUND
def _write_graph(tmp_path):
nodes = [
("src_a_alphaclient", "AlphaClient", "src/a.ts", "L1"),
("src_a_alphaclient_nonce", ".nonce()", "src/a.ts", "L2"),
("src_b_betaclient", "BetaClient", "src/b.ts", "L1"),
("src_b_betaclient_nonce", ".nonce()", "src/b.ts", "L2"),
("src_c_run", "run()", "src/c.ts", "L1"),
("src_d_island", "island()", "src/d.ts", "L1"),
]
links = [
("src_c_run", "src_b_betaclient", "calls"),
("src_b_betaclient", "src_b_betaclient_nonce", "method"),
("src_a_alphaclient", "src_a_alphaclient_nonce", "method"),
]
p = tmp_path / "graph.json"
p.write_text(json.dumps({
"directed": False, "multigraph": False, "graph": {},
"nodes": [
{"id": i, "label": lbl, "source_file": sf, "source_location": loc}
for i, lbl, sf, loc in nodes
],
"links": [
{"source": a, "target": b, "relation": rel, "confidence": "EXTRACTED"}
for a, b, rel in links
],
}))
return p
def _run(monkeypatch, capsys, *argv):
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(mainmod.sys, "argv", ["graphify", *argv])
try:
mainmod.main()
except SystemExit as exc:
captured = capsys.readouterr()
return exc.code, captured.out + captured.err
captured = capsys.readouterr()
return None, captured.out + captured.err
def test_exit_codes_are_distinct_from_success_and_from_each_other():
assert len({0, 1, EXIT_AMBIGUOUS, EXIT_NOT_FOUND}) == 4
def test_explain_answer_exits_zero(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys, "explain", "run()", "--graph", str(_write_graph(tmp_path)))
assert code is None
assert "ID: src_c_run" in out
def test_explain_ambiguous_exits_ambiguous(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys, "explain", ".nonce()", "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_AMBIGUOUS
assert "Ambiguous: '.nonce()' matches 2 nodes in different files." in out
@pytest.mark.parametrize("label", ["NoSuchSymbolXyz", "src/a.ts::NotThere"])
def test_explain_miss_exits_not_found(monkeypatch, tmp_path, capsys, label):
code, out = _run(monkeypatch, capsys, "explain", label, "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_NOT_FOUND
assert f"No node matching '{label}' found." in out
def test_path_answer_exits_zero(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys,
"path", "run()", "src/b.ts::.nonce()", "--graph", str(_write_graph(tmp_path)))
assert code is None
assert "Shortest path (2 hops):" in out
def test_path_without_route_is_an_answer(monkeypatch, tmp_path, capsys):
"""Both endpoints exist and no route joins them: that is the answer, not a miss."""
code, out = _run(monkeypatch, capsys,
"path", "run()", "island()", "--graph", str(_write_graph(tmp_path)))
assert code == 0
assert "No directed path found" in out
def test_path_ambiguous_endpoint_exits_ambiguous(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys,
"path", "run()", ".nonce()", "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_AMBIGUOUS
assert "Ambiguous: '.nonce()'" in out
def test_path_same_node_both_ends_exits_ambiguous(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys,
"path", "run()", "src_c_run", "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_AMBIGUOUS
assert "both resolved to the same node" in out
@pytest.mark.parametrize("endpoint", ["NoSuchSymbolXyz", "src/a.ts::NotThere"])
def test_path_missing_endpoint_exits_not_found(monkeypatch, tmp_path, capsys, endpoint):
code, out = _run(monkeypatch, capsys,
"path", "run()", endpoint, "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_NOT_FOUND
assert "Shortest path" not in out
def test_query_answer_exits_zero(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys, "query", "run", "--graph", str(_write_graph(tmp_path)))
assert code is None
assert "NODE run()" in out
def test_query_without_any_match_exits_not_found(monkeypatch, tmp_path, capsys):
code, out = _run(monkeypatch, capsys,
"query", "zzqqxx", "--graph", str(_write_graph(tmp_path)))
assert code == EXIT_NOT_FOUND
assert "No matching nodes found." in out