- 新增标准库叶子层 timeouts/gates/quota/sandbox_policy: 审批 3600 与单轮 600 各有名常量与环境覆盖层,已建项 expires_at 永不重算;目录/控制/合并状态门禁统一判定, 目录违规无任何覆盖参数;配额取数与比较唯一来源 (USD 先于无价、命中均为 >=、无价只计 actual 行); /tmp 隔离矩阵与失联清理判定纯函数化。 - 调用点委托:server/worker/automations/auto_memory 的 配额 SQL 与比较、server/tree/marker 的目录解析、 marker 的控制门、sandbox 的任务容器名正则、两处 300 秒 陈旧阈值默认值均收敛,别名与错误文案不变。 - 统一两个 P0 口径分歧:服务端无价计数由全部 usd IS NULL 行改为 actual-only;显式不限额用户在服务端准入亦不受 无价上限约束(与 worker 运行时一致)。 - 不新增门禁种类与 record 模式(主线无此实现);不改 skill_seed/skills/SKILL.md;不改 v1.1 契约与分支保护。 - 新增 test_arch_converge.py 18 项。
345 lines
16 KiB
Python
345 lines
16 KiB
Python
"""架构收敛 A 线单测:超时 / 门禁 / 成本配额 / 沙箱清理。
|
||
|
||
只覆盖本轨新增的四个叶子模块与调用点的委托关系,不跑
|
||
模型、不联网、不碰生产:SQLite 内存库 + 临时目录。
|
||
|
||
钉住的冻结语义(改动这些断言即改语义,必须先改设计):
|
||
- 审批默认 3600、服务端入口 60..86400、已建项 expires_at
|
||
永不因默认值变化而重算;600 秒是单轮护栏,不是审批超时。
|
||
- 目录门无 confirm / override / bypass 参数;合并状态门
|
||
仅 completed 放行;控制门与旧 marker 行为逐字一致。
|
||
- 配额判定唯一来源是 quota.decide:USD 门先于无价门,
|
||
命中均为 >=;无价只计 source='actual',估算行不触发上限;
|
||
无配额行按默认值,显式 NULL 行才是不限额。
|
||
- 失联清理只在 interrupted + 心跳停滞严格超阈时放行;
|
||
容器清理只认 codex-<32 位 hex>,绝不按前缀删。
|
||
"""
|
||
|
||
import inspect
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
from unittest import mock
|
||
|
||
try:
|
||
from cloud import gates, quota, sandbox_policy, timeouts
|
||
from cloud.approvals import DEFAULT_TIMEOUT_SECONDS, create_approval
|
||
from cloud.automations import (
|
||
_automation_quota_exceeded,
|
||
_unpriced_token_cap as _auto_cap,
|
||
)
|
||
from cloud.coordinator import DEFAULT_TIMEOUT_SECONDS as _COORD_TIMEOUT
|
||
from cloud.cost import unpriced_token_cap as _cost_cap
|
||
from cloud.harness2_tasks import (
|
||
DEFAULT_TASK_TIMEOUT_SECONDS as _HARNESS_TIMEOUT,
|
||
)
|
||
from cloud.marker_tools import (
|
||
MAX_POLL_TIMEOUT_SECONDS as _MARKER_POLL_MAX,
|
||
)
|
||
from cloud.marker_tools import check_control_gate, safe_worktree_path
|
||
from cloud.sandbox import DockerExecutor
|
||
from cloud.server import _safe_worktree_path as _server_safe_path
|
||
from cloud.tree import resolve_tree_path
|
||
from cloud.workers import DEFAULT_ALIVE_SECONDS as _WORKER_ALIVE
|
||
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||
from cloud import gates, quota, sandbox_policy, timeouts
|
||
from cloud.approvals import DEFAULT_TIMEOUT_SECONDS, create_approval
|
||
from cloud.automations import (
|
||
_automation_quota_exceeded,
|
||
_unpriced_token_cap as _auto_cap,
|
||
)
|
||
from cloud.coordinator import DEFAULT_TIMEOUT_SECONDS as _COORD_TIMEOUT
|
||
from cloud.cost import unpriced_token_cap as _cost_cap
|
||
from cloud.harness2_tasks import (
|
||
DEFAULT_TASK_TIMEOUT_SECONDS as _HARNESS_TIMEOUT,
|
||
)
|
||
from cloud.marker_tools import (
|
||
MAX_POLL_TIMEOUT_SECONDS as _MARKER_POLL_MAX,
|
||
)
|
||
from cloud.marker_tools import check_control_gate, safe_worktree_path
|
||
from cloud.sandbox import DockerExecutor
|
||
from cloud.server import _safe_worktree_path as _server_safe_path
|
||
from cloud.tree import resolve_tree_path
|
||
from cloud.workers import DEFAULT_ALIVE_SECONDS as _WORKER_ALIVE
|
||
|
||
|
||
def _quota_conn():
|
||
"""配额测试用的最小库:tasks + cost_records + user_quotas。"""
|
||
conn = sqlite3.connect(":memory:")
|
||
conn.row_factory = sqlite3.Row
|
||
conn.execute("CREATE TABLE tasks(id TEXT PRIMARY KEY,"
|
||
" created_by TEXT)")
|
||
conn.execute("CREATE TABLE cost_records(task_id TEXT,"
|
||
" usd_micros INTEGER, input_tokens INTEGER,"
|
||
" output_tokens INTEGER, day TEXT, source TEXT)")
|
||
conn.execute("CREATE TABLE user_quotas(user_id TEXT PRIMARY KEY,"
|
||
" quota_usd_micros BIGINT, updated_at DOUBLE PRECISION)")
|
||
conn.execute("INSERT INTO tasks(id, created_by) VALUES('t1', 'u1')")
|
||
conn.commit()
|
||
return conn
|
||
|
||
|
||
def _cost_row(conn, task_id, usd, inp, out, source, day="2026-10-31"):
|
||
conn.execute(
|
||
"INSERT INTO cost_records(task_id, usd_micros, input_tokens,"
|
||
" output_tokens, day, source) VALUES(?, ?, ?, ?, ?, ?)",
|
||
(task_id, usd, inp, out, day, source))
|
||
conn.commit()
|
||
|
||
|
||
class TimeoutConvergeTestCase(unittest.TestCase):
|
||
def test_constants_match_old_literals(self):
|
||
self.assertEqual(timeouts.APPROVAL_DEFAULT_SECONDS, 3600)
|
||
self.assertEqual(timeouts.APPROVAL_MIN_SECONDS, 60)
|
||
self.assertEqual(timeouts.APPROVAL_MAX_SECONDS, 86400)
|
||
self.assertEqual(timeouts.TURN_SECONDS, 600)
|
||
self.assertEqual(timeouts.STALE_SWEEP_SECONDS, 300)
|
||
self.assertEqual(timeouts.WORKER_ALIVE_SECONDS, 120)
|
||
# 各模块别名与单一来源同值(调用点 import 的名字不变)。
|
||
self.assertEqual(DEFAULT_TIMEOUT_SECONDS, 3600)
|
||
self.assertEqual(_COORD_TIMEOUT, timeouts.LONG_TIMEOUT_SECONDS)
|
||
self.assertEqual(_HARNESS_TIMEOUT, timeouts.LONG_TIMEOUT_SECONDS)
|
||
self.assertEqual(_MARKER_POLL_MAX, timeouts.LONG_TIMEOUT_SECONDS)
|
||
self.assertEqual(_WORKER_ALIVE, timeouts.WORKER_ALIVE_SECONDS)
|
||
|
||
def test_server_validation_keeps_old_bounds_and_message(self):
|
||
for good in (60, 3600, 86400):
|
||
self.assertEqual(timeouts.validate_server_timeout(good), good)
|
||
for bad in (None, True, 59, 86401, 3.5, "3600"):
|
||
with self.assertRaises(ValueError) as ctx:
|
||
timeouts.validate_server_timeout(bad)
|
||
self.assertEqual(str(ctx.exception),
|
||
"timeout_seconds must be 60..86400")
|
||
|
||
def test_module_timeout_semantics(self):
|
||
self.assertEqual(timeouts.resolve_module_timeout(None), 3600)
|
||
self.assertIsNone(timeouts.resolve_module_timeout(0))
|
||
self.assertEqual(timeouts.resolve_module_timeout(60), 60)
|
||
for bad in (-1, True, "60"):
|
||
with self.assertRaises(ValueError):
|
||
timeouts.resolve_module_timeout(bad)
|
||
self.assertIsNone(timeouts.expires_at_for(1000, None))
|
||
self.assertEqual(timeouts.expires_at_for(1000, 3600), 4600)
|
||
|
||
def test_env_override_and_invalid_fallback(self):
|
||
with mock.patch.dict(os.environ,
|
||
{"CLOUD_APPROVAL_TIMEOUT_SECONDS": "1800"}):
|
||
self.assertEqual(timeouts.approval_default_seconds(), 1800)
|
||
with mock.patch.dict(os.environ,
|
||
{"CLOUD_APPROVAL_TIMEOUT_SECONDS": "30"}):
|
||
self.assertEqual(timeouts.approval_default_seconds(), 3600)
|
||
with mock.patch.dict(os.environ,
|
||
{"CLOUD_APPROVAL_TIMEOUT_SECONDS": "abc"}):
|
||
self.assertEqual(timeouts.approval_default_seconds(), 3600)
|
||
|
||
def test_existing_approval_expires_at_never_recomputed(self):
|
||
conn = sqlite3.connect(":memory:")
|
||
conn.row_factory = sqlite3.Row
|
||
try:
|
||
with mock.patch.dict(os.environ, {}, clear=False):
|
||
os.environ.pop("CLOUD_APPROVAL_TIMEOUT_SECONDS", None)
|
||
old = create_approval(conn, "t1", "p1", summary="s",
|
||
now=1000.0)
|
||
self.assertEqual(old["expires_at"], 4600.0)
|
||
with mock.patch.dict(os.environ,
|
||
{"CLOUD_APPROVAL_TIMEOUT_SECONDS": "60"}):
|
||
new = create_approval(conn, "t1", "p1", summary="s",
|
||
now=2000.0)
|
||
self.assertEqual(new["expires_at"], 2060.0)
|
||
# 旧行不受默认值变化影响:不回填、不重算。
|
||
from cloud.approvals import get_approval
|
||
self.assertEqual(get_approval(conn, old["id"])["expires_at"],
|
||
4600.0)
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
class GatesConvergeTestCase(unittest.TestCase):
|
||
def test_path_gate_cases_across_all_wrappers(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
base = os.path.join(tmp, "w")
|
||
os.makedirs(os.path.join(base, "sub"))
|
||
# 目录内指回内部的符号链接放行,指出去的拦住。
|
||
os.symlink(os.path.join(base, "sub"),
|
||
os.path.join(base, "inner-link"))
|
||
os.symlink("/etc", os.path.join(base, "escape-link"))
|
||
for fn in (gates.resolve_inside, resolve_tree_path,
|
||
safe_worktree_path, _server_safe_path):
|
||
self.assertEqual(fn(base, ""), base)
|
||
self.assertEqual(fn(base, "sub"),
|
||
os.path.realpath(os.path.join(base, "sub")))
|
||
self.assertIsNone(fn(base, "../etc/passwd"))
|
||
self.assertIsNone(fn(base, "/etc/passwd"))
|
||
self.assertIsNotNone(fn(base, "inner-link"))
|
||
self.assertIsNone(fn(base, "escape-link"))
|
||
|
||
def test_path_and_control_gates_have_no_bypass_parameter(self):
|
||
for fn in (gates.resolve_inside, gates.decide_control,
|
||
gates.merge_state_decision):
|
||
params = set(inspect.signature(fn).parameters)
|
||
self.assertFalse(params & {"confirm", "override", "bypass",
|
||
"force", "confirmed"})
|
||
|
||
def test_merge_state_gate(self):
|
||
self.assertEqual(gates.merge_state_decision("completed"),
|
||
(True, ""))
|
||
for status in ("queued", "running", "failed", "interrupted",
|
||
"merged", "merge-failed"):
|
||
ok, reason = gates.merge_state_decision(status)
|
||
self.assertFalse(ok)
|
||
self.assertEqual(reason,
|
||
"only completed tasks can be marked merged")
|
||
|
||
def test_control_gate_matches_old_marker_behavior(self):
|
||
sender = {"id": "s1", "project_id": "p1"}
|
||
target = {"id": "t1", "project_id": "p1", "status": "running"}
|
||
self.assertEqual(check_control_gate(target, sender, "descendant",
|
||
"cancel"), (True, ""))
|
||
self.assertEqual(
|
||
check_control_gate(target, sender, "descendant", "retry")[0],
|
||
False)
|
||
target2 = dict(target, status="failed")
|
||
self.assertEqual(check_control_gate(target2, sender, "same_creator",
|
||
"retry"), (True, ""))
|
||
self.assertEqual(
|
||
check_control_gate(target2, sender, "other", "retry")[0], False)
|
||
self.assertEqual(
|
||
check_control_gate(target2, {"id": "t1", "project_id": "p1"},
|
||
"descendant", "cancel")[0], False)
|
||
goal = dict(target2, goal_mode=1)
|
||
self.assertEqual(
|
||
check_control_gate(goal, sender, "descendant", "retry")[0],
|
||
False)
|
||
cross = dict(target2, project_id="p2")
|
||
self.assertEqual(
|
||
check_control_gate(cross, sender, "descendant", "retry")[0],
|
||
False)
|
||
|
||
|
||
class QuotaConvergeTestCase(unittest.TestCase):
|
||
def test_decide_precedence_and_equality(self):
|
||
self.assertIsNone(quota.decide(None, 999999, 999999, 100, "2026-10"))
|
||
self.assertIsNone(quota.decide(1000, 999, 99, 100, "2026-10"))
|
||
both = quota.decide(1000, 1000, 1000, 100, "2026-10")
|
||
self.assertEqual(both["kind"], "usd")
|
||
only_unpriced = quota.decide(1000, 10, 100, 100, "2026-10")
|
||
self.assertEqual(only_unpriced["kind"], "unpriced")
|
||
self.assertIn("quota_exceeded", only_unpriced["message"])
|
||
|
||
def test_quota_for_owner_rows(self):
|
||
conn = _quota_conn()
|
||
try:
|
||
with mock.patch.dict(os.environ, {}, clear=False):
|
||
os.environ.pop("CLOUD_DEFAULT_QUOTA_USD_MICROS", None)
|
||
self.assertEqual(quota.quota_for_owner(conn, "u1"),
|
||
10000000)
|
||
conn.execute("INSERT INTO user_quotas VALUES('u1', NULL, 0)")
|
||
conn.commit()
|
||
self.assertIsNone(quota.quota_for_owner(conn, "u1"))
|
||
conn.execute("UPDATE user_quotas SET quota_usd_micros = 5000"
|
||
" WHERE user_id = 'u1'")
|
||
conn.commit()
|
||
self.assertEqual(quota.quota_for_owner(conn, "u1"), 5000)
|
||
finally:
|
||
conn.close()
|
||
|
||
def test_monthly_usage_actual_only(self):
|
||
conn = _quota_conn()
|
||
try:
|
||
_cost_row(conn, "t1", 300, 10, 5, "actual")
|
||
_cost_row(conn, "t1", None, 100, 50, "actual")
|
||
_cost_row(conn, "t1", None, 999900, 99, "estimated")
|
||
used, unpriced = quota.monthly_usage(conn, "u1", "2026-10")
|
||
self.assertEqual(used, 300)
|
||
# 估算行 999999 token 不计入无价上限口径。
|
||
self.assertEqual(unpriced, 150)
|
||
finally:
|
||
conn.close()
|
||
|
||
def test_automation_gate_uses_actual_only(self):
|
||
conn = _quota_conn()
|
||
try:
|
||
conn.execute("INSERT INTO user_quotas VALUES('u1', 100000000, 0)")
|
||
conn.commit()
|
||
_cost_row(conn, "t1", None, 999900, 99, "estimated")
|
||
with mock.patch.dict(
|
||
os.environ,
|
||
{"CLOUD_QUOTA_UNPRICED_TOKEN_CAP": "1000"}):
|
||
# 估算行再大也不拦。
|
||
self.assertIsNone(_automation_quota_exceeded(conn, "u1"))
|
||
_cost_row(conn, "t1", None, 600, 500, "actual")
|
||
blocked = _automation_quota_exceeded(conn, "u1")
|
||
self.assertIsNotNone(blocked)
|
||
self.assertIn("quota_exceeded", blocked)
|
||
finally:
|
||
conn.close()
|
||
|
||
def test_unpriced_cap_env_alias_matches_cost(self):
|
||
with mock.patch.dict(
|
||
os.environ, {"CLOUD_QUOTA_UNPRICED_TOKEN_CAP": "12345"}):
|
||
self.assertEqual(_auto_cap(), 12345)
|
||
self.assertEqual(_cost_cap(), 12345)
|
||
with mock.patch.dict(
|
||
os.environ, {"CLOUD_QUOTA_UNPRICED_TOKEN_CAP": "junk"}):
|
||
self.assertEqual(_auto_cap(), _cost_cap())
|
||
|
||
|
||
class SandboxPolicyTestCase(unittest.TestCase):
|
||
def test_tmpfs_matrix(self):
|
||
self.assertTrue(sandbox_policy.tmpfs_isolated("docker", "/tmp")[0])
|
||
self.assertTrue(
|
||
sandbox_policy.tmpfs_isolated("docker", "/var/tmp")[0])
|
||
self.assertFalse(
|
||
sandbox_policy.tmpfs_isolated("host-bwrap", "/tmp")[0])
|
||
self.assertFalse(sandbox_policy.tmpfs_isolated("local", "/tmp")[0])
|
||
self.assertFalse(
|
||
sandbox_policy.tmpfs_isolated("docker", "/work")[0])
|
||
|
||
def test_task_container_regex_shared_with_sandbox(self):
|
||
self.assertIs(sandbox_policy.TASK_CONTAINER_RE,
|
||
DockerExecutor._TASK_CONTAINER_RE)
|
||
good = "codex-" + "a" * 32
|
||
self.assertTrue(sandbox_policy.TASK_CONTAINER_RE.match(good))
|
||
for bad in ("codex-postgres", "codex-xyz", "codex-" + "a" * 31,
|
||
"codex-" + "A" * 32):
|
||
self.assertIsNone(sandbox_policy.TASK_CONTAINER_RE.match(bad))
|
||
|
||
def test_cleanup_decision(self):
|
||
now = 10000.0
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("running", 1.0, now, 300)[0],
|
||
False)
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("interrupted", now - 301,
|
||
now, 300)[0], True)
|
||
# 恰等于阈值不杀(旧 _reap:hb >= now - threshold 即跳过)。
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("interrupted", now - 300,
|
||
now, 300)[0], False)
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("interrupted", now - 10,
|
||
now, 300)[0], False)
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("interrupted", None,
|
||
now, 300)[0], True)
|
||
self.assertEqual(
|
||
sandbox_policy.cleanup_decision("interrupted", "junk",
|
||
now, 300)[0], False)
|
||
|
||
def test_worker_address(self):
|
||
self.assertEqual(sandbox_policy.worker_address("my-host-42"),
|
||
("my-host", 42))
|
||
self.assertEqual(sandbox_policy.worker_address("plain"),
|
||
("plain", None))
|
||
self.assertEqual(sandbox_policy.worker_address("host-abc"),
|
||
("host-abc", None))
|
||
self.assertEqual(sandbox_policy.worker_address(""), ("", None))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|