Merge pull request #2018 from MemPalace/agent/fix-explicit-palace-graph-scope
fix: scope derived graph state to explicit palace
This commit is contained in:
commit
68edb110a1
|
|
@ -1005,7 +1005,15 @@ def cmd_hallways(args):
|
|||
"""List within-wing entity hallways (the auto-built associative graph)."""
|
||||
from .hallways import list_hallways
|
||||
|
||||
rows = list_hallways(getattr(args, "wing", None))
|
||||
palace_path = (
|
||||
os.path.expanduser(args.palace)
|
||||
if getattr(args, "palace", None)
|
||||
else MempalaceConfig().palace_path
|
||||
)
|
||||
rows = list_hallways(
|
||||
getattr(args, "wing", None),
|
||||
config=MempalaceConfig(palace_path=palace_path),
|
||||
)
|
||||
if not rows:
|
||||
print("No hallways yet — they are built from drawer entities when you mine.")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -342,18 +342,26 @@ class MempalaceConfig:
|
|||
Load order: env vars > config file > defaults.
|
||||
"""
|
||||
|
||||
def __init__(self, config_dir=None):
|
||||
def __init__(self, config_dir=None, palace_path=None):
|
||||
"""Initialize config.
|
||||
|
||||
Args:
|
||||
config_dir: Override config directory (useful for testing).
|
||||
Defaults to ~/.mempalace.
|
||||
palace_path: Explicit palace data directory. This is primarily
|
||||
used by CLI operations that received ``--palace``;
|
||||
it takes precedence over environment and file config.
|
||||
"""
|
||||
self._config_dir = (
|
||||
Path(config_dir) if config_dir else Path(os.path.expanduser("~/.mempalace"))
|
||||
)
|
||||
self._config_file = self._config_dir / "config.json"
|
||||
self._people_map_file = self._config_dir / "people_map.json"
|
||||
self._palace_path_override = (
|
||||
os.path.abspath(os.path.expanduser(str(palace_path)))
|
||||
if palace_path is not None
|
||||
else None
|
||||
)
|
||||
self._file_config = {}
|
||||
|
||||
if self._config_file.exists():
|
||||
|
|
@ -366,6 +374,8 @@ class MempalaceConfig:
|
|||
@property
|
||||
def palace_path(self):
|
||||
"""Path to the memory palace data directory."""
|
||||
if self._palace_path_override is not None:
|
||||
return self._palace_path_override
|
||||
env_val = os.environ.get("MEMPALACE_PALACE_PATH") or os.environ.get("MEMPAL_PALACE_PATH")
|
||||
if env_val:
|
||||
# Normalize: expand ~ and collapse .. to match the CLI --palace
|
||||
|
|
|
|||
|
|
@ -705,7 +705,7 @@ def mine_convos(
|
|||
)
|
||||
|
||||
|
||||
def _compute_hallways_for_wing_safe(wing, collection, drawers_filed):
|
||||
def _compute_hallways_for_wing_safe(wing, collection, drawers_filed, config=None):
|
||||
"""Auto-populate the associative graph from the entities just mined.
|
||||
|
||||
Best-effort: hallway computation must never fail an otherwise-good mine, and is
|
||||
|
|
@ -716,7 +716,7 @@ def _compute_hallways_for_wing_safe(wing, collection, drawers_filed):
|
|||
try:
|
||||
from .hallways import compute_hallways_for_wing
|
||||
|
||||
compute_hallways_for_wing(wing, col=collection)
|
||||
compute_hallways_for_wing(wing, col=collection, config=config)
|
||||
except Exception as exc:
|
||||
print(f" (hallways skipped: {exc})")
|
||||
|
||||
|
|
@ -732,7 +732,7 @@ def _mine_convos_impl(
|
|||
):
|
||||
from .config import MempalaceConfig
|
||||
|
||||
palace_config = MempalaceConfig()
|
||||
palace_config = MempalaceConfig(palace_path=palace_path)
|
||||
cfg_chunk_size = palace_config.chunk_size
|
||||
# Only override convo_miner's MIN_CHUNK_SIZE when the user has set
|
||||
# min_chunk_size explicitly. min_chunk_size_explicit returns the
|
||||
|
|
@ -889,7 +889,7 @@ def _mine_convos_impl(
|
|||
# Compute hallways before the FTS5 validation: the latter opens a direct sqlite
|
||||
# connection to the Chroma DB, which can invalidate the live collection handle on
|
||||
# some Chroma builds and make the hallway fetch fail.
|
||||
_compute_hallways_for_wing_safe(wing, collection, total_drawers)
|
||||
_compute_hallways_for_wing_safe(wing, collection, total_drawers, config=palace_config)
|
||||
_validate_palace_fts5_after_mine(palace_path)
|
||||
|
||||
print(f"\n{'=' * 55}")
|
||||
|
|
|
|||
|
|
@ -750,7 +750,7 @@ def mine_formats(
|
|||
# min_chunk_size) are now threaded through chunk_text below, so users
|
||||
# who customized their config see the effect in format-mode mining.
|
||||
# Per PR #1555 review (Gemini #3).
|
||||
palace_config = MempalaceConfig()
|
||||
palace_config = MempalaceConfig(palace_path=palace_path)
|
||||
|
||||
format_path = Path(format_dir).expanduser().resolve()
|
||||
if not wing:
|
||||
|
|
@ -945,7 +945,7 @@ def mine_formats(
|
|||
# skipped quietly.
|
||||
if not dry_run:
|
||||
try:
|
||||
tunnels_added = _compute_topic_tunnels_for_wing(wing)
|
||||
tunnels_added = _compute_topic_tunnels_for_wing(wing, config=palace_config)
|
||||
if tunnels_added:
|
||||
print(f"\n Topic tunnels: +{tunnels_added} cross-wing link(s)")
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ def compute_hallways_for_wing(
|
|||
wing: str,
|
||||
col=None,
|
||||
min_count: int = 2,
|
||||
config=None,
|
||||
) -> list[dict]:
|
||||
"""Compute entity-pair hallways for one wing.
|
||||
|
||||
|
|
@ -229,6 +230,10 @@ def compute_hallways_for_wing(
|
|||
hallway between two entities. Default 2 — single co-occurrences
|
||||
are noise (entities mentioned together once in one drawer);
|
||||
two or more is a real signal. Clamped to ``>=1``.
|
||||
config: Optional ``MempalaceConfig`` selecting the palace-scoped
|
||||
hallway sidecar. Callers using an explicit palace path must pass
|
||||
the matching config so derived graph state cannot leak into the
|
||||
default palace.
|
||||
|
||||
Returns:
|
||||
List of hallway dicts created for this wing. Records for other
|
||||
|
|
@ -310,7 +315,7 @@ def compute_hallways_for_wing(
|
|||
# across recomputes. Without this preservation, every mine wipes
|
||||
# the connection weights accumulated through use — defeating the
|
||||
# living-connection layer entirely.
|
||||
existing = _load_hallways()
|
||||
existing = _load_hallways(config)
|
||||
existing_dynamics_lookup: dict = {}
|
||||
for h in existing:
|
||||
if h.get("wing") != wing:
|
||||
|
|
@ -360,7 +365,7 @@ def compute_hallways_for_wing(
|
|||
|
||||
# 4. Persist — preserve other-wing records, replace this wing's records.
|
||||
preserved_other_wings = [h for h in existing if h.get("wing") != wing]
|
||||
_save_hallways(preserved_other_wings + created)
|
||||
_save_hallways(preserved_other_wings + created, config)
|
||||
|
||||
return created
|
||||
|
||||
|
|
@ -370,19 +375,19 @@ def compute_hallways_for_wing(
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def list_hallways(wing: Optional[str] = None) -> list[dict]:
|
||||
def list_hallways(wing: Optional[str] = None, config=None) -> list[dict]:
|
||||
"""List hallway records. Filter by ``wing`` if specified."""
|
||||
all_hallways = _load_hallways()
|
||||
all_hallways = _load_hallways(config)
|
||||
if wing is None:
|
||||
return list(all_hallways)
|
||||
return [h for h in all_hallways if h.get("wing") == wing]
|
||||
|
||||
|
||||
def delete_hallway(hallway_id: str) -> bool:
|
||||
def delete_hallway(hallway_id: str, config=None) -> bool:
|
||||
"""Remove one hallway record by id. Returns True if a record was removed."""
|
||||
hallways = _load_hallways()
|
||||
hallways = _load_hallways(config)
|
||||
filtered = [h for h in hallways if h.get("id") != hallway_id]
|
||||
if len(filtered) == len(hallways):
|
||||
return False
|
||||
_save_hallways(filtered)
|
||||
_save_hallways(filtered, config)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1873,12 +1873,15 @@ def _mine_impl(
|
|||
break
|
||||
|
||||
if not dry_run:
|
||||
from .config import MempalaceConfig
|
||||
|
||||
graph_config = MempalaceConfig(palace_path=palace_path)
|
||||
# Cross-wing topic tunnels: after every file in this wing has been
|
||||
# processed, link this wing to any other wing that shares a
|
||||
# confirmed TOPIC label. Out of scope for v1: manifest-dependency
|
||||
# overlap, per-topic allow/deny lists, search-result surfacing.
|
||||
try:
|
||||
tunnels_added = _compute_topic_tunnels_for_wing(wing)
|
||||
tunnels_added = _compute_topic_tunnels_for_wing(wing, config=graph_config)
|
||||
if tunnels_added:
|
||||
print(f"\n Topic tunnels: +{tunnels_added} cross-wing link(s)")
|
||||
except Exception as e:
|
||||
|
|
@ -1894,7 +1897,9 @@ def _mine_impl(
|
|||
# must never fail a mine; it's a derived analytic, not load-bearing
|
||||
# for the drawer write that already committed above.
|
||||
try:
|
||||
hallways_created = compute_hallways_for_wing(wing, col=collection)
|
||||
hallways_created = compute_hallways_for_wing(
|
||||
wing, col=collection, config=graph_config
|
||||
)
|
||||
if hallways_created:
|
||||
print(f"\n Hallways: +{len(hallways_created)} within-wing entity link(s)")
|
||||
except Exception as e:
|
||||
|
|
@ -1910,7 +1915,7 @@ def _mine_impl(
|
|||
# ``kind="entity"`` / ``kind="topic"``. Same fault-tolerance
|
||||
# pattern: never fail a mine over a derived analytic.
|
||||
try:
|
||||
entity_tunnels_added = _compute_entity_tunnels_for_wing(wing)
|
||||
entity_tunnels_added = _compute_entity_tunnels_for_wing(wing, config=graph_config)
|
||||
if entity_tunnels_added:
|
||||
print(f"\n Entity tunnels: +{entity_tunnels_added} cross-wing entity link(s)")
|
||||
except Exception as e:
|
||||
|
|
@ -2034,7 +2039,7 @@ def _cleanup_mine_pid_file() -> None:
|
|||
pass
|
||||
|
||||
|
||||
def _compute_topic_tunnels_for_wing(wing: str) -> int:
|
||||
def _compute_topic_tunnels_for_wing(wing: str, config=None) -> int:
|
||||
"""Drop tunnels between ``wing`` and every other wing that shares
|
||||
confirmed topics, honoring the ``topic_tunnel_min_count`` config knob.
|
||||
|
||||
|
|
@ -2047,13 +2052,13 @@ def _compute_topic_tunnels_for_wing(wing: str) -> int:
|
|||
topics_map = get_topics_by_wing()
|
||||
if not topics_map or wing not in topics_map:
|
||||
return 0
|
||||
cfg = MempalaceConfig()
|
||||
cfg = config or MempalaceConfig()
|
||||
min_count = cfg.topic_tunnel_min_count
|
||||
created = topic_tunnels_for_wing(wing, topics_map, min_count=min_count)
|
||||
created = topic_tunnels_for_wing(wing, topics_map, min_count=min_count, config=cfg)
|
||||
return len(created)
|
||||
|
||||
|
||||
def _compute_entity_tunnels_for_wing(wing: str) -> int:
|
||||
def _compute_entity_tunnels_for_wing(wing: str, config=None) -> int:
|
||||
"""Drop tunnels between ``wing`` and every other wing that shares an
|
||||
entity via the within-wing hallway primitive.
|
||||
|
||||
|
|
@ -2071,10 +2076,10 @@ def _compute_entity_tunnels_for_wing(wing: str) -> int:
|
|||
from .hallways import list_hallways
|
||||
from .palace_graph import entity_tunnels_for_wing
|
||||
|
||||
hallways = list_hallways()
|
||||
hallways = list_hallways(config=config)
|
||||
if not hallways:
|
||||
return 0
|
||||
created = entity_tunnels_for_wing(wing, hallways)
|
||||
created = entity_tunnels_for_wing(wing, hallways, config=config)
|
||||
return len(created)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -494,6 +494,7 @@ def create_tunnel(
|
|||
source_drawer_id: str = None,
|
||||
target_drawer_id: str = None,
|
||||
kind: str = "explicit",
|
||||
config=None,
|
||||
):
|
||||
"""Create an explicit (symmetric) tunnel between two locations in the palace.
|
||||
|
||||
|
|
@ -520,6 +521,9 @@ def create_tunnel(
|
|||
topical link where rooms are synthetic ``topic:<name>``
|
||||
identifiers). Preserved on the stored dict so readers can
|
||||
distinguish real-room traversals from topic connections.
|
||||
config: Optional ``MempalaceConfig`` selecting the palace and its
|
||||
tunnel sidecar. Explicit-path callers must pass the matching
|
||||
config instead of falling back to the ambient default palace.
|
||||
|
||||
Returns:
|
||||
The stored tunnel dict.
|
||||
|
|
@ -545,7 +549,7 @@ def create_tunnel(
|
|||
# mempalace.yaml from disk; before this change the helpers each
|
||||
# instantiated their own, triggering several redundant disk reads per
|
||||
# create_tunnel call (flagged by gemini-code-assist on #1469).
|
||||
config = MempalaceConfig()
|
||||
config = config or MempalaceConfig()
|
||||
|
||||
# Validate room existence for explicit tunnels only. Use the verbatim wing
|
||||
# slugs here so #1504's hyphen-preserving write path remains intact.
|
||||
|
|
@ -742,6 +746,7 @@ def compute_topic_tunnels(
|
|||
topics_by_wing: dict,
|
||||
min_count: int = 1,
|
||||
label_prefix: str = "shared topic",
|
||||
config=None,
|
||||
) -> list[dict]:
|
||||
"""Create tunnels for every pair of wings that share >= ``min_count`` topics.
|
||||
|
||||
|
|
@ -817,6 +822,7 @@ def compute_topic_tunnels(
|
|||
target_room=room,
|
||||
label=f"{label_prefix}: {topic_name}",
|
||||
kind="topic",
|
||||
config=config,
|
||||
)
|
||||
created.append(tunnel)
|
||||
return created
|
||||
|
|
@ -827,6 +833,7 @@ def topic_tunnels_for_wing(
|
|||
topics_by_wing: dict,
|
||||
min_count: int = 1,
|
||||
label_prefix: str = "shared topic",
|
||||
config=None,
|
||||
) -> list[dict]:
|
||||
"""Compute topic tunnels involving a single wing.
|
||||
|
||||
|
|
@ -871,6 +878,7 @@ def topic_tunnels_for_wing(
|
|||
slice_map,
|
||||
min_count=min_count,
|
||||
label_prefix=label_prefix,
|
||||
config=config,
|
||||
)
|
||||
)
|
||||
return created
|
||||
|
|
@ -880,6 +888,7 @@ def entity_tunnels_for_wing(
|
|||
wing: str,
|
||||
hallways: list,
|
||||
label_prefix: str = "shared entity",
|
||||
config=None,
|
||||
) -> list:
|
||||
"""Compute entity tunnels involving a single wing.
|
||||
|
||||
|
|
@ -946,6 +955,7 @@ def entity_tunnels_for_wing(
|
|||
target_room=room,
|
||||
label=f"{label_prefix}: {entity}",
|
||||
kind="entity",
|
||||
config=config,
|
||||
)
|
||||
created.append(tunnel)
|
||||
return created
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def test_lists_sorted_by_count(monkeypatch, capsys):
|
|||
"label": "A <-> B (x3)",
|
||||
},
|
||||
]
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None: list(rows))
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None, config=None: list(rows))
|
||||
cmd_hallways(Namespace(wing=None, limit=50))
|
||||
out = capsys.readouterr().out
|
||||
assert "2 hallway(s)" in out
|
||||
|
|
@ -37,7 +37,7 @@ def test_respects_limit(monkeypatch, capsys):
|
|||
{"entity_a": f"E{i}", "entity_b": "X", "co_occurrence_count": i, "label": f"E{i} <-> X"}
|
||||
for i in range(5)
|
||||
]
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None: list(rows))
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None, config=None: list(rows))
|
||||
cmd_hallways(Namespace(wing=None, limit=2))
|
||||
assert capsys.readouterr().out.count("<->") == 2
|
||||
|
||||
|
|
@ -47,13 +47,28 @@ def test_negative_limit_shows_nothing_not_tail(monkeypatch, capsys):
|
|||
{"entity_a": f"E{i}", "entity_b": "X", "co_occurrence_count": i, "label": f"E{i} <-> X"}
|
||||
for i in range(5)
|
||||
]
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None: list(rows))
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None, config=None: list(rows))
|
||||
cmd_hallways(Namespace(wing=None, limit=-2))
|
||||
# A negative limit must not slice from the end (which would print all-but-2).
|
||||
assert capsys.readouterr().out.count("<->") == 0
|
||||
|
||||
|
||||
def test_empty_message(monkeypatch, capsys):
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None: [])
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", lambda wing=None, config=None: [])
|
||||
cmd_hallways(Namespace(wing="x", limit=50))
|
||||
assert "No hallways yet" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_explicit_palace_scopes_hallway_listing(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
|
||||
def fake_list(wing=None, config=None):
|
||||
calls.append((wing, config.palace_path))
|
||||
return []
|
||||
|
||||
selected = tmp_path / "selected" / "palace"
|
||||
monkeypatch.setattr(hallways_mod, "list_hallways", fake_list)
|
||||
|
||||
cmd_hallways(Namespace(wing="wing_aya", limit=50, palace=str(selected)))
|
||||
|
||||
assert calls == [("wing_aya", str(selected))]
|
||||
|
|
|
|||
|
|
@ -888,3 +888,18 @@ def test_max_backups_bad_env_falls_back_to_config(monkeypatch, tmp_path):
|
|||
monkeypatch.setenv("MEMPALACE_MAX_BACKUPS", "garbage")
|
||||
cfg = MempalaceConfig(config_dir=str(tmp_path))
|
||||
assert cfg.max_backups == 4
|
||||
|
||||
|
||||
def test_explicit_palace_path_overrides_env_and_file_config(monkeypatch, tmp_path):
|
||||
configured = tmp_path / "configured" / "palace"
|
||||
explicit = tmp_path / "explicit" / "../explicit" / "palace"
|
||||
with open(tmp_path / "config.json", "w") as f:
|
||||
json.dump({"palace_path": str(configured)}, f)
|
||||
monkeypatch.setenv("MEMPALACE_PALACE_PATH", str(tmp_path / "environment" / "palace"))
|
||||
|
||||
cfg = MempalaceConfig(config_dir=str(tmp_path), palace_path=str(explicit))
|
||||
|
||||
expected = os.path.abspath(os.path.expanduser(str(explicit)))
|
||||
assert cfg.palace_path == expected
|
||||
assert cfg.hallway_file == os.path.join(os.path.dirname(expected), "hallways.json")
|
||||
assert cfg.tunnel_file == os.path.join(os.path.dirname(expected), "tunnels.json")
|
||||
|
|
|
|||
|
|
@ -1158,7 +1158,9 @@ def test_mine_formats_calls_compute_topic_tunnels_after_loop(_mine_formats_mocks
|
|||
patch("mempalace.format_miner._compute_topic_tunnels_for_wing", return_value=0) as p_tun,
|
||||
):
|
||||
mine_formats(format_dir=str(tmp), palace_path=str(tmp / "palace"), wing="wing_aya")
|
||||
p_tun.assert_called_once_with("wing_aya")
|
||||
p_tun.assert_called_once()
|
||||
assert p_tun.call_args.args == ("wing_aya",)
|
||||
assert p_tun.call_args.kwargs["config"].palace_path == str(tmp / "palace")
|
||||
|
||||
|
||||
def test_mine_formats_tunnel_failure_does_not_crash_mine(_mine_formats_mocks):
|
||||
|
|
@ -1518,13 +1520,16 @@ def test_mine_formats_threads_chunk_size_from_user_config(monkeypatch, tmp_path:
|
|||
|
||||
# Inject custom config values via a fake MempalaceConfig.
|
||||
class _FakeMempalaceConfig:
|
||||
def __init__(self, **kwargs):
|
||||
self._palace_path = kwargs.get("palace_path", str(tmp_path / "palace"))
|
||||
|
||||
chunk_size = 1234
|
||||
chunk_overlap = 56
|
||||
min_chunk_size = 78
|
||||
|
||||
@property
|
||||
def palace_path(self):
|
||||
return str(tmp_path / "palace")
|
||||
return self._palace_path
|
||||
|
||||
monkeypatch.setattr(format_miner, "MempalaceConfig", _FakeMempalaceConfig)
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,24 @@ class TestHallwayStorage:
|
|||
|
||||
|
||||
class TestComputeHallways:
|
||||
def test_explicit_config_scopes_persistence_to_selected_palace(self, tmp_path):
|
||||
from mempalace.config import MempalaceConfig
|
||||
|
||||
default_cfg = MempalaceConfig(palace_path=tmp_path / "default" / "palace")
|
||||
selected_cfg = MempalaceConfig(palace_path=tmp_path / "selected" / "palace")
|
||||
col = _fake_collection(
|
||||
[
|
||||
{"wing": "wing_aya", "room": "diary", "entities": "Aya;Lumi"},
|
||||
{"wing": "wing_aya", "room": "letters", "entities": "Aya;Lumi"},
|
||||
]
|
||||
)
|
||||
|
||||
created = hallways_mod.compute_hallways_for_wing("wing_aya", col=col, config=selected_cfg)
|
||||
|
||||
assert len(created) == 1
|
||||
assert hallways_mod.list_hallways(config=selected_cfg) == created
|
||||
assert hallways_mod.list_hallways(config=default_cfg) == []
|
||||
|
||||
def test_returns_empty_for_unknown_wing(self, tmp_path, monkeypatch):
|
||||
"""Wing with no drawers → no hallways, no crash."""
|
||||
_use_tmp_hallway_file(monkeypatch, tmp_path)
|
||||
|
|
@ -300,6 +318,19 @@ class TestHallwayQuery:
|
|||
hallways_mod._save_hallways([{"id": "h1", "wing": "wing_aya"}])
|
||||
assert hallways_mod.delete_hallway("nonexistent") is False
|
||||
|
||||
def test_delete_hallway_uses_selected_palace_config(self, tmp_path):
|
||||
from mempalace.config import MempalaceConfig
|
||||
|
||||
default_cfg = MempalaceConfig(palace_path=tmp_path / "default" / "palace")
|
||||
selected_cfg = MempalaceConfig(palace_path=tmp_path / "selected" / "palace")
|
||||
record = {"id": "h1", "wing": "wing_aya"}
|
||||
hallways_mod._save_hallways([record], default_cfg)
|
||||
hallways_mod._save_hallways([record], selected_cfg)
|
||||
|
||||
assert hallways_mod.delete_hallway("h1", config=selected_cfg) is True
|
||||
assert hallways_mod.list_hallways(config=selected_cfg) == []
|
||||
assert hallways_mod.list_hallways(config=default_cfg) == [record]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# L7 dynamics integration — hallway records carry strength/stability/etc
|
||||
|
|
|
|||
|
|
@ -95,8 +95,8 @@ def test_mine_computes_hallways_for_wing_post_mine(monkeypatch):
|
|||
|
||||
hallway_calls = []
|
||||
|
||||
def fake_compute(wing, col=None, min_count=2):
|
||||
hallway_calls.append({"wing": wing, "col": col, "min_count": min_count})
|
||||
def fake_compute(wing, col=None, min_count=2, config=None):
|
||||
hallway_calls.append({"wing": wing, "col": col, "min_count": min_count, "config": config})
|
||||
return [] # no hallways materialized — that's not what we're testing
|
||||
|
||||
# Patch at the call site (mempalace.miner.compute_hallways_for_wing) so
|
||||
|
|
@ -134,6 +134,7 @@ def test_mine_computes_hallways_for_wing_post_mine(monkeypatch):
|
|||
assert call["col"] is not None, (
|
||||
"must pass the live collection so hallways can query drawers"
|
||||
)
|
||||
assert call["config"].palace_path == str(palace_path)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
|
@ -147,7 +148,7 @@ def test_mine_hallway_failure_does_not_crash_mine(monkeypatch):
|
|||
"""
|
||||
from mempalace import miner as miner_mod
|
||||
|
||||
def angry_compute(wing, col=None, min_count=2):
|
||||
def angry_compute(wing, col=None, min_count=2, config=None):
|
||||
raise RuntimeError("simulated hallway-compute explosion")
|
||||
|
||||
monkeypatch.setattr(miner_mod, "compute_hallways_for_wing", angry_compute)
|
||||
|
|
@ -194,8 +195,8 @@ def test_mine_computes_entity_tunnels_for_wing_post_mine(monkeypatch):
|
|||
|
||||
entity_tunnel_calls = []
|
||||
|
||||
def fake_compute(wing):
|
||||
entity_tunnel_calls.append({"wing": wing})
|
||||
def fake_compute(wing, config=None):
|
||||
entity_tunnel_calls.append({"wing": wing, "config": config})
|
||||
return 0 # no tunnels — that's not what we're testing here
|
||||
|
||||
# Patch at the call site (mempalace.miner._compute_entity_tunnels_for_wing)
|
||||
|
|
@ -227,6 +228,7 @@ def test_mine_computes_entity_tunnels_for_wing_post_mine(monkeypatch):
|
|||
f"got {len(entity_tunnel_calls)}"
|
||||
)
|
||||
assert entity_tunnel_calls[0]["wing"] == "test_project"
|
||||
assert entity_tunnel_calls[0]["config"].palace_path == str(palace_path)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
|
@ -241,7 +243,7 @@ def test_mine_entity_tunnel_failure_does_not_crash_mine(monkeypatch):
|
|||
"""
|
||||
from mempalace import miner as miner_mod
|
||||
|
||||
def angry_compute(wing):
|
||||
def angry_compute(wing, config=None):
|
||||
raise RuntimeError("simulated entity-tunnel-compute explosion")
|
||||
|
||||
monkeypatch.setattr(miner_mod, "_compute_entity_tunnels_for_wing", angry_compute)
|
||||
|
|
|
|||
|
|
@ -201,6 +201,21 @@ class TestTopicTunnels:
|
|||
dedup and persistence with explicit tunnels.
|
||||
"""
|
||||
|
||||
def test_explicit_config_scopes_persistence_to_selected_palace(self, tmp_path):
|
||||
from mempalace.config import MempalaceConfig
|
||||
|
||||
default_cfg = MempalaceConfig(palace_path=tmp_path / "default" / "palace")
|
||||
selected_cfg = MempalaceConfig(palace_path=tmp_path / "selected" / "palace")
|
||||
topics_by_wing = {"wing_alpha": ["OpenAPI"], "wing_beta": ["OpenAPI"]}
|
||||
|
||||
created = palace_graph.compute_topic_tunnels(
|
||||
topics_by_wing, min_count=1, config=selected_cfg
|
||||
)
|
||||
|
||||
assert len(created) == 1
|
||||
assert palace_graph._load_tunnels(selected_cfg) == created
|
||||
assert palace_graph._load_tunnels(default_cfg) == []
|
||||
|
||||
def test_compute_topic_tunnels_creates_link_for_shared_topic(self, tmp_path, monkeypatch):
|
||||
_use_tmp_tunnel_file(monkeypatch, tmp_path)
|
||||
topics_by_wing = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue