- export.py: guard to_obsidian/to_canvas against dangling community member IDs (KeyError crash when a node in communities dict is absent from graph, #1236) - detect.py: NFC-normalize path before hashing Office sidecar filename to fix macOS NFC/NFD mismatch causing --update to re-extract all Office files (#1226) - extract.py: add _is_config_json() to skip data JSON files (only extract package.json, tsconfig.json, eslint, deno, JSON Schema etc.) eliminating 561 orphan key-nodes on large repos (#1224) - llm.py: add GRAPHIFY_LLM_TEMPERATURE env var + _resolve_temperature() helper; auto-omit temperature for o1/o3/o4/gpt-5 reasoning models that reject temp=0; mirrors GRAPHIFY_MAX_OUTPUT_TOKENS precedence pattern (#1191) - tests: 20 new regression tests across obsidian, detect, extract, llm_backends Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
51 lines
2.1 KiB
Python
51 lines
2.1 KiB
Python
"""Regression test for issue #1236: to_obsidian must not crash with KeyError
|
|
when a community's member list contains an id that has no backing node in G
|
|
(e.g. pruned nodes, stale community assignments, or synthesized/merge-artifact
|
|
ids). Such dangling members must be skipped, not abort the whole vault export."""
|
|
import networkx as nx
|
|
|
|
from graphify.export import to_obsidian
|
|
|
|
|
|
def _graph_with_dangling_member():
|
|
"""Two real nodes plus a community that references a third, non-existent id."""
|
|
G = nx.Graph()
|
|
G.add_node("n0", label="Alpha", file_type="code", source_file="a.py")
|
|
G.add_node("n1", label="Beta", file_type="code", source_file="b.py")
|
|
G.add_edge("n0", "n1", relation="calls", confidence="EXTRACTED")
|
|
# 'agents_doc' is a synthesized member id with no backing node in G.
|
|
communities = {0: ["n0", "n1", "agents_doc"]}
|
|
return G, communities
|
|
|
|
|
|
def test_obsidian_dangling_community_member_does_not_crash(tmp_path):
|
|
G, comms = _graph_with_dangling_member()
|
|
# Before the fix this raised KeyError: 'agents_doc'.
|
|
n = to_obsidian(G, comms, str(tmp_path))
|
|
assert n > 0
|
|
|
|
# The community note is still written for the surviving members.
|
|
comm_notes = list(tmp_path.glob("_COMMUNITY_*.md"))
|
|
assert len(comm_notes) == 1
|
|
body = comm_notes[0].read_text(encoding="utf-8")
|
|
|
|
# Real members appear in the Members section; the dangling id does not.
|
|
assert "[[Alpha]]" in body
|
|
assert "[[Beta]]" in body
|
|
assert "agents_doc" not in body
|
|
|
|
# Member count reflects only the real (resolvable) members.
|
|
assert "**Members:** 2 nodes" in body
|
|
|
|
|
|
def test_obsidian_community_of_only_dangling_members(tmp_path):
|
|
"""A community whose members are all dangling should still not crash."""
|
|
G = nx.Graph()
|
|
G.add_node("n0", label="Alpha", file_type="code", source_file="a.py")
|
|
comms = {0: ["n0"], 1: ["ghost_a", "ghost_b"]}
|
|
n = to_obsidian(G, comms, str(tmp_path))
|
|
assert n > 0
|
|
ghost_note = tmp_path / "_COMMUNITY_Community 1.md"
|
|
assert ghost_note.exists()
|
|
assert "**Members:** 0 nodes" in ghost_note.read_text(encoding="utf-8")
|