mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
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)
129 lines
4.9 KiB
Python
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
|