Files

1904 lines
64 KiB
Python

from __future__ import annotations
import asyncio
import json
import threading
from types import SimpleNamespace
import pytest
from fastapi import FastAPI, HTTPException
from langchain_core.messages import HumanMessage
from starlette.requests import Request
from app.gateway.services import (
_with_knowledge_retrieval_progress_stream,
enforce_knowledge_retrieval_mode_run_config,
resolve_knowledge_retrieval_mode,
)
from deerflow.integrations.weknora.client import WeKnoraClient, WeKnoraError
from deerflow.tools.builtins import llmwiki_reasoning_tools as tools
class _SettingsStore:
def __init__(self, enabled: bool) -> None:
self.enabled = enabled
async def get(self) -> bool:
return self.enabled
class _MappingStore:
async def get_authorized(
self,
mapping_id: str,
user_id: str,
*,
write: bool,
is_admin: bool,
) -> dict | None:
assert user_id == "user-1"
assert write is False
if mapping_id != "mapping-1":
return None
return {
"id": "mapping-1",
"weknora_id": "remote-1",
"name": "制度库",
}
class _Client:
async def search(self, query: str, knowledge_base_ids: list[str]) -> list[dict]:
assert knowledge_base_ids == ["remote-1"]
return [
{
"chunk_id": "chunk-1",
"knowledge_id": "doc-1",
"knowledge_base_id": "remote-1",
"title": "采购制度",
"content": "采购金额超过十万元时需要集体审议。",
"score": 0.8 if query == "采购金额" else 0.9,
}
]
async def list_documents(self, knowledge_base_id: str, *, page: int, page_size: int) -> dict:
assert knowledge_base_id == "remote-1"
return {
"items": [{"id": "doc-1", "title": "采购制度", "updated_at": "2026-01-01"}],
"total": 1,
}
async def list_chunks(self, knowledge_id: str, *, page: int, page_size: int) -> dict:
assert knowledge_id == "doc-1"
return {
"items": [
{"id": "chunk-1", "content": "条款 A-17:采购金额超过十万元。", "chunk_index": 2},
{"id": "chunk-2", "content": "普通说明。", "chunk_index": 3},
],
"total": 2,
}
async def grep_chunks(self, query: str, knowledge_base_ids: list[str]) -> dict:
assert query == r"A-17|十万元"
assert knowledge_base_ids == ["remote-1"]
return {
"items": [
{
"chunk_id": "chunk-1",
"knowledge_id": "doc-1",
"knowledge_base_id": "remote-1",
"title": "采购制度",
"content": "条款 A-17:采购金额超过十万元。",
"match_count": 2,
"chunk_index": 2,
}
],
"contract": "weknora-v0.7.2-grep-v1",
"compatibility_fallback": False,
"partial": False,
"warnings": [],
"successful_knowledge_base_ids": ["remote-1"],
"failed_knowledge_base_ids": [],
"scanned_chunks": 2,
"scan_truncated": False,
}
def _request(store: _SettingsStore | None) -> Request:
app = FastAPI()
if store is not None:
app.state.knowledge_retrieval_settings_store = store
return Request(
{
"type": "http",
"method": "POST",
"path": "/api/threads/t/runs",
"headers": [],
"query_string": b"",
"app": app,
}
)
@pytest.mark.asyncio
async def test_database_gate_defaults_classic_and_rejects_disabled_smart_mode() -> None:
assert await resolve_knowledge_retrieval_mode(
_request(None),
SimpleNamespace(context={}),
) == "classic"
with pytest.raises(HTTPException) as caught:
await resolve_knowledge_retrieval_mode(
_request(_SettingsStore(False)),
SimpleNamespace(context={"knowledge_retrieval_mode": "smart_reasoning"}),
)
assert caught.value.status_code == 409
assert caught.value.detail["code"] == "SMART_REASONING_DISABLED"
assert await resolve_knowledge_retrieval_mode(
_request(_SettingsStore(True)),
SimpleNamespace(context={"knowledge_retrieval_mode": "smart_reasoning"}),
) == "smart_reasoning"
def test_validated_mode_overwrites_untrusted_preseeded_config() -> None:
config = {
"configurable": {
"knowledge_retrieval_mode": "smart_reasoning",
"disable_tools": True,
"excluded_tools": ["ordinary_wiki_search"],
},
"context": {
"knowledge_retrieval_mode": "smart_reasoning",
"disable_tools": True,
"excluded_tools": ["ordinary_knowledge_search"],
},
"metadata": {"rag_mode_enabled": True, "rag_mode": True},
}
enforce_knowledge_retrieval_mode_run_config(config, "classic")
assert config["configurable"]["knowledge_retrieval_mode"] == "classic"
assert config["context"]["knowledge_retrieval_mode"] == "classic"
enforce_knowledge_retrieval_mode_run_config(config, "smart_reasoning")
assert config["configurable"]["rag_mode_enabled"] is False
assert config["context"]["rag_mode_enabled"] is False
assert config["configurable"]["knowledge_rag_enabled"] is False
assert config["context"]["knowledge_rag_enabled"] is False
assert config["metadata"]["rag_mode_enabled"] is False
assert config["metadata"]["rag_mode"] is False
assert config["configurable"]["disable_tools"] is False
assert config["context"]["disable_tools"] is False
assert set(config["configurable"]["excluded_tools"]) == set(
tools.CLASSIC_LLMWIKI_TOOL_NAMES
)
assert set(config["context"]["excluded_tools"]) == set(
tools.CLASSIC_LLMWIKI_TOOL_NAMES
)
def test_smart_mode_always_enables_custom_progress_stream() -> None:
assert _with_knowledge_retrieval_progress_stream(
["values", "messages-tuple"],
"smart_reasoning",
) == ["values", "messages-tuple", "custom"]
assert _with_knowledge_retrieval_progress_stream(
["values", "custom", "custom"],
"smart_reasoning",
) == ["values", "custom"]
assert _with_knowledge_retrieval_progress_stream(
["values"],
"classic",
) == ["values"]
def test_numbered_citation_prompt_is_classic_only() -> None:
from app.gateway import services
graph_input = {"messages": [HumanMessage(content="查制度")]}
smart = services._apply_classic_rag_citation_prompt(
graph_input,
enabled=True,
knowledge_retrieval_mode="smart_reasoning",
)
smart_extra = smart["messages"][0].additional_kwargs
assert services._RAG_CITATION_PROMPT not in str(
smart_extra.get("prompt_prefix") or ""
)
classic = services._apply_classic_rag_citation_prompt(
graph_input,
enabled=True,
knowledge_retrieval_mode="classic",
)
assert services._RAG_CITATION_PROMPT in str(
classic["messages"][0].additional_kwargs.get("prompt_prefix") or ""
)
@pytest.mark.asyncio
@pytest.mark.parametrize("assistant_id", [None, "deep-research-planner", "deep-research-collector"])
async def test_smart_scope_binds_this_library_to_authorized_database_metadata(
monkeypatch: pytest.MonkeyPatch, assistant_id: str | None,
) -> None:
from app.gateway import llmwiki_rag
class ScopeStore:
async def get_authorized(
self,
mapping_id: str,
user_id: str,
*,
write: bool,
is_admin: bool,
) -> dict | None:
assert (mapping_id, user_id, write, is_admin) == (
"mapping-news",
"user-1",
False,
False,
)
return {
"id": mapping_id,
"weknora_id": "remote-news",
"name": "国内新闻资料库",
"description": "收录国内重要新闻和政策发布。",
"wiki_focus_instructions": "突出时间、地点和关键数据",
"wiki_directory_paths": [["年度", "重大活动"], ["政策", "发布会"]],
}
app = FastAPI()
app.state.config = SimpleNamespace()
app.state.llmwiki_store = ScopeStore()
request = Request(
{
"type": "http",
"method": "POST",
"path": "/api/threads/t/runs",
"headers": [],
"query_string": b"",
"app": app,
}
)
monkeypatch.setattr(
llmwiki_rag,
"get_resolved_llmwiki_runtime",
lambda _config: SimpleNamespace(weknora_enabled=True),
)
async def actor(_request: Request) -> tuple[str, bool]:
return "user-1", False
monkeypatch.setattr(llmwiki_rag, "_actor", actor)
rag = await llmwiki_rag.build_llmwiki_smart_reasoning_context(
request=request,
body=SimpleNamespace(
context={"llmwiki_knowledge_base_ids": ["mapping-news"]},
metadata={},
assistant_id=assistant_id,
),
graph_input={"messages": [HumanMessage(content="这个库讲了什么")]},
)
assert rag.knowledge_base_context == [
{
"id": "mapping-news",
"name": "国内新闻资料库",
"description": "收录国内重要新闻和政策发布。",
"focus": "突出时间、地点和关键数据",
"directory_hints": ["年度 / 重大活动", "政策 / 发布会"],
}
]
assert "这个库" in rag.prompt
assert "国内新闻资料库" in rag.prompt
assert "ordinary_knowledge_search" in rag.prompt
assert "/mnt/knowledge" in rag.prompt
assert "不要追问" in rag.prompt
attached = llmwiki_rag.attach_llmwiki_rag_to_graph_input(
{"messages": [HumanMessage(content="这个库讲了什么")]},
rag,
)
extra = attached["messages"][0].additional_kwargs
assert extra["llmwiki_knowledge_base_context"] == rag.knowledge_base_context
assert "这个库" in extra["smart_knowledge_scope_prompt"]
assert "prompt_prefix" not in extra
def test_smart_scope_keeps_all_identities_under_one_global_prompt_budget() -> None:
from app.gateway import llmwiki_rag
rows = [
{
"id": f"mapping-{index:03d}",
"name": f"第{index:03d}个知识库" + ("长名称" * 40),
"description": "详细简介" * 300,
"wiki_focus_instructions": "抽取重点" * 300,
"wiki_directory_paths": [
[f"一级目录{index}", "二级目录" * 80]
for _ in range(12)
],
}
for index in range(100)
]
contexts = llmwiki_rag._smart_knowledge_base_context(rows)
serialized = json.dumps(contexts, ensure_ascii=False, separators=(",", ":"))
prompt = llmwiki_rag._build_smart_scope_prompt(contexts)
assert len(contexts) == 100
assert [item["id"] for item in contexts] == [
f"mapping-{index:03d}" for index in range(100)
]
assert all(item["name"] for item in contexts)
assert len(serialized) <= llmwiki_rag._MAX_SCOPE_CONTEXT_JSON_CHARS
assert len(prompt) <= llmwiki_rag._MAX_SMART_SCOPE_PROMPT_CHARS
assert any(item.get("description") for item in contexts)
assert any("description" not in item for item in contexts)
def test_smart_metadata_disables_reference_mode_while_classic_keeps_it() -> None:
from app.gateway import services
smart_run = {"rag_mode_enabled": True, "rag_mode": True}
smart_record = {"rag_mode_enabled": True, "rag_mode": True}
smart_thread = {"rag_mode_enabled": True, "rag_mode": True}
smart = services._sync_retrieval_rag_metadata(
run_metadata=smart_run,
record_metadata=smart_record,
thread_metadata_update=smart_thread,
knowledge_retrieval_mode="smart_reasoning",
knowledge_scope_active=True,
selection_declared=True,
explicit_rag_requested=True,
)
assert smart == {"rag_mode_enabled": False, "rag_mode": False}
assert smart_run == smart
assert smart_record == smart
assert smart_thread == smart
classic_run: dict = {}
classic_record: dict = {}
classic_thread: dict = {}
classic = services._sync_retrieval_rag_metadata(
run_metadata=classic_run,
record_metadata=classic_record,
thread_metadata_update=classic_thread,
knowledge_retrieval_mode="classic",
knowledge_scope_active=True,
selection_declared=True,
explicit_rag_requested=False,
)
assert classic == {"rag_mode_enabled": True, "rag_mode": True}
assert classic_run == classic
assert classic_record == classic
assert classic_thread == classic
@pytest.mark.asyncio
async def test_smart_run_stops_before_launch_when_mode_metadata_cannot_persist(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from app.gateway import analysis_projects, services
from app.gateway.llmwiki_rag import LlmWikiRagContext
class ThreadStore:
def __init__(self) -> None:
self.statuses: list[str] = []
self.metadata_writes = 0
async def get(self, _thread_id: str) -> dict:
return {"metadata": {}, "assistant_id": None}
async def update_status(self, _thread_id: str, status: str) -> None:
self.statuses.append(status)
async def update_metadata(self, _thread_id: str, _metadata: dict) -> None:
self.metadata_writes += 1
raise RuntimeError("database unavailable")
record = SimpleNamespace(run_id="run-1", metadata={}, task=None)
class RunManager:
def __init__(self) -> None:
self.terminal_status: object | None = None
async def create_or_reject(self, *_args, **kwargs):
record.metadata = dict(kwargs["metadata"])
return record
async def set_status(self, _run_id, status, **_kwargs) -> None:
self.terminal_status = status
thread_store = ThreadStore()
run_manager = RunManager()
run_context = SimpleNamespace(app_config=object(), thread_store=thread_store)
async def prepare_project_run(body, _thread_id, _request):
return body, None
async def unexpected_fixed_answer_lookup(**_kwargs):
raise AssertionError("smart retrieval must bypass fixed answers")
async def smart_context(**_kwargs):
return LlmWikiRagContext(["kb-1"], [], 0, "", [], "")
launched = False
async def launch(**_kwargs):
nonlocal launched
launched = True
monkeypatch.setattr(analysis_projects, "prepare_project_run", prepare_project_run)
monkeypatch.setattr(services, "get_stream_bridge", lambda _request: object())
monkeypatch.setattr(services, "get_run_manager", lambda _request: run_manager)
monkeypatch.setattr(services, "get_run_context", lambda _request: run_context)
monkeypatch.setattr(services, "get_concurrency_gate", lambda _request: None)
monkeypatch.setattr(services, "get_effective_user_id", lambda: "user-1")
monkeypatch.setattr(
services,
"_find_fixed_answer",
unexpected_fixed_answer_lookup,
)
monkeypatch.setattr(services, "_resolve_effective_model_name", lambda *_args, **_kwargs: "model-1")
monkeypatch.setattr(services, "_get_model_concurrency_limit", lambda _name: None)
monkeypatch.setattr(services, "_get_user_model_concurrency_limit", lambda _name: None)
monkeypatch.setattr(services, "resolve_agent_factory", lambda _assistant_id: object())
monkeypatch.setattr(services, "build_llmwiki_smart_reasoning_context", smart_context)
monkeypatch.setattr(services, "_run_agent_with_concurrency_lease", launch)
body = SimpleNamespace(
input={"messages": [{"role": "user", "content": "查制度"}]},
config={},
context={"knowledge_retrieval_mode": "smart_reasoning"},
metadata={},
assistant_id=None,
on_disconnect="continue",
multitask_strategy="reject",
)
request = SimpleNamespace(
app=SimpleNamespace(
state=SimpleNamespace(
knowledge_retrieval_settings_store=_SettingsStore(True),
)
),
client=None,
headers={},
)
with pytest.raises(RuntimeError, match="database unavailable"):
await services.start_run(body, "thread-1", request, enforce_access=False)
assert thread_store.metadata_writes == 1
assert thread_store.statuses == ["running", "error"]
assert str(run_manager.terminal_status).endswith("error")
assert launched is False
@pytest.mark.asyncio
async def test_multi_query_search_merges_same_chunk_and_preserves_query_evidence(monkeypatch) -> None:
monkeypatch.setattr(tools, "get_llmwiki_store", lambda: _MappingStore())
monkeypatch.setattr(tools, "get_effective_user_id", lambda: "user-1")
monkeypatch.setattr(tools, "get_current_user", lambda: SimpleNamespace(system_role="user"))
monkeypatch.setattr(tools, "_client", lambda: _Client())
payload = json.loads(
await tools._knowledge_search_impl(
["采购金额", "十万元审议"],
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["ok"] is True
assert payload["result_count"] == 1
assert payload["results"][0]["score"] == pytest.approx(0.9)
assert payload["results"][0]["matched_queries"] == ["采购金额", "十万元审议"]
assert payload["results"][0]["knowledge_base_id"] == "mapping-1"
@pytest.mark.asyncio
async def test_vector_tool_streams_public_query_and_aggregate_result_progress(
monkeypatch,
) -> None:
events: list[dict] = []
context = {
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
}
runtime = SimpleNamespace(
context=context,
stream_writer=events.append,
tool_call_id="call-vector-1",
)
async def fake_search(queries, received_context):
assert queries == ["国内新闻", "政策发布"]
assert received_context is context
return tools._result(
operation="knowledge_search",
results=[
{
"chunk_id": "chunk-secret",
"knowledge_id": "doc-secret",
"knowledge_base_id": "mapping-1",
"knowledge_base": "国内新闻库",
"title": "新闻原文",
"content": "抽屉中显示的安全新闻正文预览",
}
],
queries=queries,
partial=True,
warnings=[{"code": "PROVIDER_QUERY_FAILED", "query_index": 2}],
)
monkeypatch.setattr(tools, "_knowledge_search_impl", fake_search)
clock = iter([10.0, 10.456])
monkeypatch.setattr(tools.time, "perf_counter", lambda: next(clock))
output = await tools.ordinary_knowledge_search_tool.coroutine(
queries=["国内新闻", "政策发布"],
runtime=runtime,
)
output_payload = json.loads(output)
assert output_payload["result_count"] == 1
assert output_payload["elapsed_ms"] == 456
assert [(event["stage"], event["status"]) for event in events] == [
("query_rewrite", "completed"),
("vector_search", "started"),
("vector_search", "completed"),
]
assert all(
event["type"] == "knowledge_retrieval_progress"
and event["version"] == 1
and event["retrieval_id"] == "call-vector-1"
for event in events
)
assert events[0]["queries"] == ["国内新闻", "政策发布"]
assert events[1]["knowledge_base_count"] == 2
assert events[-1]["result_count"] == 1
assert events[-1]["partial"] is True
assert events[-1]["elapsed_ms"] == 456
assert events[-1]["warning_codes"] == ["PROVIDER_QUERY_FAILED"]
assert events[-1]["result_items"] == [
{
"kind": "document_chunk",
"knowledge_base_id": "mapping-1",
"knowledge_base": "国内新闻库",
"title": "新闻原文",
"chunk_id": "chunk-secret",
"knowledge_id": "doc-secret",
"content": "抽屉中显示的安全新闻正文预览",
"content_truncated": False,
}
]
assert events[-1]["result_items_truncated"] is False
@pytest.mark.asyncio
async def test_wiki_tool_streams_channel_coverage_and_scan_count(monkeypatch) -> None:
events: list[dict] = []
context = {
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
}
runtime = SimpleNamespace(
context=context,
stream_writer=events.append,
tool_call_id="call-wiki-1",
)
async def fake_wiki(pattern, received_context):
assert pattern == "新闻|发布会"
assert received_context is context
return tools._result(
operation="wiki_search",
results=[
{
"page_ref": tools._page_ref("mapping-1", "news/release"),
"knowledge_base_id": "mapping-1",
"knowledge_base": "国内新闻库",
"title": "发布会",
"slug": "news/release",
"summary": "可在抽屉中展示的 Wiki 摘要",
}
],
pattern=pattern,
channel="provider_native_regex",
partial=False,
native_regex_supported=True,
unsupported_knowledge_base_count=0,
coverage=[
{
"knowledge_base": "国内新闻库",
"channel": "provider_native_regex",
"status": "searched",
}
],
scanned_pages=17,
)
monkeypatch.setattr(tools, "_wiki_search_impl", fake_wiki)
await tools.ordinary_wiki_search_tool.coroutine(
pattern="新闻|发布会",
runtime=runtime,
)
assert [(event["stage"], event["status"]) for event in events] == [
("query_rewrite", "completed"),
("wiki_regex_search", "started"),
("wiki_regex_search", "completed"),
]
completed = events[-1]
assert completed["pattern"] == "新闻|发布会"
assert completed["channel"] == "provider_native_regex"
assert completed["scanned_pages"] == 17
assert completed["result_count"] == 1
assert completed["native_regex_supported"] is True
assert completed["unsupported_knowledge_base_count"] == 0
assert completed["coverage"] == [
{
"knowledge_base": "国内新闻库",
"channel": "provider_native_regex",
"status": "searched",
}
]
assert completed["result_items"] == [
{
"kind": "wiki_page",
"knowledge_base_id": "mapping-1",
"knowledge_base": "国内新闻库",
"title": "发布会",
"page_ref": tools._page_ref("mapping-1", "news/release"),
"slug": "news/release",
"summary": "可在抽屉中展示的 Wiki 摘要",
}
]
def test_public_result_items_are_scope_filtered_and_bounded() -> None:
payload = {
"operation": "knowledge_search",
"results": [
{
"knowledge_base_id": "mapping-1",
"provider_knowledge_base_id": "remote-must-not-leak",
"knowledge_base": "制度库",
"title": f"文档 {index}",
"chunk_id": f"chunk-{index}",
"knowledge_id": f"doc-{index}",
"content": "甲" * 2_000,
}
for index in range(22)
]
+ [
{
"knowledge_base_id": "mapping-foreign",
"title": "越权结果",
"chunk_id": "foreign-chunk",
"content": "不得进入事件",
}
],
}
items, truncated = tools._public_result_items(
payload,
authorized_mapping_ids={"mapping-1"},
)
assert len(items) == tools._MAX_PROGRESS_RESULT_ITEMS
assert truncated is True
assert all(item["knowledge_base_id"] == "mapping-1" for item in items)
assert all(len(item["content"]) <= tools._MAX_PROGRESS_RAW_CONTENT_CHARS for item in items)
rendered = json.dumps(items, ensure_ascii=False)
assert "remote-must-not-leak" not in rendered
assert "mapping-foreign" not in rendered
assert "foreign-chunk" not in rendered
def test_oversized_nested_tool_metadata_still_returns_valid_json() -> None:
output = tools._result(
operation="wiki_browse_index",
results=[{"title": "入口"}],
groups=[{"type": "summary", "items": [{"summary": "甲" * 60_000}]}],
)
payload = json.loads(output)
assert payload == {
"ok": True,
"operation": "wiki_browse_index",
"result_count": 0,
"results": [],
"output_truncated": True,
}
@pytest.mark.asyncio
async def test_progress_failure_hides_raw_exception_and_keeps_tool_contract(
monkeypatch,
) -> None:
events: list[dict] = []
runtime = SimpleNamespace(
context={
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
stream_writer=events.append,
tool_call_id="call-failed-1",
)
async def failed_search(_queries, _context):
raise RuntimeError("provider-token-must-never-leak")
monkeypatch.setattr(tools, "_knowledge_search_impl", failed_search)
clock = iter([20.0, 20.125])
monkeypatch.setattr(tools.time, "perf_counter", lambda: next(clock))
output = await tools.ordinary_knowledge_search_tool.coroutine(
queries=["国内新闻"],
runtime=runtime,
)
payload = json.loads(output)
assert payload["ok"] is False
assert payload["error"]["code"] == "SMART_RETRIEVAL_FAILED"
assert payload["elapsed_ms"] == 125
assert events[-1]["status"] == "failed"
assert events[-1]["elapsed_ms"] == 125
assert events[-1]["error"] == payload["error"]
assert "provider-token-must-never-leak" not in json.dumps(
events,
ensure_ascii=False,
)
@pytest.mark.asyncio
async def test_chunk_regex_search_is_case_insensitive_and_bounded(monkeypatch) -> None:
monkeypatch.setattr(tools, "get_llmwiki_store", lambda: _MappingStore())
monkeypatch.setattr(tools, "get_effective_user_id", lambda: "user-1")
monkeypatch.setattr(tools, "get_current_user", lambda: SimpleNamespace(system_role="user"))
monkeypatch.setattr(tools, "_client", lambda: _Client())
payload = json.loads(
await tools._chunk_regex_impl(
r"A-17|十万元",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["result_count"] == 1
assert payload["results"][0]["chunk_id"] == "chunk-1"
assert payload["scanned_chunks"] == 2
def test_wiki_page_reference_round_trip_and_regex_validation() -> None:
reference = tools._page_ref("mapping-1", "制度/采购")
assert tools._decode_page_ref(reference) == ("mapping-1", "制度/采购")
assert tools._safe_regex("采购|A-17").search("a-17")
with pytest.raises(tools.SmartKnowledgeToolError):
tools._safe_regex("(a+)+")
class _MultiMappingStore:
async def get_authorized(
self,
mapping_id: str,
user_id: str,
*,
write: bool,
is_admin: bool,
) -> dict | None:
assert user_id == "user-1"
assert write is False
if mapping_id not in {"mapping-1", "mapping-2"}:
return None
suffix = mapping_id.rsplit("-", 1)[-1]
return {
"id": mapping_id,
"weknora_id": f"remote-{suffix}",
"name": f"制度库{suffix}",
}
def _patch_scope(monkeypatch, *, multi: bool = False) -> None:
monkeypatch.setattr(
tools,
"get_llmwiki_store",
lambda: _MultiMappingStore() if multi else _MappingStore(),
)
monkeypatch.setattr(tools, "get_effective_user_id", lambda: "user-1")
monkeypatch.setattr(
tools,
"get_current_user",
lambda: SimpleNamespace(system_role="user"),
)
@pytest.mark.asyncio
async def test_classic_wiki_index_is_scope_bound_drawer_ready_and_bounded(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class Client:
async def get_wiki_index(
self,
knowledge_base_id: str,
*,
types: list[str],
limit: int,
cursor: str,
) -> dict:
assert knowledge_base_id == "remote-1"
assert types == ["summary", "entity"]
assert limit == 100
assert cursor == ""
return {
"intro": "入口见 [[news/timeline|新闻时间线]]。",
"groups": [
{
"type": "summary",
"total": 60,
"next_cursor": "next-summary",
"items": [
{
"id": f"page-{index}",
"slug": f"news/page-{index}",
"title": f"新闻页面 {index}",
"summary": "摘要" * 1_000,
"category_path": ["新闻", "国内"],
}
for index in range(60)
],
}
],
}
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_index_impl(
"mapping-1",
{
"knowledge_retrieval_mode": "classic",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
types=["summary", "entity"],
limit=100,
)
)
assert payload["ok"] is True
assert payload["operation"] == "wiki_browse_index"
assert payload["results_truncated"] is True
assert len(payload["groups"][0]["items"]) == tools._MAX_CLASSIC_WIKI_INDEX_ITEMS
assert payload["groups"][0]["items_truncated"] is True
assert payload["groups"][0]["next_cursor"] == "next-summary"
assert "/weknora-source-preview?" in payload["results"][0]["content"]
assert "wiki_slug=news%2Ftimeline" in payload["results"][0]["content"]
assert len(json.dumps(payload, ensure_ascii=False)) < tools._MAX_TOOL_OUTPUT_CHARS
with pytest.raises(tools.SmartKnowledgeToolError) as caught:
await tools._wiki_index_impl(
"mapping-1",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
assert caught.value.code == "CLASSIC_MODE_REQUIRED"
@pytest.mark.asyncio
async def test_classic_wiki_page_read_returns_source_drawer_metadata(monkeypatch) -> None:
_patch_scope(monkeypatch)
class MissingLocalMirror:
async def list_index_status(self, _mapping_ids: list[str]) -> list[dict]:
raise RuntimeError("local index disabled")
class Client:
async def get_wiki_page(self, knowledge_base_id: str, slug: str) -> dict:
assert (knowledge_base_id, slug) == ("remote-1", "news/timeline")
return {
"slug": slug,
"title": "新闻时间线",
"description": "按日期整理",
"markdown": "火炬传递、开幕与闭幕时间线。",
"status": "published",
}
monkeypatch.setattr(tools, "get_local_wiki_index_store", MissingLocalMirror)
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_read_impl(
tools._page_ref("mapping-1", "news/timeline"),
{
"knowledge_retrieval_mode": "classic",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
required_mode="classic",
)
)
page = payload["results"][0]
assert page["knowledge_base_id"] == "mapping-1"
assert page["knowledge_base_name"] == "制度库"
assert page["wiki_slug"] == "news/timeline"
assert page["type"] == "llmwiki_wiki_page"
assert page["content"] == "火炬传递、开幕与闭幕时间线。"
assert page["url"].endswith("knowledge_base_id=mapping-1&wiki_slug=news%2Ftimeline")
@pytest.mark.asyncio
async def test_authorization_read_is_routed_to_the_engine_event_loop(
monkeypatch,
) -> None:
home_loop = asyncio.new_event_loop()
ready = threading.Event()
def run_home_loop() -> None:
asyncio.set_event_loop(home_loop)
ready.set()
home_loop.run_forever()
home_loop.close()
thread = threading.Thread(target=run_home_loop, daemon=True)
thread.start()
assert ready.wait(timeout=2)
observed_loops: list[asyncio.AbstractEventLoop] = []
class Store:
async def get_authorized(
self,
mapping_id: str,
user_id: str,
*,
write: bool,
is_admin: bool,
) -> dict:
observed_loops.append(asyncio.get_running_loop())
return {"id": mapping_id, "owner_user_id": user_id}
monkeypatch.setattr(tools, "get_engine_loop", lambda: home_loop)
try:
row = await tools._get_authorized_on_engine_loop(
Store(),
"mapping-1",
"user-1",
is_admin=False,
)
finally:
home_loop.call_soon_threadsafe(home_loop.stop)
thread.join(timeout=2)
assert row == {"id": "mapping-1", "owner_user_id": "user-1"}
assert observed_loops == [home_loop]
@pytest.mark.asyncio
async def test_search_all_failures_are_errors_and_partial_failure_is_structured(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class Client:
async def search(self, query: str, knowledge_base_ids: list[str]) -> list[dict]:
if query == "失败查询":
raise WeKnoraError("down")
return [
{
"chunk_id": "chunk-ok",
"knowledge_id": "doc-ok",
"knowledge_base_id": "remote-1",
"content": "可用证据",
"score": 1,
}
]
monkeypatch.setattr(tools, "_client", Client)
failed = json.loads(
await tools._knowledge_search_impl(
["失败查询"],
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert failed["ok"] is False
assert failed["error"]["code"] == "PROVIDER_UNAVAILABLE"
partial = json.loads(
await tools._knowledge_search_impl(
["成功查询", "失败查询"],
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert partial["ok"] is True
assert partial["partial"] is True
assert partial["warnings"] == [
{"code": "PROVIDER_QUERY_FAILED", "query_index": 2}
]
@pytest.mark.asyncio
async def test_wiki_search_uses_native_provider_for_each_mapping_concurrently(
monkeypatch,
) -> None:
_patch_scope(monkeypatch, multi=True)
class LocalIndexMustNotRun:
def __init__(self) -> None:
raise AssertionError("smart Wiki regex must use the native provider API")
class Client:
calls: list[tuple[str, str, int]] = []
active = 0
peak = 0
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
self.calls.append((knowledge_base_id, query, limit))
self.active += 1
self.peak = max(self.peak, self.active)
await asyncio.sleep(0.01)
self.active -= 1
suffix = knowledge_base_id.rsplit("-", 1)[-1]
return [
{
"slug": f"news/{suffix}",
"title": f"needle 页面 {suffix}",
"content": "needle 正文",
"status": "published",
}
]
async def list_wiki_pages(self, *args, **kwargs) -> dict:
raise AssertionError("Wiki directory enumeration is forbidden")
async def get_wiki_page(self, *args, **kwargs) -> dict:
raise AssertionError("search must not fetch each page body")
client = Client()
monkeypatch.setattr(tools, "get_local_wiki_index_store", LocalIndexMustNotRun)
monkeypatch.setattr(tools, "_client", lambda: client)
payload = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
)
)
assert payload["ok"] is True
assert payload["native_regex_supported"] is True
assert payload["result_count"] == 2
assert {call[:2] for call in client.calls} == {
("remote-1", "needle"),
("remote-2", "needle"),
}
assert client.peak == 2
assert payload["channel"] == "provider_native_regex"
assert [item["status"] for item in payload["coverage"]] == [
"searched",
"searched",
]
@pytest.mark.asyncio
async def test_wiki_search_marks_provider_limit_as_truncated(monkeypatch) -> None:
_patch_scope(monkeypatch)
class Client:
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
assert (knowledge_base_id, query, limit) == ("remote-1", "needle", 50)
return [
{
"slug": f"news/{index}",
"title": f"needle 页面 {index}",
"content": "needle 正文",
}
for index in range(limit)
]
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["scanned_pages"] == 50
assert payload["scan_truncated"] is True
assert payload["result_count"] == tools._MAX_RESULTS
@pytest.mark.asyncio
async def test_wiki_search_handles_supported_and_unsupported_mappings(
monkeypatch,
) -> None:
_patch_scope(monkeypatch, multi=True)
class Client:
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
if knowledge_base_id == "remote-2":
raise WeKnoraError("route unavailable", status_code=404)
return [{"slug": "news/one", "title": f"{query} 页面"}]
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
)
)
assert payload["ok"] is True
assert payload["native_regex_supported"] is True
assert payload["unsupported_knowledge_base_count"] == 1
assert payload["partial"] is False
assert payload["warnings"] == []
assert [item["status"] for item in payload["coverage"]] == [
"searched",
"unsupported",
]
@pytest.mark.asyncio
async def test_wiki_search_without_native_client_method_is_unsupported(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class LegacyClient:
async def list_wiki_pages(self, *args, **kwargs) -> dict:
raise AssertionError("legacy directory fallback must not run")
async def get_wiki_page(self, *args, **kwargs) -> dict:
raise AssertionError("legacy page fallback must not run")
monkeypatch.setattr(tools, "_client", LegacyClient)
payload = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["ok"] is True
assert payload["native_regex_supported"] is False
assert payload["unsupported_knowledge_base_count"] == 1
assert payload["results"] == []
assert payload["warnings"] == []
@pytest.mark.asyncio
@pytest.mark.parametrize("status_code", [404, 405])
async def test_wiki_search_skips_unsupported_native_regex_without_fallback(
monkeypatch,
status_code: int,
) -> None:
_patch_scope(monkeypatch)
class Client:
async def search_wiki_pages(self, *args, **kwargs) -> list[dict]:
raise WeKnoraError("route unavailable", status_code=status_code)
async def list_wiki_pages(self, *args, **kwargs) -> dict:
raise AssertionError("unsupported native search must not enumerate Wiki pages")
async def get_wiki_page(self, *args, **kwargs) -> dict:
raise AssertionError("unsupported native search must not read page bodies")
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_search_impl(
"星河工程",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["ok"] is True
assert payload["result_count"] == 0
assert payload["native_regex_supported"] is False
assert payload["unsupported_knowledge_base_count"] == 1
assert payload["partial"] is False
assert payload["warnings"] == []
assert payload["coverage"][0]["status"] == "unsupported"
@pytest.mark.asyncio
async def test_native_wiki_search_result_can_be_read_with_markdown_shape(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class EmptyMirror:
async def list_index_status(self, mapping_ids: list[str]) -> list[dict]:
return []
class Client:
search_calls: list[tuple[str, str, int]] = []
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
self.search_calls.append((knowledge_base_id, query, limit))
return [
{
"slug": "news/xinghe",
"title": "国内科技新闻",
"description": "科技报道汇总",
"markdown": "星河工程完成关键节点验收。",
"status": "published",
}
]
async def list_wiki_pages(self, *args, **kwargs) -> dict:
raise AssertionError("native search must not enumerate the directory")
async def get_wiki_page(self, knowledge_base_id: str, slug: str) -> dict:
assert (knowledge_base_id, slug) == ("remote-1", "news/xinghe")
return {
"slug": slug,
"title": "国内科技新闻",
"description": "科技报道汇总",
"markdown": "星河工程完成关键节点验收。参见 [[news/验收公告]]。",
"out_links": ["news/承建单位"],
"status": "published",
}
client = Client()
monkeypatch.setattr(tools, "get_local_wiki_index_store", EmptyMirror)
monkeypatch.setattr(tools, "_client", lambda: client)
payload = json.loads(
await tools._wiki_search_impl(
"星河工程",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["results"][0]["matched_fields"] == ["content"]
assert client.search_calls == [("remote-1", "星河工程", 50)]
read = json.loads(
await tools._wiki_read_impl(
payload["results"][0]["page_ref"],
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert read["results"][0]["content_markdown"] == "星河工程完成关键节点验收。参见 [[news/验收公告]]。"
assert read["results"][0]["summary"] == "科技报道汇总"
assert read["results"][0]["links"] == ["news/承建单位", "news/验收公告"]
@pytest.mark.asyncio
async def test_local_wiki_read_exposes_bounded_graph_links_for_adaptive_expansion(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class LocalIndex:
async def list_index_status(self, mapping_ids: list[str]) -> list[dict]:
assert mapping_ids == ["mapping-1"]
return [
{
"knowledge_base_mapping_id": "mapping-1",
"state": "idle",
"last_completed_at": "2026-10-14T00:00:00Z",
"remote_page_count": 2,
"local_page_count": 2,
"failed_page_count": 0,
"last_error": None,
"last_sync_result": {"status": "completed"},
}
]
async def get_page(self, mapping_id: str, slug: str) -> dict:
assert (mapping_id, slug) == ("mapping-1", "news/xinghe")
return {
"slug": slug,
"title": "星河工程建设进展",
"aliases": ["星河计划"],
"content_md": "已完成节点验收。",
"status": "published",
}
async def get_adjacent_pages(
self,
mapping_ids: list[str],
seed_slugs: dict[str, list[str]],
*,
direction: str,
limit: int,
include_drafts: bool,
) -> list[dict]:
assert mapping_ids == ["mapping-1"]
assert seed_slugs == {"mapping-1": ["news/xinghe"]}
assert direction == "both"
assert limit == 6
assert include_drafts is False
return [
{
"neighbor_slug": "entities/tianheng",
"direction": "outgoing",
"page": {
"slug": "entities/tianheng",
"title": "天衡建设集团",
},
}
]
class Client:
async def get_wiki_page(self, *_args, **_kwargs):
raise AssertionError("complete local mirror must serve the page")
monkeypatch.setattr(tools, "get_local_wiki_index_store", LocalIndex)
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_read_impl(
tools._page_ref("mapping-1", "news/xinghe"),
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["channel"] == "local_wiki_mirror"
assert payload["results"][0]["aliases"] == ["星河计划"]
assert payload["results"][0]["links"] == [
{
"slug": "entities/tianheng",
"title": "天衡建设集团",
"direction": "outgoing",
}
]
@pytest.mark.asyncio
async def test_wiki_read_hides_expansion_links_when_database_setting_is_disabled(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class DisabledMappingStore(_MappingStore):
async def get_authorized(self, *args, **kwargs) -> dict | None:
row = await super().get_authorized(*args, **kwargs)
return {
**(row or {}),
"retrieval_settings": {
"entity_expansion_enabled": False,
"max_related_terms": 6,
"statistics_enabled": True,
},
}
class LocalIndex:
async def list_index_status(self, _mapping_ids: list[str]) -> list[dict]:
return [
{
"knowledge_base_mapping_id": "mapping-1",
"state": "idle",
"last_completed_at": "2026-10-14T00:00:00Z",
"remote_page_count": 1,
"local_page_count": 1,
"failed_page_count": 0,
"last_error": None,
"last_sync_result": {"status": "completed"},
}
]
async def get_page(self, _mapping_id: str, slug: str) -> dict:
return {
"slug": slug,
"title": "星河工程建设进展",
"content_md": "参见 [[entities/tianheng]]。",
"out_links": ["entities/tianheng"],
"status": "published",
}
async def get_adjacent_pages(self, *_args, **_kwargs) -> list[dict]:
raise AssertionError("disabled expansion must not query the Wiki graph")
monkeypatch.setattr(tools, "get_llmwiki_store", DisabledMappingStore)
monkeypatch.setattr(tools, "get_local_wiki_index_store", LocalIndex)
payload = json.loads(
await tools._wiki_read_impl(
tools._page_ref("mapping-1", "news/xinghe"),
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["results"][0]["links"] == []
@pytest.mark.asyncio
async def test_wiki_regex_does_not_use_a_stale_local_mirror(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
class StaleMirror:
def __init__(self) -> None:
raise AssertionError("smart Wiki regex must not inspect the local mirror")
class Client:
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
assert (knowledge_base_id, query, limit) == (
"remote-1",
"星河工程",
50,
)
return [
{
"slug": "news/new",
"title": "新报道",
"content": "星河工程发布最新进展。",
"status": "published",
}
]
monkeypatch.setattr(tools, "get_local_wiki_index_store", StaleMirror)
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._wiki_search_impl(
"星河工程",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["result_count"] == 1
assert payload["results"][0]["slug"] == "news/new"
assert payload["channel"] == "provider_native_regex"
@pytest.mark.asyncio
async def test_wiki_provider_all_failures_return_error_and_partial_is_warning(
monkeypatch,
) -> None:
_patch_scope(monkeypatch, multi=True)
class AllFailedClient:
async def search_wiki_pages(self, *args, **kwargs) -> list[dict]:
raise WeKnoraError("down", status_code=503)
monkeypatch.setattr(tools, "_client", AllFailedClient)
failed = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
)
)
assert failed["ok"] is False
assert failed["error"]["code"] == "PROVIDER_UNAVAILABLE"
class PartiallyFailedClient:
async def search_wiki_pages(
self,
knowledge_base_id: str,
query: str,
*,
limit: int,
) -> list[dict]:
if knowledge_base_id == "remote-2":
raise WeKnoraError("down", status_code=503)
return [{"slug": "ok", "title": "needle", "content": "正文"}]
monkeypatch.setattr(tools, "_client", PartiallyFailedClient)
partial = json.loads(
await tools._wiki_search_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
)
)
assert partial["ok"] is True
assert partial["partial"] is True
assert partial["warnings"] == [
{"code": "PROVIDER_WIKI_SEARCH_FAILED", "knowledge_base": "制度库2"}
]
@pytest.mark.asyncio
async def test_client_grep_compatibility_fallback_is_marked_and_fair() -> None:
class Client(WeKnoraClient):
async def _request(self, *args, **kwargs):
raise WeKnoraError("missing", status_code=404)
async def list_documents(
self,
knowledge_base_id: str,
*,
page: int,
page_size: int,
) -> dict:
count = 30 if knowledge_base_id == "remote-1" else 1
rows = [
{"id": f"{knowledge_base_id}-doc-{index}", "title": "needle title"}
for index in range(count)
]
return {"items": rows, "total": count}
async def list_chunks(
self,
knowledge_id: str,
*,
page: int,
page_size: int,
) -> dict:
return {
"items": [
{
"id": f"{knowledge_id}-chunk",
"content": f"needle from {knowledge_id}",
"chunk_index": 0,
}
],
"total": 1,
}
client = Client(base_url="http://weknora.test")
payload = await client.grep_chunks("needle", ["remote-1", "remote-2"])
assert payload["compatibility_fallback"] is True
assert payload["contract"] == "weknora-existing-api-compat-v1"
assert payload["successful_knowledge_base_ids"] == ["remote-1", "remote-2"]
assert {item["knowledge_base_id"] for item in payload["items"]} == {
"remote-1",
"remote-2",
}
assert payload["warnings"][0]["code"] == "WEKNORA_GREP_COMPATIBILITY_FALLBACK"
@pytest.mark.asyncio
async def test_client_grep_prefers_single_cross_scope_provider_request() -> None:
class Client(WeKnoraClient):
calls: list[tuple[str, str, dict]] = []
async def _request(self, method: str, path: str, *, json=None, **kwargs):
self.calls.append((method, path, json))
return {
"success": True,
"data": {
"contract": "weknora-v0.7.2-grep-v1",
"chunk_results": [
{
"id": "chunk-2",
"knowledge_id": "doc-2",
"knowledge_base_id": "remote-2",
"knowledge_title": "第二库",
"content": "needle",
}
],
},
}
client = Client(base_url="http://weknora.test")
payload = await client.grep_chunks("needle", ["remote-1", "remote-2"])
assert client.calls == [
(
"POST",
"/api/v1/chunks/grep",
{
"query": "needle",
"knowledge_base_ids": ["remote-1", "remote-2"],
},
)
]
assert payload["compatibility_fallback"] is False
assert payload["items"][0]["knowledge_base_id"] == "remote-2"
@pytest.mark.asyncio
async def test_client_grep_compatibility_all_scope_failure_raises() -> None:
class Client(WeKnoraClient):
async def _request(self, *args, **kwargs):
raise WeKnoraError("missing", status_code=405)
async def list_documents(self, *args, **kwargs) -> dict:
raise WeKnoraError("down")
client = Client(base_url="http://weknora.test")
with pytest.raises(WeKnoraError):
await client.grep_chunks("needle", ["remote-1", "remote-2"])
@pytest.mark.asyncio
async def test_chunk_grep_all_provider_failure_returns_error(monkeypatch) -> None:
_patch_scope(monkeypatch)
class Client:
async def grep_chunks(self, query: str, knowledge_base_ids: list[str]) -> dict:
raise WeKnoraError("down")
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._chunk_regex_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
)
)
assert payload["ok"] is False
@pytest.mark.asyncio
async def test_chunk_grep_partial_failure_is_structured_and_sanitized(monkeypatch) -> None:
_patch_scope(monkeypatch, multi=True)
class Client:
async def grep_chunks(self, query: str, knowledge_base_ids: list[str]) -> dict:
return {
"items": [
{
"chunk_id": "chunk-1",
"knowledge_id": "doc-1",
"knowledge_base_id": "remote-1",
"content": "needle",
}
],
"contract": "weknora-existing-api-compat-v1",
"compatibility_fallback": True,
"partial": True,
"warnings": [
{"code": "PROVIDER_SCOPE_FAILED", "knowledge_base_id": "remote-2"}
],
"successful_knowledge_base_ids": ["remote-1"],
"failed_knowledge_base_ids": ["remote-2"],
}
monkeypatch.setattr(tools, "_client", Client)
payload = json.loads(
await tools._chunk_regex_impl(
"needle",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
)
)
assert payload["ok"] is True
assert payload["partial"] is True
assert payload["compatibility_fallback"] is True
assert payload["warnings"] == [
{"code": "PROVIDER_SCOPE_FAILED", "knowledge_base": "制度库2"}
]
@pytest.mark.asyncio
async def test_wiki_read_continuation_reconstructs_long_page_and_binds_cursor(
monkeypatch,
) -> None:
_patch_scope(monkeypatch)
content = "# 第一章\n\n" + "甲" * 35_000 + "\n\n# 第二章\n\n" + "乙" * 35_000
class LocalIndex:
async def list_index_status(self, mapping_ids: list[str]) -> list[dict]:
return [
{
"knowledge_base_mapping_id": "mapping-1",
"state": "idle",
"last_completed_at": "2026-10-14T00:00:00Z",
"remote_page_count": 1,
"local_page_count": 1,
"failed_page_count": 0,
"last_error": None,
"last_sync_result": {"status": "completed"},
}
]
async def get_page(self, mapping_id: str, slug: str) -> dict:
assert (mapping_id, slug) == ("mapping-1", "long-page")
return {"title": "长文", "slug": slug, "content_md": content}
monkeypatch.setattr(tools, "get_local_wiki_index_store", LocalIndex)
page_ref = tools._page_ref("mapping-1", "long-page")
cursor = ""
parts: list[str] = []
for _ in range(10):
payload = json.loads(
await tools._wiki_read_impl(
page_ref,
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
},
cursor=cursor,
)
)
page = payload["results"][0]
parts.append(page["content_markdown"])
if page["complete"]:
assert page["next_cursor"] is None
break
assert page["content_truncated"] is True
cursor = page["next_cursor"]
else:
pytest.fail("long Wiki page did not complete")
assert "".join(parts) == content
other_page_ref = tools._page_ref("mapping-1", "other-page")
with pytest.raises(tools.SmartKnowledgeToolError) as caught:
tools._decode_continuation_cursor(cursor, other_page_ref)
assert caught.value.code == "INVALID_CONTINUATION_CURSOR"
@pytest.mark.asyncio
async def test_deep_read_rejects_cross_scope_ids_and_allows_second_mapping(monkeypatch) -> None:
_patch_scope(monkeypatch)
class UnauthorizedClient:
async def get_chunk(self, chunk_id: str) -> dict:
return {"id": chunk_id, "knowledge_base_id": "remote-2"}
async def get_document(self, knowledge_id: str) -> dict:
return {"id": knowledge_id, "knowledge_base_id": "remote-2"}
monkeypatch.setattr(tools, "_client", UnauthorizedClient)
context = {
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1"],
}
with pytest.raises(tools.SmartKnowledgeToolError) as chunk_error:
await tools._document_chunks_impl(
"foreign-chunk",
"chunk_id",
context,
page=1,
page_size=20,
)
assert chunk_error.value.code == "CHUNK_NOT_FOUND"
with pytest.raises(tools.SmartKnowledgeToolError) as document_error:
await tools._document_chunks_impl(
"foreign-doc",
"knowledge_id",
context,
page=1,
page_size=20,
)
assert document_error.value.code == "DOCUMENT_NOT_FOUND"
_patch_scope(monkeypatch, multi=True)
class AuthorizedSecondClient:
async def get_document(self, knowledge_id: str) -> dict:
return {"id": knowledge_id, "knowledge_base_id": "remote-2"}
async def list_chunks(
self,
knowledge_id: str,
*,
page: int,
page_size: int,
) -> dict:
return {
"items": [{"id": "chunk-2", "content": "第二库正文"}],
"total": 1,
}
monkeypatch.setattr(tools, "_client", AuthorizedSecondClient)
allowed = json.loads(
await tools._document_chunks_impl(
"doc-2",
"knowledge_id",
{
"knowledge_retrieval_mode": "smart_reasoning",
"llmwiki_knowledge_base_ids": ["mapping-1", "mapping-2"],
},
page=1,
page_size=20,
)
)
assert allowed["ok"] is True
assert allowed["results"][0]["content"] == "第二库正文"
assert allowed["results"][0]["knowledge_base_id"] == "mapping-2"
def test_regex_execution_has_a_hard_timeout() -> None:
expression = tools._safe_regex("(a|aa)+$")
with pytest.raises(tools.SmartKnowledgeToolError) as caught:
tools._regex_search(expression, "a" * 10_000 + "!")
assert caught.value.code == "REGEX_TIMEOUT"
@pytest.mark.asyncio
@pytest.mark.parametrize("assistant_id", ["deep-research-planner", "deep-research-collector", "custom-agent"])
async def test_research_smart_scope_cannot_access_unauthorized_library(monkeypatch, assistant_id):
from app.gateway import llmwiki_rag
request = _request(None)
request.app.state.llmwiki_store = _MappingStore()
monkeypatch.setattr(llmwiki_rag, "get_resolved_llmwiki_runtime", lambda _: SimpleNamespace(weknora_enabled=True))
monkeypatch.setattr(llmwiki_rag, "_agent_bound_ids", lambda _: [])
async def actor(_request):
return "user-1", False
monkeypatch.setattr(llmwiki_rag, "_actor", actor)
with pytest.raises(HTTPException) as caught:
await llmwiki_rag.build_llmwiki_smart_reasoning_context(
request=request,
body=SimpleNamespace(context={"llmwiki_knowledge_base_ids": ["not-authorized"]}, metadata={}, assistant_id=assistant_id),
graph_input={"messages": [HumanMessage(content="test")]},
)
assert caught.value.status_code == 404
@pytest.mark.parametrize("agent_id", ["deep-research-planner", "deep-research-collector"])
def test_builtin_research_tools_use_caller_authorized_scope(agent_id):
assert tools._allowed_agent_ids({"agent_id": agent_id}) is None
from deerflow.tools.builtins.llmwiki_search_tool import _agent_allowed_ids
assert _agent_allowed_ids({"agent_id": agent_id}) is None