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

+66
View File
@@ -262,6 +262,72 @@ class RetryAndBudgetTest(unittest.TestCase):
os.path.join(tmp, "host-run.log")))
self.assertIn("t1 failed", Path(notify_log).read_text())
def test_notify_failure_quotes_shell_metacharacters(self):
# 任务名 / 文本里的 shell 元字符必须被 quote,不能当命令执行:
# 未加 quote 时 task 名里的 `; touch pwn` 会真的建出文件。
with tempfile.TemporaryDirectory() as tmp:
state_path = os.path.join(tmp, "host-state.json")
notify_log = os.path.join(tmp, "notify.log")
pwn = os.path.join(tmp, "pwn")
pwn2 = os.path.join(tmp, "pwn2")
cfg = {"notify_command":
f"echo {{task}} {{status}} {{text}} >> {notify_log}"}
host.notify_failure(
state_path, cfg, "t1; touch " + pwn,
{"status": "failed", "text": "$(touch " + pwn2 + ")"})
self.assertFalse(os.path.exists(pwn))
self.assertFalse(os.path.exists(pwn2))
self.assertIn("t1; touch", Path(notify_log).read_text())
def test_notify_failure_bad_template_is_logged_not_swallowed(self):
with tempfile.TemporaryDirectory() as tmp:
state_path = os.path.join(tmp, "host-state.json")
notify_log = os.path.join(tmp, "notify.log")
cfg = {"notify_command": f"echo {{unknown}} >> {notify_log}"}
host.notify_failure(state_path, cfg, "t1",
{"status": "failed", "text": "boom"})
# 模板错了:不跑命令(notify.log 不应出现),但必须
# 在 host-run.log 里留下 template error,而不是静默吞掉。
self.assertFalse(os.path.exists(notify_log))
self.assertIn("notify command template error",
Path(os.path.join(tmp, "host-run.log")).read_text())
def test_notify_failure_command_failure_is_logged(self):
with tempfile.TemporaryDirectory() as tmp:
state_path = os.path.join(tmp, "host-state.json")
cfg = {"notify_command": "exit 3"}
host.notify_failure(state_path, cfg, "t1",
{"status": "failed", "text": "boom"})
self.assertIn("notify command rc=3",
Path(os.path.join(tmp, "host-run.log")).read_text())
def test_goal_budget_stop_notifies_budget_exceeded(self):
class FakeServer:
def run_turn(self, *args, **kwargs):
raise AssertionError("预算已满,不应再跑回合")
with tempfile.TemporaryDirectory() as tmp:
state_path = os.path.join(tmp, "host-state.json")
notify_log = os.path.join(tmp, "notify.log")
today = __import__("time").strftime("%Y-%m-%d")
state = {"tasks": {}, "automations": [],
"usage": {"date": today, "turns": 5}}
task = {"name": "g", "thread_id": "x", "cwd": tmp,
"sandbox": None, "max_retries": 0, "retry_count": 0,
"turns": []}
state["tasks"]["g"] = task
cfg = {"max_daily_turns": 5,
"notify_command":
f"echo {{task}} {{status}} >> {notify_log}"}
result = host.run_goal_loop(FakeServer(), state, state_path,
task, "make it", "true", 3, "m",
"low", None, cfg=cfg, timeout=600)
self.assertFalse(result["ok"])
self.assertTrue(result["budget_stopped"])
self.assertEqual(task["status"], "interrupted")
self.assertIn("budget-exceeded",
Path(notify_log).read_text())
class TurnErrorFromHistoryTest(unittest.TestCase):
def _server_with_history(self, tmp, error_json):