Files
graphify/tests/test_semantic_id_remap_root.py
safishamsi cf4b4ef85a fix(build): don't crash when a node's source_file is the scan root (#1618)
A node whose source_file equals the absolute scan root (e.g. a project-level
semantic concept the LLM attributed to the whole repo) relativized to Path('.'),
and _semantic_id_remap fed that into _file_stem, whose path.with_suffix("")
raises `ValueError: '.' has an empty name`. The crash landed in final graph
assembly — AFTER all LLM extraction cost was spent — writing no graph.json at
all, and leaving `cluster-only` to then report "no graph found".

Two guards: _file_stem returns "" for a name-less path (protects every caller,
not just this one), and both _semantic_id_remap passes skip a root-equal
source_file explicitly (it has no per-file identity to remap — id left
untouched). Reported with a minimal LLM-free repro by @sub4biz.

Not a 0.9.5 regression: _semantic_id_remap/_file_stem are byte-identical to
0.9.4; the latent path was only hit when dedup produced a root-source_file node.
4 regression tests (dot-path stem, remap no-crash, build_from_json with a
root-level concept node, normal remap unaffected). Full suite 2849.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:32:24 +01:00

51 lines
2.0 KiB
Python

"""A node whose source_file equals the scan root must not crash build (#1618).
`_norm_source_file` relativizes an absolute source_file that equals the scan root
to `Path('.')`. `_semantic_id_remap` then fed that into `_file_stem`, whose
`path.with_suffix("")` raises `ValueError: '.' has an empty name` — crashing the
final graph assembly AFTER all LLM extraction cost was spent, writing no graph at
all. A project-level node (source_file == root) has no per-file identity to remap,
so its id is left untouched.
"""
from __future__ import annotations
from pathlib import Path
from graphify.build import _semantic_id_remap, build_from_json
from graphify.extractors.base import _file_stem
def test_file_stem_handles_dot_path():
assert _file_stem(Path(".")) == "" # no raise
assert _file_stem(Path("src/foo.py")) == "src/foo"
def test_semantic_id_remap_root_equal_source_file_no_crash():
root = "/some/project/root"
node = {"id": "some_concept", "source_file": root, "_origin": "semantic"}
remap = _semantic_id_remap([node], root) # must not raise
# a root-equal node has no file stem, so its id is left untouched (not remapped)
assert "some_concept" not in remap
def test_build_from_json_with_root_level_concept_node():
root = "/proj"
combined = {
"nodes": [
{"id": "proj_concept", "label": "Project", "file_type": "concept",
"source_file": root, "_origin": "semantic"},
{"id": "src_foo", "label": "foo", "file_type": "code",
"source_file": "src/foo.py", "_origin": "ast"},
],
"edges": [],
}
G = build_from_json(combined, root=root) # previously crashed here
assert G.number_of_nodes() == 2
def test_normal_semantic_remap_still_works():
# regression guard: a real per-file node still gets remap consideration (#1504)
remap = _semantic_id_remap(
[{"id": "foo", "source_file": "src/foo.py", "_origin": "semantic"}], "/proj")
assert isinstance(remap, dict)