import json from deerflow.agents.memory.thread_storage import SqliteThreadMemoryStorage def _payload(): return { "ownerId": None, "user": { "workContext": {"summary": "Frontend engineer", "updatedAt": "2026-05-08T00:00:00Z"}, "personalContext": {"summary": "Prefers Chinese", "updatedAt": "2026-05-08T00:00:00Z"}, "topOfMind": {"summary": "Thread memory migration", "updatedAt": "2026-05-08T00:00:00Z"}, }, "history": { "recentMonths": {"summary": "Worked on memory features", "updatedAt": "2026-05-08T00:00:00Z"}, "earlierContext": {"summary": "", "updatedAt": ""}, "longTermBackground": {"summary": "Builds web products", "updatedAt": "2026-05-08T00:00:00Z"}, }, "facts": [], } def test_sqlite_thread_memory_compare_and_swap(tmp_path): storage = SqliteThreadMemoryStorage(str(tmp_path / "thread-memory.db")) thread_id = "thread-1" assert storage.save(thread_id, _payload(), expected_version=0) is True loaded = storage.load(thread_id) assert loaded is not None assert loaded["memoryVersion"] == 0 # wrong expected version should fail assert storage.save(thread_id, _payload(), expected_version=9) is False # correct version should pass and increment assert storage.save(thread_id, _payload(), expected_version=0) is True loaded2 = storage.load(thread_id) assert loaded2 is not None assert loaded2["memoryVersion"] == 1 def test_sqlite_thread_memory_saves_json_payload(tmp_path): db_path = tmp_path / "thread-memory.db" storage = SqliteThreadMemoryStorage(str(db_path)) thread_id = "thread-md" assert storage.save(thread_id, _payload(), expected_version=0) is True with storage._lock: row = storage._conn.execute("SELECT memory_json FROM thread_memory WHERE thread_id = ?", (thread_id,)).fetchone() assert row is not None assert isinstance(row[0], str) parsed = json.loads(row[0]) assert parsed["user"]["workContext"]["summary"] == "Frontend engineer" def test_sqlite_thread_memory_uses_owner_id_column_when_json_missing_owner(tmp_path): db_path = tmp_path / "thread-memory.db" storage = SqliteThreadMemoryStorage(str(db_path)) thread_id = "thread-load" payload = _payload() with storage._lock: storage._conn.execute( """ INSERT INTO thread_memory (thread_id, owner_id, memory_json, memory_version, last_updated) VALUES (?, ?, ?, 0, datetime('now')) """, ( thread_id, "owner-1", json.dumps( { "user": payload["user"], "history": payload["history"], "facts": [], }, ensure_ascii=False, ), ), ) storage._conn.commit() loaded = storage.load(thread_id) assert loaded is not None assert loaded["ownerId"] == "owner-1" assert loaded["user"]["workContext"]["summary"] == "Frontend engineer" assert loaded["facts"] == [] def test_sqlite_thread_memory_backfill_is_noop_after_migration(tmp_path): db_path = tmp_path / "thread-memory.db" storage = SqliteThreadMemoryStorage(str(db_path)) assert storage.count_legacy_rows() == 0 stats = storage.backfill_legacy_rows() assert stats["scanned"] == 0 assert stats["updated"] == 0 assert stats["failed"] == 0 assert storage.count_legacy_rows() == 0