@@ -221,6 +221,31 @@ def test_add_directory_calls_helper_for_each_file(self, tmp_path):
221221 assert "b.txt" in called_names
222222 assert "ignore.xyz" not in called_names
223223
224+ def test_add_directory_stops_after_dirty_rollback (self , tmp_path ):
225+ import pytest
226+
227+ from openkb .add_coordinator import DirtyRollbackError
228+
229+ kb_dir = self ._setup_kb (tmp_path )
230+ docs_dir = tmp_path / "docs"
231+ docs_dir .mkdir ()
232+ (docs_dir / "a.md" ).write_text ("# A" )
233+ (docs_dir / "b.md" ).write_text ("# B" )
234+ dirty_error = DirtyRollbackError (
235+ "add" ,
236+ kb_dir / ".openkb" / "journal" / "retained.json" ,
237+ )
238+
239+ runner = CliRunner ()
240+ with patch ("openkb.cli.add_single_file" , side_effect = dirty_error ) as mock_add , \
241+ patch ("openkb.cli._find_kb_dir" , return_value = kb_dir ):
242+ with pytest .raises (DirtyRollbackError ) as exc_info :
243+ runner .invoke (cli , ["add" , str (docs_dir )], catch_exceptions = False )
244+
245+ assert exc_info .value is dirty_error
246+ mock_add .assert_called_once ()
247+ assert mock_add .call_args .args [0 ].name == "a.md"
248+
224249 def test_add_unsupported_extension (self , tmp_path ):
225250 kb_dir = self ._setup_kb (tmp_path )
226251 doc = tmp_path / "file.xyz"
@@ -525,6 +550,52 @@ def race_before_lock(openkb_dir):
525550 assert registry .get (synthetic )["doc_name" ] == expected_doc_name
526551 assert (kb_dir / "wiki" / "sources" / f"{ expected_doc_name } .json" ).exists ()
527552
553+ def test_cloud_import_skips_if_same_doc_id_registered_after_fetch (self , tmp_path ):
554+ import hashlib
555+ from contextlib import contextmanager
556+
557+ from openkb .cli import import_from_pageindex_cloud
558+ from openkb .locks import kb_ingest_lock as real_kb_ingest_lock
559+ from openkb .state import HashRegistry
560+
561+ kb_dir = self ._setup_kb (tmp_path )
562+ cloud = self ._cloud_data (doc_name = "Cloud-Paper" )
563+ path_key = "pageindex-cloud:cloud-1"
564+ synthetic = hashlib .sha256 (path_key .encode ("utf-8" )).hexdigest ()
565+ lock_calls = {"count" : 0 }
566+
567+ @contextmanager
568+ def register_same_doc_before_second_lock (openkb_dir ):
569+ lock_calls ["count" ] += 1
570+ if lock_calls ["count" ] == 2 :
571+ HashRegistry (openkb_dir / "hashes.json" ).add (
572+ synthetic ,
573+ {
574+ "name" : "Cloud Paper.pdf" ,
575+ "doc_name" : "Cloud-Paper" ,
576+ "type" : "pageindex_cloud" ,
577+ "origin" : "cloud" ,
578+ "path" : path_key ,
579+ "source_path" : "wiki/sources/Cloud-Paper.json" ,
580+ "doc_id" : "cloud-1" ,
581+ },
582+ )
583+ with real_kb_ingest_lock (openkb_dir ):
584+ yield
585+
586+ with patch ("openkb.cli.prepare_cloud_import" , return_value = cloud ) as mock_prepare , \
587+ patch ("openkb.cli.kb_ingest_lock" , side_effect = register_same_doc_before_second_lock ), \
588+ patch ("openkb.cli.compile_long_doc" ) as mock_compile , \
589+ patch ("openkb.add_coordinator.run_add_mutation" ) as mock_run , \
590+ patch ("openkb.cli._setup_llm_key" ):
591+ assert import_from_pageindex_cloud ("cloud-1" , kb_dir ) == "skipped"
592+
593+ assert lock_calls ["count" ] == 2
594+ mock_prepare .assert_called_once ()
595+ mock_compile .assert_not_called ()
596+ mock_run .assert_not_called ()
597+ assert HashRegistry (kb_dir / ".openkb" / "hashes.json" ).is_known (synthetic )
598+
528599 def test_cloud_import_propagates_dirty_rollback (self , tmp_path ):
529600 import pytest
530601
0 commit comments