Files
knowledge-adapter-service/tests/test_weknora_resource_resolver.py
T
姚章浩 693cd952cd feat(asset): 清理被截断的结尾图片引用,修复整单 502
仓库首次入库(此前仅存在于本地工作区与测试服构建产物中,未上传)。

改动(src/knowledge_adapter/asset_content_processor.py):
- 新增 _TRUNCATED_TRAILING_MARKDOWN_RESOURCE_IMAGE 规则,清除 chunk 末尾被截断的
  ![](resource://xxxx 残片(WeKnora 分块在图片引用中间截断时产生,旧逻辑无法匹配完整引用,
  残留 resource:// 触发 ContentProcessingError,导致整单 502)
- 回归测试 tests/test_asset_content_processor.py::
  test_truncated_trailing_markdown_resource_image_is_removed

验证证据:
- 测试服 120.76.217.51,镜像 knowledge-adapter-service:reviewed-test-aeab4b5fca2d
  (release aeab4b5fca2d8661):KB d015d05c-25c8-4d63-b2ab-e8590d3c2dfb 5 个查询全部
  http=200、records=5、残留 resource:// = 0;KB b9dacd62 无退化;容器日志无 error/exception
- 本地:pytest 141 项全通过、ruff 全通过
- 该文件与 258c31e0fe07fd08 源码的差异经逐文件 md5 比对,为唯一变更文件
2026-09-16 10:01:27 +08:00

238 lines
7.3 KiB
Python

from contextlib import AbstractAsyncContextManager
from types import TracebackType
from uuid import UUID
import pytest
from knowledge_adapter.weknora_resource_resolver import (
ResolvedAsset,
ResourceResolutionError,
WeKnoraResourceResolver,
)
HANDLE = "AbCdEfGhIjKlMnOpQrSt_1"
BACKEND_ID = "550e8400-e29b-41d4-a716-446655440000"
class FakeTransaction(AbstractAsyncContextManager[None]):
def __init__(self) -> None:
self.entered = False
self.readonly: bool | None = None
async def __aenter__(self) -> None:
self.entered = True
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
return None
class FakeConnection:
def __init__(self, rows: list[dict[str, object]]) -> None:
self.rows = rows
self.transaction_context = FakeTransaction()
self.query = ""
self.args: tuple[object, ...] = ()
def transaction(self, *, readonly: bool) -> FakeTransaction:
self.transaction_context.readonly = readonly
return self.transaction_context
async def fetch(self, query: str, *args: object) -> list[dict[str, object]]:
self.query = query
self.args = args
return self.rows
class AcquireContext(AbstractAsyncContextManager[FakeConnection]):
def __init__(self, connection: FakeConnection) -> None:
self.connection = connection
async def __aenter__(self) -> FakeConnection:
return self.connection
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
return None
class FakePool:
def __init__(self, connection: FakeConnection) -> None:
self.connection = connection
self.acquire_count = 0
def acquire(self) -> AcquireContext:
self.acquire_count += 1
return AcquireContext(self.connection)
class FailingConnection(FakeConnection):
async def fetch(self, query: str, *args: object) -> list[dict[str, object]]:
raise RuntimeError(
"postgresql://readonly:secret@db.internal resources physical_path=private/object"
)
async def test_resolver_uses_minimal_parameterized_readonly_query() -> None:
connection = FakeConnection(
[
{
"tenant_id": 10008,
"storage_backend_id": "backend-test",
"provider": "oss",
"physical_path": (
"storage://backend-test/"
"oss://private-assets/weknora/10008/assets/image.png"
),
"mime_type": "image/png",
}
]
)
resolver = WeKnoraResourceResolver(
pool=FakePool(connection),
tenant_id=10008,
storage_backend_id="backend-test",
bucket="private-assets",
)
resolved = await resolver.resolve(f"resource://{HANDLE}")
assert resolved == ResolvedAsset(
object_key="weknora/10008/assets/image.png",
content_type="image/png",
)
assert connection.transaction_context.entered is True
assert connection.transaction_context.readonly is True
assert connection.args == (HANDLE, 10008, "backend-test")
normalized_query = " ".join(connection.query.split()).lower()
assert normalized_query.startswith(
"select tenant_id, storage_backend_id, provider, physical_path, mime_type from resources"
)
assert "handle = $1" in normalized_query
assert "tenant_id = $2" in normalized_query
assert "storage_backend_id = $3" in normalized_query
assert "state = 'active'" in normalized_query
assert "deleted_at is null" in normalized_query
for forbidden_column in ("content", "credentials", "api_key", "secret", "original_name"):
assert forbidden_column not in normalized_query
async def test_resolver_accepts_asyncpg_uuid_storage_backend_id() -> None:
connection = FakeConnection(
[
{
"tenant_id": 10008,
"storage_backend_id": UUID(BACKEND_ID),
"provider": "oss",
"physical_path": (
f"storage://{BACKEND_ID}/"
"oss://private-assets/weknora/10008/assets/image.png"
),
"mime_type": "image/png",
}
]
)
resolver = WeKnoraResourceResolver(
pool=FakePool(connection),
tenant_id=10008,
storage_backend_id=BACKEND_ID,
bucket="private-assets",
)
resolved = await resolver.resolve(f"resource://{HANDLE}")
assert resolved.object_key == "weknora/10008/assets/image.png"
@pytest.mark.parametrize("row_count", [0, 2])
async def test_resolver_requires_exactly_one_resource_row(row_count: int) -> None:
row = {
"tenant_id": 10008,
"storage_backend_id": "backend-test",
"provider": "oss",
"physical_path": (
"storage://backend-test/"
"oss://private-assets/weknora/10008/assets/image.png"
),
"mime_type": "image/png",
}
resolver = WeKnoraResourceResolver(
pool=FakePool(FakeConnection([row] * row_count)),
tenant_id=10008,
storage_backend_id="backend-test",
bucket="private-assets",
)
with pytest.raises(ResourceResolutionError, match=r"^resource unavailable$"):
await resolver.resolve(f"resource://{HANDLE}")
@pytest.mark.parametrize(
"overrides",
[
{"tenant_id": 99999},
{"storage_backend_id": "other-backend"},
{"provider": "cos"},
],
)
async def test_resolver_rechecks_row_ownership(overrides: dict[str, object]) -> None:
row = {
"tenant_id": 10008,
"storage_backend_id": "backend-test",
"provider": "oss",
"physical_path": (
"storage://backend-test/"
"oss://private-assets/weknora/10008/assets/image.png"
),
"mime_type": "image/png",
**overrides,
}
resolver = WeKnoraResourceResolver(
pool=FakePool(FakeConnection([row])),
tenant_id=10008,
storage_backend_id="backend-test",
bucket="private-assets",
)
with pytest.raises(ResourceResolutionError, match=r"^resource unavailable$"):
await resolver.resolve(f"resource://{HANDLE}")
async def test_resolver_maps_database_error_to_safe_fixed_error() -> None:
resolver = WeKnoraResourceResolver(
pool=FakePool(FailingConnection([])),
tenant_id=10008,
storage_backend_id="backend-test",
bucket="private-assets",
)
with pytest.raises(ResourceResolutionError) as captured:
await resolver.resolve(f"resource://{HANDLE}")
assert str(captured.value) == "resource unavailable"
assert "postgresql" not in str(captured.value)
assert "physical_path" not in str(captured.value)
async def test_invalid_resource_reference_is_rejected_before_database_access() -> None:
pool = FakePool(FakeConnection([]))
resolver = WeKnoraResourceResolver(
pool=pool,
tenant_id=10008,
storage_backend_id="backend-test",
bucket="private-assets",
)
with pytest.raises(ResourceResolutionError, match=r"^resource unavailable$"):
await resolver.resolve("https://attacker.example/image.png")
assert pool.acquire_count == 0