Skip to content

Commit 2477442

Browse files
jpheinclaude
andcommitted
fix(blob-seq-marker): tests + style nit per @igorls MemPalace#1177 review
Three new tests cover the marker contract: - test_fix_blob_seq_ids_writes_marker_after_blob_path — marker is written after a successful BLOB → INTEGER conversion. - test_fix_blob_seq_ids_writes_marker_when_already_integer — marker is written even on a no-op (already-INTEGER) path. The point of the marker is to skip sqlite3.connect() on subsequent calls, not to record that a conversion happened. - test_fix_blob_seq_ids_skips_sqlite_when_marker_present — verifies the load-bearing property via monkeypatched sqlite3.connect: when the marker exists, the function never opens sqlite. This is the bug MemPalace#1090 fix — opening Python's sqlite3 against a live ChromaDB 1.5.x WAL DB corrupts the next PersistentClient call, and we must never do it again post-migration. Also folded in @igorls's style nit: - Path(marker).touch() instead of open(marker, "a").close(). Same effect, reads cleaner. Moved Path import to the top of the module since it didn't yet exist there. 35/35 backend tests pass. The "Production: fresh MCP server + stop hook + mempalace mine subprocess" checkbox in the PR body is checked in the PR reply — the canonical 151K palace has been running this fix since MemPalace#1177 was filed (via Syncthing-replicated source on the disks daemon at v1.7.0); zero PersistentClient corruption since. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent bc24aa1 commit 2477442

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

mempalace/backends/chroma.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import os
66
import sqlite3
7+
from pathlib import Path
78
from typing import Any, Optional
89

910
import chromadb
@@ -206,7 +207,7 @@ def _fix_blob_seq_ids(palace_path: str) -> None:
206207
# confirmed to be in the INTEGER-seq_id state and future opens can skip the
207208
# sqlite3.connect() entirely.
208209
try:
209-
open(marker, "a").close()
210+
Path(marker).touch()
210211
except OSError:
211212
logger.exception("Could not write migration marker %s", marker)
212213

tests/test_backends.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,75 @@ def test_fix_blob_seq_ids_noop_without_database(tmp_path):
381381
_fix_blob_seq_ids(str(tmp_path)) # should not raise
382382

383383

384+
def test_fix_blob_seq_ids_writes_marker_after_blob_path(tmp_path):
385+
"""The .blob_seq_ids_migrated marker is written after a successful BLOB → INTEGER conversion."""
386+
from pathlib import Path
387+
from mempalace.backends.chroma import _BLOB_FIX_MARKER
388+
389+
db_path = tmp_path / "chroma.sqlite3"
390+
conn = sqlite3.connect(str(db_path))
391+
conn.execute("CREATE TABLE embeddings (rowid INTEGER PRIMARY KEY, seq_id)")
392+
conn.execute("INSERT INTO embeddings (seq_id) VALUES (?)", ((42).to_bytes(8, "big"),))
393+
conn.commit()
394+
conn.close()
395+
396+
marker = tmp_path / _BLOB_FIX_MARKER
397+
assert not marker.exists()
398+
399+
_fix_blob_seq_ids(str(tmp_path))
400+
401+
assert marker.is_file(), "marker must be written after a successful migration"
402+
403+
404+
def test_fix_blob_seq_ids_writes_marker_when_already_integer(tmp_path):
405+
"""The marker is written even when the migration is a no-op (already INTEGER).
406+
407+
The point of the marker is to skip the sqlite3 open on subsequent calls,
408+
not to record that a conversion happened. So a clean palace gets the
409+
marker on first run too — next ``_fix_blob_seq_ids`` call short-circuits
410+
before touching the sqlite3 file.
411+
"""
412+
from pathlib import Path
413+
from mempalace.backends.chroma import _BLOB_FIX_MARKER
414+
415+
db_path = tmp_path / "chroma.sqlite3"
416+
conn = sqlite3.connect(str(db_path))
417+
conn.execute("CREATE TABLE embeddings (rowid INTEGER PRIMARY KEY, seq_id INTEGER)")
418+
conn.execute("INSERT INTO embeddings (seq_id) VALUES (42)")
419+
conn.commit()
420+
conn.close()
421+
422+
marker = tmp_path / _BLOB_FIX_MARKER
423+
assert not marker.exists()
424+
425+
_fix_blob_seq_ids(str(tmp_path))
426+
427+
assert marker.is_file(), "marker must be written even when no BLOBs found"
428+
429+
430+
def test_fix_blob_seq_ids_skips_sqlite_when_marker_present(tmp_path):
431+
"""When the marker exists, ``_fix_blob_seq_ids`` does not open sqlite3.
432+
433+
This is the load-bearing property of the marker — opening Python's
434+
sqlite3 against a live ChromaDB 1.5.x WAL DB corrupts the next
435+
PersistentClient call (#1090). Once a palace has been migrated, we
436+
never want to open it again, even read-only.
437+
"""
438+
from pathlib import Path
439+
from unittest.mock import patch
440+
from mempalace.backends.chroma import _BLOB_FIX_MARKER
441+
442+
# Pre-create the marker so the function should short-circuit.
443+
db_path = tmp_path / "chroma.sqlite3"
444+
db_path.write_bytes(b"sentinel") # presence required for the function to proceed
445+
(tmp_path / _BLOB_FIX_MARKER).touch()
446+
447+
with patch("mempalace.backends.chroma.sqlite3.connect") as mock_connect:
448+
_fix_blob_seq_ids(str(tmp_path))
449+
450+
mock_connect.assert_not_called()
451+
452+
384453
# ── quarantine_stale_hnsw ─────────────────────────────────────────────────
385454

386455

0 commit comments

Comments
 (0)