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:
1 parent
c2f82da5e0
commit
e4b5545a24
5 files changed
+304
-38
No files matched your search
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user