fix(palace_graph): skip None metadata in build_graph

ChromaDB can return None for drawers without metadata (legacy data,
partial writes — same root cause as upstream #1020 / our PR #1094).
build_graph at line 95 called meta.get("room", "") unconditionally,
which AttributeErrors on None and takes out every consumer of
build_graph for the whole call path: graph_stats, find_tunnels,
traverse, and (most visibly) the daemon's /stats endpoint.

Caught 2026-04-25 by palace-daemon's verify-routes.sh smoke test
against the canonical 151K-drawer palace — /stats was 500-ing on a
single None drawer.

Adds `if meta is None: continue` guard. Closes the same gap upstream's
#999 None-metadata audit closed in searcher.py / mcp_server.py /
miner.status, just in a different file the audit didn't reach. The
graph-build is recoverable: skipping a single None drawer doesn't
distort the graph since build_graph already filters
`room and room != "general" and wing` — a missing-metadata drawer was
never going to participate anyway.

Test: TestBuildGraph::test_none_metadata_does_not_crash mixes a None
entry into a 3-drawer fixture and asserts the two real drawers are
processed normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jp
2026-04-25 11:06:25 -07:00
parent 0d9929c0dd
commit 5b07b869b0
2 changed files with 31 additions and 0 deletions
+10
View File
@@ -92,6 +92,16 @@ def build_graph(col=None, config=None):
while offset < total: while offset < total:
batch = col.get(limit=1000, offset=offset, include=["metadatas"]) batch = col.get(limit=1000, offset=offset, include=["metadatas"])
for meta in batch["metadatas"]: for meta in batch["metadatas"]:
# ChromaDB can return ``None`` for drawers without metadata
# (legacy data, partial writes — upstream #1020 territory).
# Skip these silently rather than crash the whole graph
# build — a single None drawer shouldn't take down /stats
# or any caller of build_graph for the entire palace. Caught
# 2026-04-25 by palace-daemon's verify-routes.sh smoke test
# against the canonical 151K palace. Closes the same gap as
# upstream #999 / fork PR #1094 in a different read path.
if meta is None:
continue
room = meta.get("room", "") room = meta.get("room", "")
wing = meta.get("wing", "") wing = meta.get("wing", "")
hall = meta.get("hall", "") hall = meta.get("hall", "")
+21
View File
@@ -54,6 +54,27 @@ class TestBuildGraph:
assert nodes == {} assert nodes == {}
assert edges == [] assert edges == []
def test_none_metadata_does_not_crash(self):
"""ChromaDB can return None for drawers without metadata (legacy
data, partial writes — upstream #1020 territory). build_graph
must skip None entries silently rather than crash the whole
graph build with AttributeError. Caught 2026-04-25 by
palace-daemon's verify-routes.sh smoke test against the
canonical 151K palace; /stats was 500-ing on a single None
drawer and taking out every consumer of build_graph for the
whole call path."""
col = _make_fake_collection(
[
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-01"},
None, # legacy / partial-write drawer with no metadata
{"room": "auth", "wing": "wing_code", "hall": "security", "date": "2026-01-02"},
]
)
nodes, edges = build_graph(col=col)
# The two real drawers were processed; the None one was skipped.
assert "auth" in nodes
assert nodes["auth"]["count"] == 2
def test_single_wing_no_edges(self): def test_single_wing_no_edges(self):
col = _make_fake_collection( col = _make_fake_collection(
[ [