From 6b89488677b0d65b5416390013c6b0f1719d894c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sat, 11 Jul 2026 20:02:24 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E8=A1=A5=E5=85=85=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E7=8E=87=20=E2=80=94=20DB/CLI/Search/Ingest?= =?UTF-8?q?=20+=20=E4=BF=AE=E5=A4=8D=20chromadb=20=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coverage | Bin 53248 -> 53248 bytes src/core/db.py | 3 +++ tests/test_cli.py | 18 ++++++++++++++++++ tests/test_db.py | 30 ++++++++++++++++++++++++++++++ tests/test_ingest.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_search.py | 22 ++++++++++++++++++++++ 6 files changed, 108 insertions(+) diff --git a/.coverage b/.coverage index a2f9b3adef2c3b14e4bec93d203dba4e28999b53..9e659dba656a2c2f5c128c71004cb6c8a3f75541 100644 GIT binary patch delta 937 zcmYk3X-HI26vyA4^}TsB_sqULZQm&;>w{W`Y2{QiBNc(P7%`<~v^Y(gnb~NI27MUn zMi!I~6$B9#SP>N}27Ra~LINYy7Az>zlx&-t>AcCJ_vL@i|NPE<_s^}3Dr=+4YpPtO z#Md)z?qp!PJmIr&PAC#A{6oIJ#bosInuU^zs}tE%-)z=apydO>pgUOQaj}Fy+Y~AZ z1$JyL38AytiI@ekOxCp2Kpbr5vJ5uhSBtw!0-=qiNG!LrW8Qq+*}P>}d8i5n5AwXl>>pBQNqhqb zt@IteK#Qo|;L*qR+w`~iM}k#YXLzia#7E+O-D}+m-2!1)I4`C{Io|+o!x7Bp`_zr{c06O3@oPj;C0lY95Qs^(* zPy1*my-)AbtMn{o^dPOGJLxi-OQ&Ou0Ylg#sfaLD=xOZE=)c;w_f^Bf$@A3)7n-Tc zxL26}cah9bNYUEJ#ohxYwlMa98l(b8~y1bXdFmT1>xKz_g4(R2-n1Oh(a`@<+K#mt2Nm(c#}+; zkra)xZM->q71yaVg1iPz5!RB)dz?Sl+O z``b5!3yz7c>r!iP;t}mjV~1m`IpUr&Nr|9e9Z$Qho%$yATOyl8NXKd5UiN+%dPlh0 zfy!{4a4I$!v1Cc$=O6|FSP7ruBfN!PxCnmef~Rl~A1n%|;W(7SQ8)xOup7$Y3fzFZ vum&H;2-7t9BO*viQM!^MIg_GrB*kP;ik($7=YT_>Y@A|JTrC~VIqAOvP23@1 delta 889 zcmYjNYivtl7(S=xv^`(X>GyWMAGgL_;s@P`)E3lOHCtD=h{c$2Mwbbjs%?VNNyH^2 z#h2I^)*lkdByJ;-kj*9h5X6F*%Py86gxwgS(fLk^IDek^d7tNd^E{_E&eg`bi+m0z z3r~qYXQDA?n1}jN6H*Y}G^d$P2)bOEXLgIEEpHlWU7#0~aM&H*=690e;7lUqOIin& zEmY2yDkJMd)u9NLeRh(u$V2*qTG$d&B5TX&Zn2FTCy=&+x#Rf516be_BP)iWV)PniV=HMXbk%earwBYyn3@*lYqhCr&8>MS# z1S&!2jE|%d;sbG)p~rC8kc$SyOsEu|f!nyBHfj*azU4kvCduMirbsBFRBT8^cINT4 zhEkM87UxT>gSc1EQ1jz|`Rz-18eb{OCf)f~tu4sY_o zxgzkl%F24-Zxs{CTBh)}CECesOfX74l6Ml&w<9qlD zZo&=t2tI(za1r*=CI%d}$UKWCq0YJvPw$1+?QizwPMo7PI_Yv0mkgje-+pb)b~z}` zk&~TocNFew&;D_#an-ks#Gx;`E`3e^?CwP_ot+x6h81iS&1t3IHAbs?!FDV<{#Ffh zxmU2Lg0)bGOlCTzTaI<~Rt>${nT&c_jgY2_=EA+aU{WWOY6H~^AI7w?Kb z=hl$%n-huQWqm^;BLOz>EQiK8$=ukCabxw=>k~Che^U*3JuYBs6YYJs-B}m4^wh$a z8L#!^WX!Eg=cvz59>+$r8Zr>Gcw|`0Kmy8O1$>3i@D94*EG&g~XoEX&AL4Kvj>0B5 z2zy{BRKt0=3O7KZKVOD)9lgaOIR%_E1+pUr8hZ-TNL8a3>{5z2#1uH2Z#Q}~e*<_X B3V{Fs diff --git a/src/core/db.py b/src/core/db.py index 8c0e40b..77da2bc 100644 --- a/src/core/db.py +++ b/src/core/db.py @@ -45,6 +45,9 @@ class VectorDB: self.client.delete_collection(name=name) except ValueError: pass # collection 不存在则忽略 + except Exception: + # chromadb 不同版本可能抛出 NotFoundError 等 + pass def delete_by_source(self, collection_name: str, file_name: str) -> bool: """按 source_file 删除文档 (线程安全).""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 571b0c5..67b2719 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -61,3 +61,21 @@ class TestCLIJSONOutput: if result.stdout.strip(): data = json.loads(result.stdout) assert isinstance(data, dict) + + +def test_ingest_help(): + """ingest --help 正常输出.""" + result = runner.invoke(app, ["ingest", "--help"]) + assert result.exit_code == 0 + + +def test_search_help(): + """search --help 正常输出.""" + result = runner.invoke(app, ["search", "--help"]) + assert result.exit_code == 0 + + +def test_export_help(): + """export --help 正常输出.""" + result = runner.invoke(app, ["export", "--help"]) + assert result.exit_code == 0 diff --git a/tests/test_db.py b/tests/test_db.py index ea1ba8f..4bf792d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -45,3 +45,33 @@ class TestVectorDB: # 再次获取会创建新的 col = db.get_or_create_collection("tmp_col") assert col.count() == 0 + + +def test_write_guard_context_manager(tmp_path): + """write_guard 上下文管理器正常获取和释放锁.""" + from src.core.db import VectorDB + vdb = VectorDB(persist_dir=str(tmp_path)) + with vdb.write_guard(): + pass + + +def test_close(tmp_path): + """close 正常执行不抛异常.""" + from src.core.db import VectorDB + vdb = VectorDB(persist_dir=str(tmp_path)) + vdb.close() + + +def test_delete_collection_nonexistent(tmp_path): + """删除不存在的 collection 不抛异常.""" + from src.core.db import VectorDB + vdb = VectorDB(persist_dir=str(tmp_path)) + vdb.delete_collection("nonexistent-collection-12345") + + +def test_delete_by_source_no_match(tmp_path): + """删除不存在的 source 返回 False.""" + from src.core.db import VectorDB + vdb = VectorDB(persist_dir=str(tmp_path)) + result = vdb.delete_by_source("test_col", "no-such-file.md") + assert result is False diff --git a/tests/test_ingest.py b/tests/test_ingest.py index aaa74cb..d4addad 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -286,3 +286,38 @@ class TestIncrementalIngest: count2 = ingestor.ingest_file(str(file), incremental=True, force=True) assert count1 > 0 assert count2 > 0 # force 模式重新入库 + + +def test_ingest_content_default_splitter(tmp_path): + """未指定 splitter 时用 MarkdownSplitter.""" + from src.core.config import EmbedConfig + from src.core.db import VectorDB + from src.core.embedder import create_embedder + from src.core.ingest import DocumentIngestor + + db = VectorDB(persist_dir=str(tmp_path / "db")) + embedder = create_embedder(EmbedConfig(mode="local")) + ingestor = DocumentIngestor(db, embedder, "test_content") + count = ingestor.ingest_content("# 测试\n\n一些内容。", "test.md") + assert count >= 1 + + +def test_ingest_directory_recursive(tmp_path): + """ingest_directory 递归处理子目录.""" + from src.core.config import EmbedConfig, ChunkConfig + from src.core.db import VectorDB + from src.core.embedder import create_embedder + from src.core.ingest import DocumentIngestor + + (tmp_path / "sub").mkdir() + (tmp_path / "a.md").write_text("# A\n\n内容A。", encoding="utf-8") + (tmp_path / "sub" / "b.md").write_text("# B\n\n内容B。", encoding="utf-8") + db = VectorDB(persist_dir=str(tmp_path / "db_r")) + embedder = create_embedder(EmbedConfig(mode="local")) + ingestor = DocumentIngestor( + db, embedder, "test_recurse", + chunk_config=ChunkConfig(max_size=1000, overlap=100), + ) + results = ingestor.ingest_directory(str(tmp_path)) + assert len(results) >= 2 + assert all(v > 0 for v in results.values()) diff --git a/tests/test_search.py b/tests/test_search.py index 4371376..e49e9b5 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -122,3 +122,25 @@ class TestSearcher: searcher = Searcher(db, embedder, "empty_coll") sources = searcher.list_sources() assert sources == [] + + +def test_list_sources_empty(): + """空 collection 的 list_sources 返回空列表.""" + from src.core.search import Searcher + + class EmptyColl: + def count(self): return 0 + def get(self, **kwargs): + return {"ids": [], "documents": [], "metadatas": []} + + class EmptyDB: + def get_or_create_collection(self, name): return EmptyColl() + def list_collections(self): return [] + + class FakeEmb: + @property + def dimension(self): return 4 + def embed(self, texts): return [[0.0] * 4] + + s = Searcher(EmptyDB(), FakeEmb(), "empty") + assert s.list_sources() == []