@@ -66,3 +66,208 @@ async def test_handle_changes_empty_set(watch_service, project_config, test_proj
6666
6767 # Verify synced_files wasn't changed
6868 assert watch_service .state .synced_files == 0
69+
70+
71+ @pytest .mark .asyncio
72+ async def test_handle_vim_atomic_write_delete_still_exists (watch_service , project_config , test_project , sync_service ):
73+ """Test vim atomic write scenario: DELETE event but file still exists on disk."""
74+ project_dir = project_config .home
75+
76+ # Create initial file and sync it
77+ test_file = project_dir / "vim_test.md"
78+ initial_content = """---
79+ type: note
80+ title: vim test
81+ ---
82+ # Vim Test
83+ Initial content for atomic write test
84+ """
85+ test_file .write_text (initial_content )
86+ await sync_service .sync (project_dir )
87+
88+ # Get initial entity state
89+ initial_entity = await sync_service .entity_repository .get_by_file_path ("vim_test.md" )
90+ assert initial_entity is not None
91+ initial_checksum = initial_entity .checksum
92+
93+ # Simulate vim's atomic write: modify content but send DELETE event
94+ # (vim moves original file, creates new content, then deletes old inode)
95+ modified_content = """---
96+ type: note
97+ title: vim test
98+ ---
99+ # Vim Test
100+ Modified content after atomic write
101+ """
102+ test_file .write_text (modified_content )
103+
104+ # Setup DELETE event even though file still exists (vim's atomic write behavior)
105+ # Use absolute path like the real watch service would
106+ changes = {(Change .deleted , str (test_file ))}
107+
108+ # Handle the change
109+ await watch_service .handle_changes (test_project , changes )
110+
111+ # Verify the entity still exists and was updated (not deleted)
112+ entity = await sync_service .entity_repository .get_by_file_path ("vim_test.md" )
113+ assert entity is not None
114+ assert entity .id == initial_entity .id # Same entity
115+ assert entity .checksum != initial_checksum # Checksum should be updated
116+
117+ # Verify the file content was properly synced
118+ actual_content = test_file .read_text ()
119+ assert "Modified content after atomic write" in actual_content
120+
121+ # Check that correct event was recorded (should be "modified", not "deleted")
122+ events = [e for e in watch_service .state .recent_events if e .path == "vim_test.md" ]
123+ assert len (events ) == 1
124+ assert events [0 ].action == "modified"
125+ assert events [0 ].status == "success"
126+
127+
128+ @pytest .mark .asyncio
129+ async def test_handle_true_deletion_vs_vim_atomic (watch_service , project_config , test_project , sync_service ):
130+ """Test that true deletions are still handled correctly vs vim atomic writes."""
131+ project_dir = project_config .home
132+
133+ # Create and sync two files
134+ atomic_file = project_dir / "atomic_test.md"
135+ delete_file = project_dir / "delete_test.md"
136+
137+ content = """---
138+ type: note
139+ ---
140+ # Test File
141+ Content for testing
142+ """
143+
144+ atomic_file .write_text (content )
145+ delete_file .write_text (content )
146+ await sync_service .sync (project_dir )
147+
148+ # For atomic_file: modify content but keep file (vim atomic write scenario)
149+ modified_content = content .replace ("Content for testing" , "Modified content" )
150+ atomic_file .write_text (modified_content )
151+
152+ # For delete_file: actually delete it (true deletion)
153+ delete_file .unlink ()
154+
155+ # Setup DELETE events for both files
156+ # Use absolute paths like the real watch service would
157+ changes = {
158+ (Change .deleted , str (atomic_file )), # File still exists - atomic write
159+ (Change .deleted , str (delete_file )), # File deleted - true deletion
160+ }
161+
162+ # Handle the changes
163+ await watch_service .handle_changes (test_project , changes )
164+
165+ # Verify atomic_file was treated as modification (still exists in DB)
166+ atomic_entity = await sync_service .entity_repository .get_by_file_path ("atomic_test.md" )
167+ assert atomic_entity is not None
168+
169+ # Verify delete_file was truly deleted (no longer exists in DB)
170+ delete_entity = await sync_service .entity_repository .get_by_file_path ("delete_test.md" )
171+ assert delete_entity is None
172+
173+ # Check events were recorded correctly
174+ events = watch_service .state .recent_events
175+ atomic_events = [e for e in events if e .path == "atomic_test.md" ]
176+ delete_events = [e for e in events if e .path == "delete_test.md" ]
177+
178+ assert len (atomic_events ) == 1
179+ assert atomic_events [0 ].action == "modified"
180+
181+ assert len (delete_events ) == 1
182+ assert delete_events [0 ].action == "deleted"
183+
184+
185+ @pytest .mark .asyncio
186+ async def test_handle_vim_atomic_write_markdown_with_relations (watch_service , project_config , test_project , sync_service ):
187+ """Test vim atomic write with markdown files that contain relations."""
188+ project_dir = project_config .home
189+
190+ # Create target file for relations
191+ target_file = project_dir / "target.md"
192+ target_content = """---
193+ type: note
194+ title: Target Note
195+ ---
196+ # Target Note
197+ This is the target of relations.
198+ """
199+ target_file .write_text (target_content )
200+
201+ # Create main file with relations
202+ main_file = project_dir / "main.md"
203+ initial_content = """---
204+ type: note
205+ title: Main Note
206+ ---
207+ # Main Note
208+ This note links to [[Target Note]].
209+
210+ - relates_to [[Target Note]]
211+ """
212+ main_file .write_text (initial_content )
213+ await sync_service .sync (project_dir )
214+
215+ # Get initial state
216+ main_entity = await sync_service .entity_repository .get_by_file_path ("main.md" )
217+ assert main_entity is not None
218+ initial_relations = len (main_entity .relations )
219+
220+ # Simulate vim atomic write with content change that adds more relations
221+ modified_content = """---
222+ type: note
223+ title: Main Note
224+ ---
225+ # Main Note
226+ This note links to [[Target Note]] multiple times.
227+
228+ - relates_to [[Target Note]]
229+ - references [[Target Note]]
230+ """
231+ main_file .write_text (modified_content )
232+
233+ # Setup DELETE event (vim atomic write)
234+ # Use absolute path like the real watch service would
235+ changes = {(Change .deleted , str (main_file ))}
236+
237+ # Handle the change
238+ await watch_service .handle_changes (test_project , changes )
239+
240+ # Verify entity still exists and relations were updated
241+ updated_entity = await sync_service .entity_repository .get_by_file_path ("main.md" )
242+ assert updated_entity is not None
243+ assert updated_entity .id == main_entity .id
244+
245+ # Verify relations were processed correctly
246+ updated_relations = len (updated_entity .relations )
247+ assert updated_relations >= initial_relations # Should have at least as many relations
248+
249+ # Check event was recorded as modification
250+ events = [e for e in watch_service .state .recent_events if e .path == "main.md" ]
251+ assert len (events ) == 1
252+ assert events [0 ].action == "modified"
253+
254+
255+ @pytest .mark .asyncio
256+ async def test_handle_vim_atomic_write_directory_path_ignored (watch_service , project_config , test_project ):
257+ """Test that directories are properly ignored even in atomic write detection."""
258+ project_dir = project_config .home
259+
260+ # Create directory
261+ test_dir = project_dir / "test_directory"
262+ test_dir .mkdir ()
263+
264+ # Setup DELETE event for directory (should be ignored)
265+ # Use absolute path like the real watch service would
266+ changes = {(Change .deleted , str (test_dir ))}
267+
268+ # Handle the change - should not cause errors
269+ await watch_service .handle_changes (test_project , changes )
270+
271+ # Verify no events were recorded for the directory
272+ events = [e for e in watch_service .state .recent_events if "test_directory" in e .path ]
273+ assert len (events ) == 0
0 commit comments