158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
"""运行时审批队列模块单测:cloud/approvals.py。
|
|
|
|
不跑真实模型、不联网:内存 SQLite。覆盖批准放行、拒绝拦截、
|
|
回执留痕、二次决定不覆盖、超时默认拒绝、非法输入、回包映射。
|
|
"""
|
|
|
|
import sqlite3
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from cloud.approvals import (
|
|
command_response,
|
|
create_approval,
|
|
decide_approval,
|
|
elicitation_response,
|
|
ensure_approvals_table,
|
|
expire_overdue,
|
|
get_approval,
|
|
list_approvals,
|
|
)
|
|
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from cloud.approvals import (
|
|
command_response,
|
|
create_approval,
|
|
decide_approval,
|
|
elicitation_response,
|
|
ensure_approvals_table,
|
|
expire_overdue,
|
|
get_approval,
|
|
list_approvals,
|
|
)
|
|
|
|
|
|
class ApprovalsModuleTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.conn = sqlite3.connect(":memory:")
|
|
self.conn.row_factory = sqlite3.Row
|
|
ensure_approvals_table(self.conn)
|
|
self.conn.commit()
|
|
|
|
def tearDown(self):
|
|
self.conn.close()
|
|
|
|
def _mk(self, **kw):
|
|
kw.setdefault("task_id", "task-1")
|
|
kw.setdefault("project_id", "proj-1")
|
|
kw.setdefault("summary", "MCP 服务请求表单输入")
|
|
return create_approval(self.conn, **kw)
|
|
|
|
def test_create_defaults_to_pending_with_timeout(self):
|
|
item = self._mk()
|
|
self.assertEqual(item["status"], "pending")
|
|
self.assertEqual(item["kind"], "elicitation")
|
|
self.assertIsNone(item["decided_by"])
|
|
self.assertIsNone(item["decision"])
|
|
self.assertIsNotNone(item["expires_at"])
|
|
self.assertGreater(item["expires_at"], item["created_at"])
|
|
pending = list_approvals(self.conn, task_id="task-1",
|
|
status="pending")
|
|
self.assertEqual(len(pending), 1)
|
|
|
|
def test_approve_releases_with_receipt(self):
|
|
item = self._mk()
|
|
status, decided = decide_approval(
|
|
self.conn, item["id"], "approved", "user-9")
|
|
self.assertEqual(status, "decided")
|
|
self.assertEqual(decided["status"], "approved")
|
|
self.assertEqual(decided["decision"], "approved")
|
|
self.assertEqual(decided["decided_by"], "user-9")
|
|
self.assertIsNotNone(decided["decided_at"])
|
|
self.assertEqual(elicitation_response(decided["decision"]),
|
|
{"action": "accept", "content": {}})
|
|
self.assertEqual(command_response(decided["decision"]),
|
|
{"decision": "accept"})
|
|
|
|
def test_deny_blocks_with_receipt(self):
|
|
item = self._mk()
|
|
status, decided = decide_approval(
|
|
self.conn, item["id"], "denied", "user-9")
|
|
self.assertEqual(status, "decided")
|
|
self.assertEqual(decided["status"], "denied")
|
|
self.assertEqual(decided["decision"], "denied")
|
|
self.assertEqual(decided["decided_by"], "user-9")
|
|
self.assertEqual(elicitation_response(decided["decision"]),
|
|
{"action": "decline"})
|
|
self.assertEqual(command_response(decided["decision"]),
|
|
{"decision": "decline"})
|
|
|
|
def test_second_decision_never_overwrites_first(self):
|
|
item = self._mk()
|
|
decide_approval(self.conn, item["id"], "approved", "user-9")
|
|
status, kept = decide_approval(
|
|
self.conn, item["id"], "denied", "user-7")
|
|
self.assertEqual(status, "already_decided")
|
|
self.assertEqual(kept["status"], "approved")
|
|
self.assertEqual(kept["decided_by"], "user-9")
|
|
|
|
def test_expire_overdue_defaults_to_deny_not_allow(self):
|
|
item = self._mk(now=1000.0, timeout_seconds=60)
|
|
self.assertEqual(item["expires_at"], 1060.0)
|
|
count = expire_overdue(self.conn, now=1061.0)
|
|
self.assertEqual(count, 1)
|
|
done = get_approval(self.conn, item["id"])
|
|
self.assertEqual(done["status"], "expired")
|
|
self.assertEqual(done["decision"], "denied")
|
|
self.assertEqual(done["decided_by"], "system-timeout")
|
|
self.assertEqual(elicitation_response(done["decision"]),
|
|
{"action": "decline"})
|
|
status, kept = decide_approval(
|
|
self.conn, item["id"], "approved", "user-9")
|
|
self.assertEqual(status, "already_decided")
|
|
self.assertEqual(kept["status"], "expired")
|
|
|
|
def test_not_yet_due_is_not_expired(self):
|
|
self._mk(now=1000.0, timeout_seconds=3600)
|
|
self.assertEqual(expire_overdue(self.conn, now=2000.0), 0)
|
|
self.assertEqual(len(list_approvals(self.conn, status="pending")),
|
|
1)
|
|
|
|
def test_decide_missing_is_not_found(self):
|
|
status, item = decide_approval(self.conn, "nope", "approved",
|
|
"user-9")
|
|
self.assertEqual(status, "not_found")
|
|
self.assertIsNone(item)
|
|
|
|
def test_invalid_inputs_raise_value_error(self):
|
|
with self.assertRaises(ValueError):
|
|
self._mk(kind="bogus")
|
|
with self.assertRaises(ValueError):
|
|
self._mk(summary=" ")
|
|
with self.assertRaises(ValueError):
|
|
self._mk(summary="x" * 2001)
|
|
with self.assertRaises(ValueError):
|
|
self._mk(timeout_seconds=-1)
|
|
with self.assertRaises(ValueError):
|
|
decide_approval(self.conn, "x", "maybe", "user-9")
|
|
|
|
def test_ensure_is_idempotent_and_keeps_rows(self):
|
|
item = self._mk()
|
|
ensure_approvals_table(self.conn)
|
|
self.conn.commit()
|
|
self.assertEqual(get_approval(self.conn, item["id"])["id"],
|
|
item["id"])
|
|
|
|
def test_response_mapping_never_defaults_to_allow(self):
|
|
for weird in ("expired", "bogus", None, ""):
|
|
self.assertEqual(elicitation_response(weird),
|
|
{"action": "decline"})
|
|
self.assertEqual(command_response(weird),
|
|
{"decision": "decline"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|