Files
MemeMeow/tests/test_config_http.py

111 lines
4.7 KiB
Python

"""公共核心 `/config` HTTP 边界测试。"""
from __future__ import annotations
import ast
import asyncio
from pathlib import Path
from types import SimpleNamespace
from starlette.requests import Request
import api
import backend.config_http as config_http
def test_config_http_boundary_preserves_aliases_and_one_way_dependency() -> None:
"""新模块与旧入口共享摘要 aliases,且不反向依赖 api。"""
assert api.STORAGE_PREFLIGHT_BLOCKING_KEYS is config_http.STORAGE_PREFLIGHT_BLOCKING_KEYS
assert api._storage_preflight_summary is config_http._storage_preflight_summary
assert api.config_status.__name__ == "config_status"
source = Path(config_http.__file__).read_text(encoding="utf-8")
tree = ast.parse(source)
imported_modules = {
alias.name.split(".", 1)[0]
for node in ast.walk(tree)
if isinstance(node, ast.Import)
for alias in node.names
}
imported_modules.update(
node.module.split(".", 1)[0]
for node in ast.walk(tree)
if isinstance(node, ast.ImportFrom) and node.module
)
assert "api" not in imported_modules
assert "server_api" not in imported_modules
def test_config_route_snapshot_keeps_public_metadata_and_position() -> None:
"""`/config` 继续是单个公开 GET system 路由,并位于 health 之后。"""
routes = [route for route in api.app.routes if getattr(route, "path", None) in {"/health", "/config"}]
assert [route.path for route in routes] == ["/health", "/config"]
config_route = routes[1]
assert config_route.methods == {"GET"}
assert config_route.name == "config_status"
assert config_route.tags == ["system"]
assert sum(getattr(route, "path", None) == "/config" for route in api.app.routes) == 1
created = api.create_app(scope_resolver=api.LocalScopeResolver())
created_routes = [route for route in created.routes if getattr(route, "path", None) == "/config"]
assert len(created_routes) == 1
assert created_routes[0].methods == {"GET"}
assert created_routes[0].name == "config_status"
def test_config_projection_uses_callbacks_and_redacts_runtime_storage_details() -> None:
"""配置投影复用注入 callback,并只返回固定 runtime/storage 字段。"""
runtime = SimpleNamespace(
runtime_probe=lambda: {
"mode": "host-runtime-slot-lock",
"verified": True,
"executor_running": False,
"container_name": "must-not-leak",
"diagnostic": "/srv/private/diagnostic",
}
)
reverse = SimpleNamespace(available=True)
app_stub = SimpleNamespace(
state=SimpleNamespace(
settings=SimpleNamespace(status=lambda: {"embedding_api_key_configured": False}),
opencode=runtime,
search_engine=SimpleNamespace(has_cache=lambda: True),
reverse_image=reverse,
expose_scope=False,
storage_preflight={
"orphan_files": ["/srv/private/orphan.png"],
"missing_files": ["meme-1"],
"non_flat_keys": ["nested"],
},
)
)
request = Request({"type": "http", "method": "GET", "path": "/config", "raw_path": b"/config", "query_string": b"", "headers": [], "app": app_stub})
calls: list[tuple[str, str]] = []
def request_scope(_request: Request) -> SimpleNamespace:
"""返回当前请求 scope,供投影测试确认 callback 注入。"""
calls.append(("scope", "config"))
return SimpleNamespace(scope_id="scope-secret")
def service(_request: Request, name: str) -> object:
"""返回 callback 选择的 scope service。"""
calls.append(("service", name))
return reverse
payload = asyncio.run(config_http.config_status(request, request_scope=request_scope, service=service))
assert "scope_id" not in payload
assert payload["embedding_cache_ready"] is True
assert payload["reverse_image_available"] is True
assert payload["runtime_ready"] is True
assert payload["agent_runtime"] == {"mode": "host-runtime-slot-lock", "verified": True, "executor_running": False}
assert payload["storage_preflight"] == {"status": "warning", "orphan_files": 1, "blocking_errors": {"non_flat_keys": 1, "missing_files": 1}}
assert calls == [("service", "reverse_image")]
assert "must-not-leak" not in repr(payload)
assert "/srv/private" not in repr(payload)
app_stub.state.expose_scope = True
visible_payload = asyncio.run(config_http.config_status(request, request_scope=request_scope, service=service))
assert visible_payload["scope_id"] == "scope-secret"
assert calls == [("service", "reverse_image"), ("scope", "config"), ("service", "reverse_image")]