Skip to content

Commit 136bf0a

Browse files
author
Wahaj Ahmed
committed
fix(tunnels): normalize wing names in topic tunnel lookup for hyphenated dirs (#1194)
1 parent 9b35d9f commit 136bf0a

2 files changed

Lines changed: 92 additions & 6 deletions

File tree

mempalace/palace_graph.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import hashlib
1919
import json
20+
import logging
2021
import os
2122
import threading
2223
import time
@@ -27,6 +28,21 @@
2728
from .palace import get_collection as _get_palace_collection
2829
from .palace import mine_lock
2930

31+
logger = logging.getLogger("mempalace_graph")
32+
33+
34+
def _normalize_wing(wing: str | None) -> str | None:
35+
"""Normalize a wing name for consistent lookup.
36+
37+
``init`` stores wing names with hyphens and spaces replaced by underscores
38+
(e.g. ``mempalace_public``). Callers that pass the raw directory name
39+
(``mempalace-public``) would silently miss. This helper aligns the lookup
40+
key with the stored metadata.
41+
"""
42+
if wing is None:
43+
return None
44+
return wing.lower().replace(" ", "_").replace("-", "_")
45+
3046
# Module-level graph cache with TTL and write-invalidation.
3147
# Warm cache serves build_graph() in O(1); invalidate_graph_cache() clears on writes.
3248
_graph_cache_lock = threading.Lock()
@@ -215,15 +231,18 @@ def find_tunnels(wing_a: str = None, wing_b: str = None, col=None, config=None):
215231
"""
216232
nodes, edges = build_graph(col, config)
217233

234+
norm_a = _normalize_wing(wing_a)
235+
norm_b = _normalize_wing(wing_b)
236+
218237
tunnels = []
219238
for room, data in nodes.items():
220239
wings = data["wings"]
221240
if len(wings) < 2:
222241
continue
223242

224-
if wing_a and wing_a not in wings:
243+
if norm_a and norm_a not in wings:
225244
continue
226-
if wing_b and wing_b not in wings:
245+
if norm_b and norm_b not in wings:
227246
continue
228247

229248
tunnels.append(
@@ -236,6 +255,15 @@ def find_tunnels(wing_a: str = None, wing_b: str = None, col=None, config=None):
236255
}
237256
)
238257

258+
if not tunnels and (wing_a or wing_b):
259+
logger.warning(
260+
"No tunnels found for wing filter(s): wing_a=%r (normalized=%r), wing_b=%r (normalized=%r)",
261+
wing_a,
262+
norm_a,
263+
wing_b,
264+
norm_b,
265+
)
266+
239267
tunnels.sort(key=lambda x: -x["count"])
240268
return tunnels[:50]
241269

@@ -394,6 +422,9 @@ def create_tunnel(
394422
target_wing = _require_name(target_wing, "target_wing")
395423
target_room = _require_name(target_room, "target_room")
396424

425+
source_wing = _normalize_wing(source_wing)
426+
target_wing = _normalize_wing(target_wing)
427+
397428
tunnel_id = _canonical_tunnel_id(source_wing, source_room, target_wing, target_room)
398429

399430
tunnel = {
@@ -433,9 +464,13 @@ def list_tunnels(wing: str = None):
433464
Returns tunnels where ``wing`` appears as either source or target
434465
(tunnels are symmetric, so either endpoint is a valid filter match).
435466
"""
467+
norm_wing = _normalize_wing(wing)
436468
tunnels = _load_tunnels()
437-
if wing:
438-
tunnels = [t for t in tunnels if t["source"]["wing"] == wing or t["target"]["wing"] == wing]
469+
if norm_wing:
470+
tunnels = [
471+
t for t in tunnels
472+
if t["source"]["wing"] == norm_wing or t["target"]["wing"] == norm_wing
473+
]
439474
return tunnels
440475

441476

@@ -454,14 +489,15 @@ def follow_tunnels(wing: str, room: str, col=None, config=None):
454489
Given a location (wing/room), finds all tunnels leading from or to it,
455490
and optionally fetches the connected drawer content.
456491
"""
492+
norm_wing = _normalize_wing(wing) or wing
457493
tunnels = _load_tunnels()
458494
connections = []
459495

460496
for t in tunnels:
461497
src = t["source"]
462498
tgt = t["target"]
463499

464-
if src["wing"] == wing and src["room"] == room:
500+
if src["wing"] == norm_wing and src["room"] == room:
465501
connections.append(
466502
{
467503
"direction": "outgoing",
@@ -472,7 +508,7 @@ def follow_tunnels(wing: str, room: str, col=None, config=None):
472508
"tunnel_id": t["id"],
473509
}
474510
)
475-
elif tgt["wing"] == wing and tgt["room"] == room:
511+
elif tgt["wing"] == norm_wing and tgt["room"] == room:
476512
connections.append(
477513
{
478514
"direction": "incoming",
@@ -484,6 +520,9 @@ def follow_tunnels(wing: str, room: str, col=None, config=None):
484520
}
485521
)
486522

523+
if not connections:
524+
logger.warning("No explicit tunnels found for %s/%s", wing, room)
525+
487526
# If we have a collection, fetch drawer content for connected items
488527
if col and connections:
489528
drawer_ids = [c["drawer_id"] for c in connections if c.get("drawer_id")]

tests/test_palace_graph_tunnels.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,50 @@ def test_follow_tunnels_returns_connections_even_if_collection_lookup_fails(
135135
connections = palace_graph.follow_tunnels("wing_code", "auth", col=col)
136136
assert len(connections) == 1
137137
assert "drawer_preview" not in connections[0]
138+
139+
140+
class TestHyphenatedWingNormalization:
141+
"""Wing names with hyphens or spaces are normalized to underscores on init.
142+
143+
Tunnel helpers must apply the same normalization at lookup time so that
144+
``mempalace-public`` resolves to ``mempalace_public`` and matches the
145+
metadata written by ``room_detector_local.py``.
146+
"""
147+
148+
def test_list_tunnels_filters_hyphenated_wing(self, tmp_path, monkeypatch):
149+
_use_tmp_tunnel_file(monkeypatch, tmp_path)
150+
151+
palace_graph.create_tunnel("mempalace_public", "auth", "wing_people", "users")
152+
153+
assert len(palace_graph.list_tunnels("mempalace-public")) == 1
154+
assert len(palace_graph.list_tunnels("mempalace_public")) == 1
155+
156+
def test_follow_tunnels_matches_hyphenated_wing(self, tmp_path, monkeypatch):
157+
_use_tmp_tunnel_file(monkeypatch, tmp_path)
158+
159+
palace_graph.create_tunnel("mempalace_public", "auth", "wing_people", "users")
160+
161+
by_hyphen = palace_graph.follow_tunnels("mempalace-public", "auth")
162+
by_under = palace_graph.follow_tunnels("mempalace_public", "auth")
163+
assert len(by_hyphen) == 1
164+
assert len(by_under) == 1
165+
assert by_hyphen[0]["connected_wing"] == "wing_people"
166+
167+
def test_create_tunnel_normalizes_wing_names(self, tmp_path, monkeypatch):
168+
_use_tmp_tunnel_file(monkeypatch, tmp_path)
169+
170+
t = palace_graph.create_tunnel(
171+
"my-project", "src", "your-project", "dst", label="cross"
172+
)
173+
assert t["source"]["wing"] == "my_project"
174+
assert t["target"]["wing"] == "your_project"
175+
assert len(palace_graph.list_tunnels("my_project")) == 1
176+
assert len(palace_graph.list_tunnels("my-project")) == 1
177+
178+
def test_find_tunnels_warns_on_empty_result(self, tmp_path, monkeypatch, caplog):
179+
_use_tmp_tunnel_file(monkeypatch, tmp_path)
180+
# No data in collection, so build_graph returns empty nodes
181+
with caplog.at_level("WARNING", logger="mempalace_graph"):
182+
result = palace_graph.find_tunnels("nonexistent-wing")
183+
assert result == []
184+
assert "No tunnels found" in caplog.text

0 commit comments

Comments
 (0)