feat: 成本预算与失败通知第一批(G1/G2/G5/G6/无价上报)

- G1:修正 _check_quota docstring(Local popen 30 秒轮询掐断、
  Docker/同步路径只有边界检查),README 配额节同步;followup
  创建不查配额是已声明设计,保持不动。
- G2:notify_failure 对插值 shlex.quote,模板错误记日志不静默
  吞,命令返回值/超时/异常记 host-run.log;dispatch 前置预算门
  与 goal 预算中断补 budget-exceeded 通知(没配命令时只写日志)。
- G5/G6:重试与续跑的配额检查按任务创建者查(费用记创建者名下),
  维护者替他人操作不再用错人的额度;续跑 API 侧补查,超限回
  402 且不改状态,不再先 200 排队再悄悄 interrupted。
- 无价上报:配额自查/管理端点返回增加 unpriced_actual_tokens
  (actual 且 usd_micros 为 NULL,与 CostStore 口径一致),只读
  上报、不计价、不拦任务;不给 gpt-5.6-terra 编造价格。
测试:host 68、cloud 760(4 跳过)、deploy 19 全过。
This commit is contained in:
host committed 2026-10-27 10:13:51 +00:00
1 parent c2f82da5e0
commit e4b5545a24
5 files changed
+304 -38

No files matched your search

+89
View File
@@ -234,6 +234,95 @@ class ServerQuotasTestCase(unittest.TestCase):
self.assertEqual(s, 200, retried)
self.assertEqual(retried["status"], "queued")
def test_retry_uses_creator_quota_not_operator(self):
# 重试按任务创建者查配额:费用记创建者名下,操作人
# 是谁不应影响准入。other 须是 maintainer 才能过
# approve_merge 权限门,否则 403 会掩盖配额判断。
self.auth.set_member(self.project_id, self.owner_id,
self.other_id, "maintainer")
s, task = self._create_task()
self.assertEqual(s, 201, task)
_insert_cost(self.db_path, task["id"], self.project_id,
2000000, _utc_today())
_set_task_status(self.db_path, task["id"], "failed")
# 创建者超限:维护者替他重试也应拦。
s, _ = self._set_quota(self.owner_id, 2000000)
self.assertEqual(s, 200)
s, body = _req(self.port, "POST",
"/tasks/%s/retry" % task["id"], {},
self.other_key)
self.assertEqual(s, 402, body)
# 创建者不限额、操作人额度为 0:按旧口径(查操作人)
# 会拦,按创建者口径应放行。
s, _ = self._set_quota(self.owner_id, None)
self.assertEqual(s, 200)
s, _ = self._set_quota(self.other_id, 0)
self.assertEqual(s, 200)
s, retried = _req(self.port, "POST",
"/tasks/%s/retry" % task["id"], {},
self.other_key)
self.assertEqual(s, 200, retried)
self.assertEqual(retried["status"], "queued")
def test_continue_blocked_at_creator_quota(self):
s, task = self._create_task()
self.assertEqual(s, 201, task)
_insert_cost(self.db_path, task["id"], self.project_id,
5000000, _utc_today())
_set_task_status(self.db_path, task["id"], "completed")
s, _ = self._set_quota(self.owner_id, 5000000)
self.assertEqual(s, 200)
# 以前:先回 200 排队、随后认领门悄悄标 interrupted。
# 现在:API 侧直接 402,状态保持 completed 不变。
s, body = _req(self.port, "POST",
"/tasks/%s/continue" % task["id"],
{"message": "again"}, self.owner_key)
self.assertEqual(s, 402, body)
s, row = _req(self.port, "GET", "/tasks/" + task["id"],
key=self.owner_key)
self.assertEqual(row["status"], "completed")
# 额度调高后放行。
s, _ = self._set_quota(self.owner_id, None)
self.assertEqual(s, 200)
s, continued = _req(self.port, "POST",
"/tasks/%s/continue" % task["id"],
{"message": "again"}, self.owner_key)
self.assertEqual(s, 200, continued)
self.assertEqual(continued["status"], "queued")
# 续跑同样按创建者查:操作人额度为 0、创建者不限额时放行。
self.auth.set_member(self.project_id, self.owner_id,
self.other_id, "maintainer")
s, task2 = self._create_task()
self.assertEqual(s, 201, task2)
_set_task_status(self.db_path, task2["id"], "completed")
s, _ = self._set_quota(self.other_id, 0)
self.assertEqual(s, 200)
s, continued2 = _req(self.port, "POST",
"/tasks/%s/continue" % task2["id"],
{"message": "again"}, self.other_key)
self.assertEqual(s, 200, continued2)
self.assertEqual(continued2["status"], "queued")
def test_quota_reports_unpriced_tokens(self):
s, task = self._create_task()
self.assertEqual(s, 201, task)
# 有价 5000000 计入 used;无价 actual 150 只进
# unpriced_actual_tokens,不计价、不拦任务。
_insert_cost(self.db_path, task["id"], self.project_id,
5000000, _utc_today())
_insert_cost(self.db_path, task["id"], self.project_id,
None, _utc_today())
s, q = _req(self.port, "GET", "/users/me/quota",
key=self.owner_key)
self.assertEqual(s, 200, q)
self.assertEqual(q["used_usd_micros"], 5000000)
self.assertEqual(q["unpriced_actual_tokens"], 150)
s, q = _req(self.port, "GET",
"/admin/quotas?user_id=" + self.owner_id,
key=self.owner_key)
self.assertEqual(s, 200, q)
self.assertEqual(q["unpriced_actual_tokens"], 150)
if __name__ == "__main__":
unittest.main()