import os import tempfile import unittest from pathlib import Path from unittest.mock import Mock from leann.sync import FileSynchronizer, MerkleTree, hash_data class TestMerkleTreeCompare(unittest.TestCase): def test_no_changes_if_root_hash_same(self): tree1 = Mock() tree2 = Mock() tree1.root = Mock(hash="root_hash") tree2.root = Mock(hash="root_hash") added, removed, modified = MerkleTree.compare_with(tree1, tree2) self.assertEqual(added, []) self.assertEqual(removed, []) self.assertEqual(modified, []) def test_added_removed_modified(self): tree1 = Mock() tree2 = Mock() file_a_new = Mock() file_b_new = Mock() file_a_old = Mock() file_c_old = Mock() file_a_new.__eq__ = Mock(return_value=False) tree1.root = Mock( hash="new_root", children={ "a.txt": file_a_new, "b.txt": file_b_new, }, ) tree2.root = Mock( hash="old_root", children={ "a.txt": file_a_old, "c.txt": file_c_old, }, ) added, removed, modified = MerkleTree.compare_with(tree1, tree2) self.assertEqual(added, ["c.txt"]) self.assertEqual(removed, ["b.txt"]) self.assertEqual(modified, ["a.txt"]) class TestFileSynchronizer(unittest.TestCase): def test_generate_file_hashes(self): with tempfile.TemporaryDirectory() as temp_dir: file_path = Path(temp_dir) / "file.txt" file_path.write_text("hello world", encoding="utf-8") fs = FileSynchronizer(root_dir=temp_dir, auto_load=False) result = fs.generate_file_hashes() assert result == {str(file_path.resolve()): hash_data(file_path.read_bytes())} def test_generate_file_hashes_skips_binary_extensions(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) (root / "code.py").write_text("print('hi')", encoding="utf-8") (root / "image.png").write_bytes(b"\x89PNG\r\n") fs = FileSynchronizer( root_dir=temp_dir, include_extensions=[".py"], auto_load=False, ) result = fs.generate_file_hashes() assert len(result) == 1 assert str((root / "code.py").resolve()) in result def test_generate_file_hashes_explicit_files_only(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) src = root / "src" src.mkdir() readme = root / "README.md" readme.write_text("# hi", encoding="utf-8") (src / "module.py").write_text("x = 1", encoding="utf-8") (root / "assets").mkdir() (root / "assets" / "icon.png").write_bytes(b"png") fs = FileSynchronizer( explicit_files=[str(readme.resolve())], include_extensions=[".md"], auto_load=False, ) result = fs.generate_file_hashes() assert set(result.keys()) == {str(readme.resolve())} def test_build_merkle_tree(self): fs = FileSynchronizer.__new__(FileSynchronizer) file_hashes = { "a.txt": "hashA", "b.txt": "hashB", } tree = fs.build_merkle_tree(file_hashes) assert tree.root is not None assert set(tree.root.children.keys()) == {"a.txt", "b.txt"} assert tree.root.children["a.txt"].data == "hashA" assert tree.root.children["b.txt"].data == "hashB" expected_root_data = "a.txt" + "hashA" + "b.txt" + "hashB" assert tree.root.hash == hash_data(expected_root_data) def test_check_for_changes_detected(self): fs = FileSynchronizer.__new__(FileSynchronizer) fs.generate_file_hashes = Mock(return_value={"a.txt": "hash"}) fs.build_merkle_tree = Mock(return_value=Mock()) old_tree = Mock() new_tree = fs.build_merkle_tree.return_value old_tree.compare_with.return_value = (["a.txt"], [], []) fs.tree = old_tree fs.save_snapshot = Mock() changes = fs.check_for_changes() assert changes == (["a.txt"], [], []) fs.build_merkle_tree.assert_called_once_with({"a.txt": "hash"}) old_tree.compare_with.assert_called_once_with(new_tree) fs.save_snapshot.assert_called_once() assert fs.tree is new_tree def test_touch_no_false_positive(self): with tempfile.TemporaryDirectory() as temp_dir: docs = Path(temp_dir) f = docs / "a.txt" f.write_text("hello", encoding="utf-8") snapshot = str(docs / "test.pickle") fs = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) fs.detect_changes() fs.commit() os.utime(f, None) fs2 = FileSynchronizer(root_dir=str(docs), snapshot_path=snapshot) added, removed, modified = fs2.detect_changes() assert not added and not removed and not modified