199 lines
6.5 KiB
Python
199 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from yuxi.knowledge.implementations.notion import NOTION_DEFAULT_VERSION, NotionAPIError, NotionKB
|
|
|
|
|
|
PAGE_ID = "page-1"
|
|
DATA_SOURCE_ID = "ds-1"
|
|
|
|
|
|
PAGE = {
|
|
"object": "page",
|
|
"id": PAGE_ID,
|
|
"url": "https://www.notion.so/page-1",
|
|
"created_time": "2026-01-01T00:00:00.000Z",
|
|
"last_edited_time": "2026-01-02T00:00:00.000Z",
|
|
"parent": {"type": "data_source_id", "data_source_id": DATA_SOURCE_ID},
|
|
"properties": {
|
|
"Name": {"type": "title", "title": [{"plain_text": "Reasoning Paper"}]},
|
|
"Abstract": {"type": "rich_text", "rich_text": [{"plain_text": "Chain of thought reasoning"}]},
|
|
},
|
|
}
|
|
|
|
|
|
BLOCKS = {
|
|
PAGE_ID: [
|
|
{
|
|
"object": "block",
|
|
"id": "block-1",
|
|
"type": "paragraph",
|
|
"has_children": False,
|
|
"paragraph": {"rich_text": [{"plain_text": "This page discusses reasoning models."}]},
|
|
},
|
|
{
|
|
"object": "block",
|
|
"id": "block-2",
|
|
"type": "heading_2",
|
|
"has_children": False,
|
|
"heading_2": {"rich_text": [{"plain_text": "Evaluation"}]},
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
class _FakeNotionClient:
|
|
def __init__(self, token: str, notion_version: str) -> None:
|
|
self.token = token
|
|
self.notion_version = notion_version
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
return False
|
|
|
|
async def search_pages(self, query_text: str, limit: int) -> list[dict]:
|
|
del query_text, limit
|
|
return [PAGE]
|
|
|
|
async def query_data_source(self, data_source_id: str, limit: int) -> list[dict]:
|
|
del limit
|
|
assert data_source_id == DATA_SOURCE_ID
|
|
return [PAGE]
|
|
|
|
async def retrieve_page(self, page_id: str) -> dict:
|
|
assert page_id == PAGE_ID
|
|
return PAGE
|
|
|
|
async def retrieve_block_children(self, block_id: str, limit: int) -> list[dict]:
|
|
del limit
|
|
return BLOCKS.get(block_id, [])
|
|
|
|
|
|
class _FailingNotionClient(_FakeNotionClient):
|
|
async def search_pages(self, query_text: str, limit: int) -> list[dict]:
|
|
del query_text, limit
|
|
raise NotionAPIError("boom")
|
|
|
|
|
|
class _UnknownParentNotionClient(_FakeNotionClient):
|
|
async def retrieve_page(self, page_id: str) -> dict:
|
|
page = await super().retrieve_page(page_id)
|
|
return {**page, "parent": {"type": "page_id", "page_id": "other-page"}}
|
|
|
|
|
|
@pytest.fixture
|
|
def notion_kb(tmp_path):
|
|
kb = NotionKB(str(tmp_path))
|
|
kb.databases_meta["kb_notion"] = {
|
|
"name": "notion-kb",
|
|
"description": "test",
|
|
"kb_type": "notion",
|
|
"query_params": {"options": {}},
|
|
"metadata": {
|
|
"notion_token": "token",
|
|
"notion_data_source_id": DATA_SOURCE_ID,
|
|
"notion_version": NOTION_DEFAULT_VERSION,
|
|
},
|
|
}
|
|
return kb
|
|
|
|
|
|
def test_notion_create_params_config_and_validation(monkeypatch):
|
|
monkeypatch.delenv("NOTION_TOKEN", raising=False)
|
|
monkeypatch.delenv("NOTION_API_KEY", raising=False)
|
|
|
|
config = NotionKB.get_create_params_config()
|
|
keys = [option["key"] for option in config["options"]]
|
|
assert keys == ["notion_token", "notion_data_source_id", "notion_version"]
|
|
|
|
params = NotionKB.normalize_additional_params(
|
|
{
|
|
"notion_token": " token ",
|
|
"notion_data_source_id": " ds-1 ",
|
|
"notion_version": " 2026-03-11 ",
|
|
}
|
|
)
|
|
assert params == {
|
|
"notion_token": "token",
|
|
"notion_data_source_id": DATA_SOURCE_ID,
|
|
"notion_version": NOTION_DEFAULT_VERSION,
|
|
}
|
|
assert "chunk_preset_id" not in params
|
|
|
|
|
|
def test_notion_validation_accepts_env_token(monkeypatch):
|
|
monkeypatch.setenv("NOTION_TOKEN", "env-token")
|
|
params = NotionKB.normalize_additional_params({"notion_data_source_id": DATA_SOURCE_ID})
|
|
assert params["notion_token"] == ""
|
|
assert params["notion_data_source_id"] == DATA_SOURCE_ID
|
|
|
|
|
|
def test_notion_validation_rejects_missing_params(monkeypatch):
|
|
monkeypatch.delenv("NOTION_TOKEN", raising=False)
|
|
monkeypatch.delenv("NOTION_API_KEY", raising=False)
|
|
|
|
with pytest.raises(ValueError, match="notion_data_source_id"):
|
|
NotionKB.normalize_additional_params({"notion_token": "token"})
|
|
|
|
with pytest.raises(ValueError, match="notion_token"):
|
|
NotionKB.normalize_additional_params({"notion_data_source_id": DATA_SOURCE_ID})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notion_kb_aquery_maps_pages(monkeypatch, notion_kb):
|
|
monkeypatch.setattr("yuxi.knowledge.implementations.notion._NotionClient", _FakeNotionClient)
|
|
|
|
result = await notion_kb.aquery("reasoning", "kb_notion")
|
|
|
|
assert len(result) == 1
|
|
assert "reasoning" in result[0]["content"].lower()
|
|
assert result[0]["score"] > 0
|
|
assert result[0]["metadata"]["source"] == "Reasoning Paper"
|
|
assert result[0]["metadata"]["file_id"] == PAGE_ID
|
|
assert result[0]["metadata"]["chunk_id"].startswith(f"{PAGE_ID}:")
|
|
assert result[0]["metadata"]["notion_url"] == "https://www.notion.so/page-1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notion_open_file_content_uses_page_markdown(monkeypatch, notion_kb):
|
|
monkeypatch.setattr("yuxi.knowledge.implementations.notion._NotionClient", _FakeNotionClient)
|
|
|
|
result = await notion_kb.open_file_content("kb_notion", PAGE_ID, offset=0, limit=3)
|
|
|
|
assert result["start_line"] == 1
|
|
assert result["end_line"] == 3
|
|
assert result["has_more_after"] is True
|
|
assert "Reasoning Paper" in result["content"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notion_find_file_content_uses_page_markdown(monkeypatch, notion_kb):
|
|
monkeypatch.setattr("yuxi.knowledge.implementations.notion._NotionClient", _FakeNotionClient)
|
|
|
|
result = await notion_kb.find_file_content("kb_notion", PAGE_ID, ["models"], window_size=4)
|
|
|
|
assert result["match_mode"] == "keyword"
|
|
assert result["total_matches"] == 1
|
|
assert result["windows"]
|
|
assert "reasoning models" in result["windows"][0]["content"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notion_open_file_content_rejects_unknown_parent(monkeypatch, notion_kb):
|
|
monkeypatch.setattr("yuxi.knowledge.implementations.notion._NotionClient", _UnknownParentNotionClient)
|
|
|
|
with pytest.raises(ValueError, match="不属于当前 Data Source"):
|
|
await notion_kb.open_file_content("kb_notion", PAGE_ID)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notion_kb_aquery_error_returns_empty(monkeypatch, notion_kb):
|
|
monkeypatch.setattr("yuxi.knowledge.implementations.notion._NotionClient", _FailingNotionClient)
|
|
|
|
result = await notion_kb.aquery("reasoning", "kb_notion")
|
|
|
|
assert result == []
|