Files

137 lines
6.3 KiB
Python

import asyncio
from types import SimpleNamespace
import httpx
import pytest
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.gateway import assistant_file_ingest as ingest
from app.gateway.routers import assistant_knowledge as router
from deerflow.knowledge.schemas import NoteDraft
from deerflow.persistence.assistant_knowledge import AssistantKnowledgeRepository
from deerflow.persistence.base import Base
@pytest.mark.asyncio
async def test_new_base_exposes_an_empty_wiki_directory(monkeypatch):
"""A valid assistant base does not need a source before its Wiki route opens."""
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
store = AssistantKnowledgeRepository(async_sessionmaker(engine, expire_on_commit=False))
base = await store.create_base(created_by="admin", name="尚未上传文件的知识库")
async def actor(*args, **kwargs):
return "admin"
app = FastAPI()
app.include_router(router.router)
app.state.assistant_knowledge_store = store
monkeypatch.setattr(router, "_actor", actor)
try:
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test",
) as client:
response = await client.get(
"/api/assistant-knowledge/wiki-pages",
params={"base_id": base["id"], "include_content": "false"},
)
assert response.status_code == 200
assert response.json() == {"pages": [], "total": 0, "next_offset": None}
finally:
await engine.dispose()
def test_same_topic_across_document_parts_preserves_all_knowledge():
package = ingest.notes_package([
{"title": "设备维护", "content_md": "每周清理过滤网", "tags": ["运维"]},
{"title": "设备 维护", "content_md": "每月检查电机温度", "tags": ["设备"]},
], {"digest": "document", "filename": "维护指南.md"})
assert len(package["wiki_pages"]) == 1
assert "每周清理过滤网" in package["wiki_pages"][0]["content"]
assert "每月检查电机温度" in package["wiki_pages"][0]["content"]
assert package["wiki_pages"][0]["tags"] == ["运维", "设备"]
@pytest.mark.asyncio
async def test_standalone_upload_vectors_retry_and_dedup(tmp_path, monkeypatch):
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
store = AssistantKnowledgeRepository(async_sessionmaker(engine, expire_on_commit=False))
base = await store.create_base(name="独立文档库", created_by="admin")
app = FastAPI()
app.include_router(router.router)
app.state.assistant_knowledge_store = store
app.state.llmwiki_embedding = None
monkeypatch.setattr(ingest, "runtime_home", lambda: tmp_path)
extraction_calls = []
async def actor(*args, **kwargs):
return "admin"
async def extract(text, **kwargs):
extraction_calls.append(text)
return [NoteDraft(title="冷却系统", summary="摘要占位", content_md="# 冷却系统\n风扇的额定转速为每分钟 1200 转。", tags=["设备"], entities=[{"name": "风扇", "type": "product"}])]
class Encoder:
fingerprint = "local-test-3"
dimensions = 3
config = SimpleNamespace(model="offline-test")
async def embed_texts(self, texts):
return [[1.0, 0.0, 0.0] for _ in texts]
async def embed_query(self, text):
return [1.0, 0.0, 0.0]
monkeypatch.setattr(router, "_actor", actor)
monkeypatch.setattr(ingest, "extract_document_llm_multi", extract)
async def finish():
if ingest._tasks:
await asyncio.gather(*list(ingest._tasks.values()))
try:
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
payload = {"assistant_base_id": base["id"]}
response = await client.post("/api/assistant-knowledge/imports/files", data=payload, files={"file": ("../../设备.md", "风扇额定转速为 1200 转。".encode())})
assert response.status_code == 202, response.text
job_id = response.json()["id"]
await finish()
assert (await store.get_job(job_id))["status"] == "failed"
assert (ingest.file_job_dir(job_id) / "wiki.json").exists()
app.state.llmwiki_embedding = Encoder()
retry = await client.post(f"/api/assistant-knowledge/import-jobs/{job_id}/retry-file")
assert retry.status_code == 202
await finish()
job = await store.get_job(job_id)
assert job["status"] == "completed", job
assert job["counts"]["vectors"] == 1
assert len(extraction_calls) == 1
results = await store.search_pages(base_ids=[base["id"]], query="额定转速", embedding_client=app.state.llmwiki_embedding)
assert results
result = results[0]
assert result["retrieval_mode"] == "keyword"
assert result["retrieval_channels"] == ["vector", "keyword"]
assert set(result["channel_scores"]) == {"vector", "keyword"}
assert "1200" in result["matched_section"]["content"]
assert "摘要占位" not in result["matched_section"]["content"]
global_base = await store.get_base()
assert len(await store.list_pages(base_id=global_base["id"])) == 1
again = await client.post("/api/assistant-knowledge/imports/files", data=payload, files={"file": ("设备.md", "风扇额定转速为 1200 转。".encode())})
assert again.status_code == 202
await finish()
assert len(await store.list_sources(base_id=base["id"])) == 1
assert len(await store.list_pages(base_id=global_base["id"])) == 1
bad = await client.post("/api/assistant-knowledge/imports/files", data=payload, files={"file": ("script.py", b"print('no')")})
assert bad.status_code == 415
empty = await client.post("/api/assistant-knowledge/imports/files", data=payload, files={"file": ("empty.txt", b"")})
assert empty.status_code == 422
finally:
await ingest.stop_file_jobs()
await engine.dispose()