agent-Specialization/test/test_runtime_service.py
JOJO f43b03484d refactor(runtime): 拆除 session_data 兼容桥,任务上下文三层结构化直传
- TaskRecord 去 session_data dict,直存 principal/task_params/directives
  + 可变 goal_progress 字段(__slots__)
- create_chat_task 签名改为收 RuntimeContext 必填,TaskRecord.__init__
  同步精简(身份/消息/模式等全部从 ctx 派生)
- _run_chat_task 身份还原/门闸认领/事件注入/terminal 属性设置
  全部改为三层属性访问;goal_progress 回写改独立可变字段
- RuntimeService.create_task 原样透传 ctx;to_session_data() 删除
- task_public_payload / api.py 单任务载荷改三层读取
- 契约/注释/i18n 文案同步去 session_data 化;测试重写为三层断言

验证:75 测试全量回归,失败恰为 4 项存量(host_workspace_manager /
skills_manager / token_usage_extractor / conversation_workspace_storage)
2026-09-08 02:08:33 +08:00

198 lines
7.8 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.

"""阶段二公共任务入口RuntimeService的显式上下文验收测试。
契约用例docs/runtime_contract.md §7
- T01显式上下文受理无 HTTP 请求、无 test_request_context
- T02同对话并发 chat 互斥 / notice 豁免
- T04取消受理层语义执行线程以 no-op 替身阻断,不触达模型调用)
- 上下文固化受理后三层结构principal/task_params/directives原样落到任务记录
本测试全程不创建 Flask 应用/请求上下文——这本身就是
「公共入口不依赖隐式 Web 环境」的直接证明。
"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from server.runtime import ( # noqa: E402
InternalDirectives,
RuntimeContext,
TaskParams,
TrustedPrincipal,
principal_from_session_snapshot,
runtime_service,
)
from server.tasks import task_manager # noqa: E402
def _make_ctx(conversation_id="conv_test_rt", task_type="chat", username="tester"):
return RuntimeContext(
principal=TrustedPrincipal(
username=username,
workspace_id="default",
role="user",
is_api_user=False,
host_mode=True,
host_workspace_id="default",
preferred_model_key="kimi-test",
preferred_run_mode="fast",
preferred_thinking_mode=False,
),
params=TaskParams(
message="hello",
conversation_id=conversation_id,
run_mode="fast",
task_type=task_type,
),
directives=InternalDirectives(main_task_gate_token="tok123"),
)
class RuntimeContextModelTest(unittest.TestCase):
def test_validate_rejects_empty_username(self):
ctx = _make_ctx()
object.__setattr__(ctx.principal, "username", "") # frozen dataclass 测试绕过
with self.assertRaises(ValueError):
ctx.validate()
def test_context_layers_carry_identity_and_preferences(self):
ctx = _make_ctx()
# 身份与资源范围principal 层)
p = ctx.principal
self.assertEqual(p.username, "tester")
self.assertEqual(p.workspace_id, "default")
self.assertEqual(p.role, "user")
self.assertFalse(p.is_api_user)
self.assertTrue(p.host_mode)
self.assertEqual(p.host_workspace_id, "default")
# 偏好快照层(非本次覆盖)
self.assertEqual(p.preferred_model_key, "kimi-test")
self.assertEqual(p.preferred_run_mode, "fast")
self.assertFalse(p.preferred_thinking_mode)
# 内部指令directives 层)
self.assertEqual(ctx.directives.main_task_gate_token, "tok123")
# 默认不回放事件
self.assertFalse(ctx.directives.auto_user_message_event)
def test_context_directives_fields(self):
ctx = RuntimeContext(
principal=_make_ctx().principal,
params=TaskParams(message="m", conversation_id="c1", approval_timeout_seconds=120),
directives=InternalDirectives(
auto_user_message_event=True,
auto_user_message_payload={"visibility": "chat"},
preceding_user_notices=[{"message": "n1", "payload": {}}],
),
)
d = ctx.directives
self.assertTrue(d.auto_user_message_event)
self.assertEqual(d.auto_user_message_payload, {"visibility": "chat"})
self.assertEqual(len(d.preceding_user_notices), 1)
# 超时透传机制(默认 None显式设置后按层携带
self.assertEqual(ctx.params.approval_timeout_seconds, 120)
def test_principal_from_session_snapshot(self):
snap = {
"username": "u1",
"role": "api",
"is_api_user": True,
"host_mode": False,
"workspace_id": "ws1",
"model_key": "m1",
}
p = principal_from_session_snapshot(snap, "ws1")
self.assertEqual(p.username, "u1")
self.assertEqual(p.role, "api")
self.assertTrue(p.is_api_user)
self.assertFalse(p.host_mode)
self.assertIsNone(p.host_workspace_id)
self.assertEqual(p.preferred_model_key, "m1")
class RuntimeServiceAdmissionTest(unittest.TestCase):
"""受理层行为:用 no-op 替身阻断执行线程,不触达模型与工作区装配。"""
def setUp(self):
self._orig_run = task_manager._run_chat_task
task_manager._run_chat_task = lambda *a, **k: None # 线程即刻结束
self._created = []
def tearDown(self):
task_manager._run_chat_task = self._orig_run
for task_id in self._created:
with task_manager._lock:
task_manager._tasks.pop(task_id, None)
def _create(self, **kw):
rec = runtime_service.create_task(_make_ctx(**kw))
self._created.append(rec.task_id)
return rec
def test_t01_create_task_without_http_context(self):
rec = self._create()
self.assertTrue(rec.task_id)
self.assertEqual(rec.username, "tester")
self.assertEqual(rec.conversation_id, "conv_test_rt")
# 三层上下文经显式入口固化到任务记录,未触碰 Flask session
self.assertEqual(rec.principal.username, "tester")
self.assertEqual(rec.directives.main_task_gate_token, "tok123")
def test_t02_same_conversation_chat_mutex_and_notice_exempt(self):
# 第一个任务保持 running线程 no-op 但 status 已被置 running
rec1 = self._create()
self.assertEqual(rec1.status, "running")
# 同对话第二个 chat 被拒
with self.assertRaises(RuntimeError):
self._create()
# notice 类型豁免互斥
rec2 = self._create(task_type="notice")
self.assertEqual(rec2.task_type, "notice")
# 不同对话不受影响
rec3 = self._create(conversation_id="conv_other")
self.assertTrue(rec3.task_id)
def test_create_task_validates_context(self):
bad = _make_ctx()
object.__setattr__(bad.principal, "workspace_id", "")
with self.assertRaises(ValueError):
runtime_service.create_task(bad)
def test_create_chat_task_requires_explicit_context(self):
# 直调底层入口且不带显式上下文 → 明确拒绝(不再静默读 Flask session
with self.assertRaises(ValueError):
task_manager.create_chat_task(None)
def test_t04_cancel_task(self):
rec = self._create()
ok = runtime_service.cancel_task("tester", rec.task_id)
self.assertTrue(ok)
# 他人不可取消
rec2 = self._create(conversation_id="conv_cancel2")
self.assertFalse(runtime_service.cancel_task("someone_else", rec2.task_id))
def test_get_task_events_offset_protocol(self):
rec = self._create()
task_manager._append_event(rec, "system_message", {"text": "a"})
task_manager._append_event(rec, "text_chunk", {"text": "b"})
events, next_offset, err, meta = runtime_service.get_task_events("tester", rec.task_id, 0)
self.assertIsNone(err)
self.assertEqual([e["idx"] for e in events], [0, 1])
self.assertEqual(next_offset, 2)
# 缺口检测水位:未裁剪时为 0
self.assertEqual(meta, {"window_start": 0})
# offset 续读
events2, next2, _, meta2 = runtime_service.get_task_events("tester", rec.task_id, 1)
self.assertEqual([e["idx"] for e in events2], [1])
self.assertEqual(next2, 2)
self.assertEqual(meta2["window_start"], 0)
# 无权/不存在
events3, _, err3, meta3 = runtime_service.get_task_events("someone_else", rec.task_id, 0)
self.assertIsNone(events3)
self.assertIsNone(meta3)
self.assertTrue(err3)
if __name__ == "__main__":
unittest.main()