Files
mempalace/tests/test_migrate_wings.py
T
Igor Lins e Silva e46374d24b feat(migrate): mempalace migrate-wings — normalize legacy wing names
Follow-up to the wing-name normalization (#1675). Palaces built before the
rule filed drawers under leading/trailing-separator wing names (e.g. a
Claude Code path-encoded dir `-home-user-proj` -> `_home_user_proj`); the
new derivation strips those, so searches/diary reads under the new name miss
the old memories — the history is split, not lost.

`migrate_wing_names` (CLI: `mempalace migrate-wings [--dry-run] [--yes]`)
re-keys the `wing` metadata field on drawers and closets to the normalized
form, merging collisions. Design (verified against the codebase):

- Drawer/closet IDs embed the wing as an opaque prefix that is never decoded
  back into a wing, and mining idempotency keys on `source_file`, so IDs are
  left untouched: closet ->drawer_id pointers stay valid and future mining
  still skips already-mined files. No risky extract-rebuild needed.
- `topics_by_wing` registry keys are re-keyed (merging on collision).
- Tunnels resolve via existing read-time normalization and need no rewrite.
- Backend-agnostic (uses the get/update collection abstraction); idempotent;
  dry-run-able. The strip is applied by the migration itself, so it is
  correct regardless of whether the running build's normalize_wing_name
  already carries the #1675 change.

Tests: pure planner coverage (strip, no-op, empty/non-string, collision) +
hermetic backend integration (relabel, merge, dry-run, idempotency).
2026-06-06 03:11:46 -03:00

154 lines
5.1 KiB
Python

"""Tests for the wing-name normalization migration (migrate-wings).
normalize_wing_name strips leading/trailing separators (#1675); palaces built
before that rule filed drawers under the old name (e.g. ``_alpha``).
``migrate_wing_names`` re-keys the ``wing`` metadata in place so those memories
stay discoverable under the new name, merging collisions. IDs are left untouched
(they are opaque keys), and the pass is idempotent.
"""
from mempalace.migrate import migrate_wing_names, plan_wing_renames
# --- pure planner ---------------------------------------------------------
def test_plan_renames_strips_leading_and_trailing():
summary, updates = plan_wing_renames(
[
("d1", {"wing": "_alpha", "room": "r"}),
("d2", {"wing": "beta_", "room": "r"}),
("d3", {"wing": "clean", "room": "r"}),
]
)
assert dict(summary) == {("_alpha", "alpha"): 1, ("beta_", "beta"): 1}
assert {u[0] for u in updates} == {"d1", "d2"}
# only 'wing' is rewritten; other metadata is preserved
by_id = {u[0]: u[1] for u in updates}
assert by_id["d1"]["wing"] == "alpha"
assert by_id["d1"]["room"] == "r"
assert by_id["d2"]["wing"] == "beta"
def test_plan_renames_noop_for_clean_wings():
summary, updates = plan_wing_renames([("d", {"wing": "already_clean", "room": "r"})])
assert not summary
assert not updates
def test_plan_renames_ignores_empty_nonstring_and_all_separator():
_, updates = plan_wing_renames(
[
("a", {"wing": "_"}), # normalizes to "" -> skip (never strand a drawer)
("b", {"wing": ""}),
("c", {"wing": None}),
("d", {}),
]
)
assert updates == []
def test_plan_renames_collision_maps_both_to_same_target():
_, updates = plan_wing_renames(
[
("d1", {"wing": "_gamma"}),
("d2", {"wing": "gamma_"}),
]
)
assert {u[1]["wing"] for u in updates} == {"gamma"}
# --- integration over a real backend collection ---------------------------
def _seed(palace, rows):
from mempalace.palace import get_collection
col = get_collection(str(palace), create=True)
# Explicit embeddings keep the test hermetic (no embedding model needed) —
# the migration only reads/writes metadata.
col.upsert(
ids=[r["id"] for r in rows],
documents=[r["doc"] for r in rows],
metadatas=[r["meta"] for r in rows],
embeddings=[[float(i + 1)] * 8 for i in range(len(rows))],
)
return col
def _wing_ids(palace, wing):
from mempalace.palace import get_collection
col = get_collection(str(palace), create=False)
res = col.get(where={"wing": wing}, include=["metadatas"])
return set(res.ids if hasattr(res, "ids") else res["ids"])
def _meta(wing, room, source_file, idx):
return {"wing": wing, "room": room, "source_file": source_file, "chunk_index": idx}
def test_migrate_relabels_old_format_wings(tmp_path):
palace = tmp_path / "palace"
palace.mkdir()
_seed(
palace,
[
{"id": "drawer__alpha_r_1", "doc": "auth jwt", "meta": _meta("_alpha", "r", "a.py", 0)},
{"id": "drawer_beta__r_2", "doc": "db alembic", "meta": _meta("beta_", "r", "b.py", 0)},
{
"id": "drawer_clean_r_3",
"doc": "react query",
"meta": _meta("clean", "r", "c.py", 0),
},
],
)
assert migrate_wing_names(str(palace), confirm=True) is True
assert _wing_ids(palace, "alpha") == {"drawer__alpha_r_1"}
assert _wing_ids(palace, "beta") == {"drawer_beta__r_2"}
assert _wing_ids(palace, "_alpha") == set()
assert _wing_ids(palace, "beta_") == set()
# an already-clean wing is left untouched
assert _wing_ids(palace, "clean") == {"drawer_clean_r_3"}
def test_migrate_merges_collision_into_existing_wing(tmp_path):
palace = tmp_path / "palace"
palace.mkdir()
_seed(
palace,
[
{"id": "drawer_gamma_r_1", "doc": "current", "meta": _meta("gamma", "r", "g1.py", 0)},
{"id": "drawer__gamma_r_2", "doc": "legacy", "meta": _meta("_gamma", "r", "g2.py", 0)},
],
)
migrate_wing_names(str(palace), confirm=True)
assert _wing_ids(palace, "gamma") == {"drawer_gamma_r_1", "drawer__gamma_r_2"}
assert _wing_ids(palace, "_gamma") == set()
def test_migrate_dry_run_changes_nothing(tmp_path):
palace = tmp_path / "palace"
palace.mkdir()
_seed(palace, [{"id": "drawer__x_r_1", "doc": "d", "meta": _meta("_x", "r", "x.py", 0)}])
assert migrate_wing_names(str(palace), dry_run=True) is True
# nothing actually moved
assert _wing_ids(palace, "_x") == {"drawer__x_r_1"}
assert _wing_ids(palace, "x") == set()
def test_migrate_is_idempotent(tmp_path):
palace = tmp_path / "palace"
palace.mkdir()
_seed(palace, [{"id": "drawer__y_r_1", "doc": "d", "meta": _meta("_y", "r", "y.py", 0)}])
assert migrate_wing_names(str(palace), confirm=True) is True
# second run finds nothing left to normalize
assert migrate_wing_names(str(palace), confirm=True) is False
assert _wing_ids(palace, "y") == {"drawer__y_r_1"}