Skip to content

Commit 2fa7ebd

Browse files
Tomas Pflanzerclaude
andcommitted
release: v2.0.4 — pack tests stop polluting the developer's ledger
Pack tests already monkeypatched LEDGER_PATH onto a per-test tmp dir, but if anything bypassed pytest's normal flow (REPL, IDE runner, `python -c` reproduction) the install ledger at ~/.memee/packs.json collected drift. Got noticed when 4 stray test packs (rt-pack, tamper-installer, dedup-pack, merge-pack) showed up in a real user's ledger with pytest tmp paths in the `file:` column. Two-layer fix in conftest: 1. Autouse fixture redirects `memee.engine.packs.LEDGER_PATH` to a per-test tmp file BEFORE every test runs. Existing per-test `monkeypatch.setattr` calls keep working (last-patch-wins, monkeypatch unwinds in LIFO order); they're now redundant but harmless. 2. Session-end guard snapshots size+mtime of the real ledger and raises if it moved. Loud regression detection — if a future change reintroduces a leak, the suite goes red. Verified: full test suite (362 tests trimmed of long simulations) passes and ~/.memee/packs.json is byte-identical before and after. No behaviour change for end users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d685e7c commit 2fa7ebd

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.4] — 2026-04-27
11+
12+
The "tests stop polluting the developer's ledger" patch.
13+
No behaviour change for end users.
14+
15+
### Fixed
16+
17+
- **Pack-test ledger isolation.** Pack-related tests already
18+
``monkeypatch.setattr("memee.engine.packs.LEDGER_PATH", ...)`` in
19+
every test that needs it, but a forgotten patch — or an import path
20+
that bypassed pytest entirely (REPL, IDE runner, ``python -c``) —
21+
would silently append entries to the developer's real
22+
``~/.memee/packs.json``. Two layers of defence land in this release:
23+
a session-wide autouse fixture that redirects ``LEDGER_PATH`` to a
24+
per-test ``tmp_path`` for **every** test (existing per-test patches
25+
are still honoured, just redundant), and a session-end leak guard
26+
that fails the suite loud if the real ledger's size or mtime moved.
27+
1028
## [2.0.3] — 2026-04-27
1129

1230
The "people install once and forget" patch. Three things that should

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "memee"
7-
version = "2.0.3"
7+
version = "2.0.4"
88
description = "Cross-model shared memory for AI agents. One canon. Every model, every project, every teammate."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/memee/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Memee — Institutional memory for AI agent companies."""
22

3-
__version__ = "2.0.3"
3+
__version__ = "2.0.4"

tests/conftest.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,83 @@ def session(db_engine):
4747
def org(session):
4848
"""Return the test organization."""
4949
return session.query(Organization).filter_by(name="test-org").first()
50+
51+
52+
# ── Test-time isolation of the pack-install ledger ──────────────────────
53+
#
54+
# The ledger normally lives at ``~/.memee/packs.json`` and is appended to
55+
# every time ``install_pack`` succeeds. Individual pack tests already
56+
# monkeypatch ``LEDGER_PATH``, but a forgotten patch (or a test that
57+
# imports through a side path — REPL, ``python -c``, IDE runner) silently
58+
# pollutes the developer's real ledger. We saw this happen.
59+
#
60+
# The autouse fixture below makes that impossible: every test, in every
61+
# file, gets ``memee.engine.packs.LEDGER_PATH`` repointed at a per-test
62+
# ``tmp_path``. Existing per-test ``monkeypatch.setattr`` calls keep
63+
# working — they just override the redirect with another tmp_path, which
64+
# is also fine.
65+
66+
67+
@pytest.fixture(autouse=True)
68+
def _isolate_pack_ledger(tmp_path, monkeypatch):
69+
"""Redirect ``memee.engine.packs.LEDGER_PATH`` to a per-test tmp file.
70+
71+
Belt-and-suspenders default isolation. Without this, any test (or
72+
fixture) that calls ``install_pack`` without an explicit patch would
73+
write to the developer's real ``~/.memee/packs.json``.
74+
"""
75+
# Importing the engine module here (lazily) avoids loading SQLAlchemy
76+
# for tests that never touch packs — keeps suite startup fast.
77+
try:
78+
import memee.engine.packs as _packs
79+
except ImportError:
80+
# Engine missing in some minimal environments — nothing to patch.
81+
yield
82+
return
83+
fake = tmp_path / "test-ledger" / "packs.json"
84+
fake.parent.mkdir(parents=True, exist_ok=True)
85+
monkeypatch.setattr(_packs, "LEDGER_PATH", fake)
86+
yield
87+
88+
89+
@pytest.fixture(scope="session", autouse=True)
90+
def _ledger_leak_guard():
91+
"""Fail the suite if any test wrote to the real ``~/.memee/packs.json``.
92+
93+
Snapshots size + mtime at session start; checks again at session end.
94+
A drift means *something* bypassed the autouse fixture above — that's
95+
a regression we want loud. The guard skips when no real ledger exists
96+
yet (fresh checkout, CI sandbox).
97+
"""
98+
real = Path.home() / ".memee" / "packs.json"
99+
before: tuple | None = None
100+
if real.exists():
101+
st = real.stat()
102+
before = (st.st_size, st.st_mtime_ns)
103+
104+
yield
105+
106+
if before is None:
107+
# No real ledger before the run. If one appeared, that's a leak.
108+
if real.exists():
109+
raise AssertionError(
110+
f"Test suite created a real ledger at {real}. "
111+
"Some test bypassed the _isolate_pack_ledger autouse fixture. "
112+
"Check for direct imports of LEDGER_PATH or for tests that "
113+
"construct paths relative to Path.home() directly."
114+
)
115+
return
116+
if not real.exists():
117+
# Existed before, gone now — the suite deleted it. Loud.
118+
raise AssertionError(
119+
f"Test suite removed the real ledger at {real}. "
120+
"Refuse to assume it's safe — restore from backup."
121+
)
122+
st = real.stat()
123+
after = (st.st_size, st.st_mtime_ns)
124+
if after != before:
125+
raise AssertionError(
126+
f"Test suite modified the real ledger at {real} "
127+
f"(size/mtime changed from {before} to {after}). "
128+
"A test bypassed the _isolate_pack_ledger autouse fixture."
129+
)

0 commit comments

Comments
 (0)