agent-Specialization/test/test_runtime_identity_resources.py
JOJO 7d290d1790 refactor(context): server/context.py 拆分为子包,get_user_resources 参数化
- 989 行单文件拆为 server/context/ 9 模块(identity/broadcast/personalization/usage/upload/conversation/resources/decorators/reaper)+ __init__.py 兼容 re-export,外部 import 路径不变
- 新增 RuntimeIdentity 显式身份快照(identity.py),get_user_resources 支持参数化身份驱动,为任务线程拆除 Flask 隐式上下文做准备
- 测试 patch 目标随迁至 server.context.personalization;新增 test_runtime_identity_resources 覆盖 web/host/api 身份路由矩阵

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
Co-authored-by: Codex powered by ChatGPT-6-Astra <codex@example.com>
2026-09-07 16:14:53 +08:00

131 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""get_user_resources 显式身份RuntimeIdentity分支路由回归测试。
契约 docs/runtime_contract.md §7 T10host / web / API 三种身份 × 资源解析,
是阶段二拆桥后的最高风险点is_api_user/host_mode 分支选错会静默串工作区)。
本测试用重 mock 阻断容器/终端/磁盘副作用,聚焦验证:
- identity 模式下全程无 Flask 请求上下文不崩溃(不读 session
- host_mode=True → 宿主机工作区解析路径
- is_api_user=True → api_user_manager而非 user_manager
- web 身份 → user_manager 与正确的工作区 id
"""
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from server.context import RuntimeIdentity, get_user_resources # noqa: E402
def _base_patches():
"""公共 mock容器、terminal 缓存、skills 同步、个性化应用、配额。"""
return [
patch("server.context.resources.state.container_manager"),
patch("server.context.resources._ensure_workspace_skills_synced"),
patch("server.context.resources._apply_workspace_personalization_preferences"),
patch("server.context.resources.get_or_create_usage_tracker", return_value=None),
patch("server.context.resources.WebTerminal"),
]
class GetUserResourcesIdentityTest(unittest.TestCase):
def _run_with_mocks(self, identity, username="tester", workspace_id="default", extra_patches=None):
patches = _base_patches() + (extra_patches or [])
mocks = []
for p in patches:
mocks.append(p.start())
try:
# 缓存命中路径:预置一个对话级 terminal避免 WebTerminal 构造细节
import server.context.resources as resources
fake_terminal = MagicMock()
fake_terminal._reaper_closing = False
term_key = f"host::{workspace_id}::conv_t10" if identity.host_mode else f"{username}::{workspace_id}::conv_t10"
with patch.dict(resources.state.user_terminals, {term_key: fake_terminal}, clear=False):
terminal, workspace = get_user_resources(
username,
workspace_id=workspace_id,
update_session=False,
conversation_id="conv_t10",
identity=identity,
)
return terminal, workspace, mocks
finally:
for p in patches:
p.stop()
def test_host_mode_routes_to_host_workspace(self):
"""host 身份 → resolve_host_workspace 路径,且不需要请求上下文。"""
host_ws = {"workspace_id": "default", "path": "/tmp/host_ws_t10", "label": "默认"}
with patch("server.context.resources.resolve_host_workspace", return_value=(None, host_ws)):
terminal, workspace, _ = self._run_with_mocks(
RuntimeIdentity(host_mode=True, host_workspace_id="default", role="admin")
)
self.assertIsNotNone(terminal)
# host 工作区 username 固定为 "host",路径来自 resolve_host_workspace
# (生产代码对路径做 resolve()macOS 上 /tmp 会解析为 /private/tmp
self.assertEqual(getattr(workspace, "username", None), "host")
self.assertEqual(
str(getattr(workspace, "project_path", "")),
str(Path("/tmp/host_ws_t10").resolve()),
)
def test_api_user_routes_to_api_manager(self):
"""API 身份 → api_user_manager.ensure_workspace绝不走 user_manager。"""
import server.context.resources as resources
api_ws = MagicMock()
api_ws.workspace_id = "ws_api"
api_ws.project_path = Path("/tmp/api_ws")
api_ws.data_dir = Path("/tmp/api_data")
with patch.object(resources.state, "api_user_manager") as api_mgr, \
patch.object(resources.state, "user_manager") as user_mgr:
api_mgr.ensure_workspace.return_value = api_ws
terminal, workspace, _ = self._run_with_mocks(
RuntimeIdentity(is_api_user=True, role="api"),
username="api_tester",
workspace_id="ws_api",
)
api_mgr.ensure_workspace.assert_called_once_with("api_tester", "ws_api")
user_mgr.ensure_user_workspace.assert_not_called()
self.assertIsNotNone(terminal)
def test_web_user_routes_to_user_manager(self):
"""普通网页身份 → user_manager.ensure_user_workspace。"""
import server.context.resources as resources
web_ws = MagicMock()
web_ws.workspace_id = "default"
web_ws.project_path = Path("/tmp/web_ws")
web_ws.data_dir = Path("/tmp/web_data")
with patch.object(resources.state, "user_manager") as user_mgr, \
patch.object(resources.state, "api_user_manager") as api_mgr:
user_mgr.ensure_user_workspace.return_value = web_ws
user_mgr.get_user.return_value = None
user_mgr.list_user_workspaces.return_value = {}
terminal, workspace, _ = self._run_with_mocks(
RuntimeIdentity(is_api_user=False, role="user"),
username="web_tester",
)
user_mgr.ensure_user_workspace.assert_called_once_with("web_tester", "default")
api_mgr.ensure_workspace.assert_not_called()
self.assertIsNotNone(terminal)
def test_identity_mode_never_touches_flask_session(self):
"""identity 模式在无请求上下文下运行全程不抛(不读 session 的直接证明)。"""
import server.context.resources as resources
web_ws = MagicMock()
web_ws.workspace_id = "default"
web_ws.project_path = Path("/tmp/web_ws2")
web_ws.data_dir = Path("/tmp/web_data2")
with patch.object(resources.state, "user_manager") as user_mgr:
user_mgr.ensure_user_workspace.return_value = web_ws
user_mgr.get_user.return_value = None
user_mgr.list_user_workspaces.return_value = {}
# 无 flask app/request context若有任何 session 读取会抛 RuntimeError
terminal, _ws, _mocks = self._run_with_mocks(RuntimeIdentity(role="user"), username="clean_tester")
self.assertIsNotNone(terminal)
if __name__ == "__main__":
unittest.main()