From b6624298423d4d1f6e6744e575a6039012d6fe15 Mon Sep 17 00:00:00 2001 From: Steven Date: Sun, 1 Nov 2026 02:55:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(knowledge):=20=E7=BF=BB=E9=A1=B5=E6=B8=B8?= =?UTF-8?q?=E6=A0=87=E7=9A=84=E5=AD=97=E6=AE=B5=E5=90=8D=E5=86=99=E9=94=99?= =?UTF-8?q?=EF=BC=8C=E7=82=B9=E3=80=8C=E5=8A=A0=E8=BD=BD=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E3=80=8D=E5=BF=85=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 知识列表拿游标做 keyset 分页时写成了 `cur.item_id`,而 `Cursor` 只有 `created_at` 和 `id` 两个字段——属性不存在,直接 AttributeError, 前端只看到"服务器内部错误"。 库里不足一页(默认 30 条)时永远走不到这条分支,所以一直没暴露; 旧备忘导进来 84 条之后,用户点「加载更多」就撞上了。 补一条回归用例:造 25 条、每页 10 条翻到底,断言不重不漏、末页游标为空。 --- backend/app/api/knowledge.py | 2 +- backend/tests/test_knowledge.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/app/api/knowledge.py b/backend/app/api/knowledge.py index 98eecae..c035ce5 100644 --- a/backend/app/api/knowledge.py +++ b/backend/app/api/knowledge.py @@ -328,7 +328,7 @@ async def list_knowledge( stmt = stmt.where( or_( Item.updated_at < cur.created_at, - and_(Item.updated_at == cur.created_at, Item.id < cur.item_id), + and_(Item.updated_at == cur.created_at, Item.id < cur.id), ) ) diff --git a/backend/tests/test_knowledge.py b/backend/tests/test_knowledge.py index 76cdebc..568eb87 100644 --- a/backend/tests/test_knowledge.py +++ b/backend/tests/test_knowledge.py @@ -381,3 +381,45 @@ async def test_draft_timeline_reads_like_a_story(client: AsyncClient) -> None: body = doc["body_md"] assert "待办 → 进行中" in body assert "进行中 → 已完成" in body + + +@pytest.mark.asyncio +async def test_list_pages_through_every_document(client: AsyncClient) -> None: + """翻页要能翻到底,且不重不漏。 + + 这条用例是有来历的:知识列表的游标解出来是 `Cursor(created_at, id)`, + 查询里却写成了 `cur.item_id`——属性不存在,第二页必 500。 + 库小于一页(默认 30 条)时永远走不到这条路径,所以一直没暴露, + 直到把旧备忘导进来 84 条,用户点「加载更多」就看到"服务器内部错误"。 + """ + owner = await login(client) + ws = await create_workspace(client, owner, "翻页") + titles = [f"备忘 {i:02d}" for i in range(25)] + for title in titles: + unwrap( + await client.post( + f"{V1}/workspaces/{ws['id']}/knowledge", + json={"title": title}, + headers=owner["headers"], + ) + ) + + seen: list[str] = [] + cursor: str | None = None + for _ in range(10): # 25 条 / 每页 10 条,三轮就该到底,多留几次余地 + params: dict[str, Any] = {"limit": 10} + if cursor: + params["cursor"] = cursor + page = unwrap( + await client.get( + f"{V1}/workspaces/{ws['id']}/knowledge", params=params, headers=owner["headers"] + ) + ) + seen.extend(i["title"] for i in page["items"]) + cursor = page["next_cursor"] + if not page["has_more"] or not cursor: + break + + assert cursor is None + assert sorted(seen) == titles + assert len(seen) == len(set(seen)), "翻页翻出了重复条目"