72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""#1631 — a malformed LLM chunk (a stray non-dict entry in edges/nodes) must
|
|
not crash the merge/cache-write and discard all successful chunks.
|
|
|
|
`sem_result["edges"]` could contain a bare list where an edge object belongs
|
|
(a JSON array slipping past parse). Downstream code calls ``.get()`` per entry
|
|
(the AST+semantic merge at __main__.py and the semantic-cache writer both did),
|
|
raising ``AttributeError: 'list' object has no attribute 'get'`` and losing all
|
|
33 successful chunks. `_parse_llm_json` now sanitizes the fragment at the single
|
|
parse chokepoint so every consumer only ever sees lists of dicts.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from graphify.llm import _parse_llm_json, _sanitize_fragment
|
|
|
|
|
|
def test_sanitize_drops_non_dict_edge_entries():
|
|
frag = {
|
|
"nodes": [{"id": "a"}, ["not", "a", "dict"], "bare-string", {"id": "b"}],
|
|
"edges": [{"source": "a", "target": "b"}, ["stray", "list"], 42],
|
|
"hyperedges": [{"id": "h"}, None],
|
|
}
|
|
out = _sanitize_fragment(frag)
|
|
assert out["nodes"] == [{"id": "a"}, {"id": "b"}]
|
|
assert out["edges"] == [{"source": "a", "target": "b"}]
|
|
assert out["hyperedges"] == [{"id": "h"}]
|
|
|
|
|
|
def test_sanitize_coerces_non_list_values_to_empty():
|
|
frag = {"nodes": {"id": "oops"}, "edges": "nope", "hyperedges": None}
|
|
out = _sanitize_fragment(frag)
|
|
assert out["nodes"] == []
|
|
assert out["edges"] == []
|
|
# None is left as-is (absent key semantics) — the guard only fixes lists/values
|
|
assert out.get("hyperedges") is None
|
|
|
|
|
|
def test_parse_llm_json_sanitizes_stray_list_in_edges():
|
|
raw = json.dumps({
|
|
"nodes": [{"id": "a"}],
|
|
"edges": [{"source": "a", "target": "b"}, ["malformed"]],
|
|
"hyperedges": [],
|
|
})
|
|
parsed = _parse_llm_json(raw)
|
|
# Every entry that survives must be a dict so downstream .get() is safe.
|
|
for key in ("nodes", "edges", "hyperedges"):
|
|
assert all(isinstance(x, dict) for x in parsed.get(key, []))
|
|
assert parsed["edges"] == [{"source": "a", "target": "b"}]
|
|
|
|
|
|
def test_parse_llm_json_fenced_response_is_sanitized():
|
|
raw = (
|
|
"Here you go:\n\n```json\n"
|
|
+ json.dumps({"nodes": [["bad"], {"id": "ok"}], "edges": []})
|
|
+ "\n```\n"
|
|
)
|
|
parsed = _parse_llm_json(raw)
|
|
assert parsed["nodes"] == [{"id": "ok"}]
|
|
|
|
|
|
def test_merge_after_sanitize_does_not_raise_on_source_file_access():
|
|
# Mirrors the __main__.py comprehension that crashed: e.get("source_file", "").
|
|
parsed = _parse_llm_json(json.dumps({
|
|
"nodes": [{"id": "a", "source_file": "d.md"}],
|
|
"edges": [{"source": "a", "target": "b"}, ["oops"]],
|
|
}))
|
|
# This is exactly the pattern at __main__.py:4858-4860.
|
|
seen = {n.get("source_file", "") for n in parsed.get("nodes", [])}
|
|
seen |= {e.get("source_file", "") for e in parsed.get("edges", [])}
|
|
assert "d.md" in seen
|