Files
codex-app-server/tests/test_host_continue.py
host 3a4997d346 feat: 旧任务继续会话——终端任务带新消息 + 全部选项同一会话续跑
server:POST /tasks/{id}/continue(终端非 merged 任务重排队,
resume_message 列存本轮消息,不传选项沿用原值,plan/goal 冲突
与新建一致,goal_mode=1 时转新 goal 语义;CAS 防并发改状态,
审计 task_continued);tasks.resume_message 三处迁移 + PG 建表。
worker:认领后本轮 prompt 换成续跑消息(只换内存副本),
goal 续跑走 goal-resume 而不是 goal,跑完清 resume_message
(进程被杀时不清,下次认领重跑同一条消息)。
host:goal-resume --goal-text(旧任务接续 goal 模式:跳过状态
与记录守卫,旧线程上从第 1 轮开新目标循环,新语义不限轮次;
无线程时拒绝)。
sandbox:Local/Docker goal_resume_task + popen goal-resume kind;
附带 popen goal 命令改成新语义(无验收不传 --verify,轮次 0
原样传,此前 goal 任务到本地 worker 会直接报错)。
console:详情页终端非 merged 任务出继续会话表单(消息 +
模型/强度/权限/计划/goal,模型下拉复用中转列表)。
测试:新增 test_task_continue.py 11 个 + test_host_continue.py 2 个。
2026-10-27 04:40:11 +08:00

87 lines
3.0 KiB
Python

"""旧任务接续 goal 模式:host.py goal-resume --goal-text。
已终局的旧任务没有 goal 记录是正常情况,--goal-text 模式跳过
状态与记录守卫,在旧线程上从第 1 轮开新目标循环(新语义:
无验收、不传轮次时不限轮次,模型宣告完成才结束)。
"""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
import host
class FakeServer(object):
def __init__(self, text):
self.text = text
self.resumed = []
def initialize(self):
pass
def close(self):
pass
def resume_thread(self, thread_id):
self.resumed.append(thread_id)
def run_turn(self, thread_id, prompt, model=None, effort=None,
sandbox=None, timeout=None):
return {"status": "completed", "text": self.text,
"turn_id": "u1"}
class GoalResumeTextTest(unittest.TestCase):
def _run_main(self, task, goal_text):
with tempfile.TemporaryDirectory() as tmp:
state_path = os.path.join(tmp, "host-state.json")
cfg_path = os.path.join(tmp, "host-config.json")
with open(state_path, "w", encoding="utf-8") as f:
json.dump({"tasks": {"g": task}, "automations": []}, f)
with open(cfg_path, "w", encoding="utf-8") as f:
json.dump({}, f)
fake = FakeServer("done\n目标已达成")
argv = ["host.py", "--state", state_path,
"--config", cfg_path,
"goal-resume", "--task", "g"]
if goal_text is not None:
argv += ["--goal-text", goal_text]
with mock.patch.object(sys, "argv", argv):
with mock.patch.object(host, "Server",
lambda **kw: fake):
host.main()
with open(state_path, encoding="utf-8") as f:
saved = json.load(f)["tasks"]["g"]
return saved, fake
def test_goal_text_init_on_completed_task(self):
task = {"name": "g", "thread_id": "x", "cwd": "/tmp",
"sandbox": None, "max_retries": 0, "retry_count": 0,
"turns": [], "status": "completed"}
saved, fake = self._run_main(task, "new goal")
self.assertEqual(saved["status"], "goal-met")
self.assertEqual(saved["goal"], "new goal")
self.assertEqual(saved["goal_rounds_done"], 1)
self.assertEqual(fake.resumed, ["x"])
def test_goal_text_refuses_task_without_thread(self):
task = {"name": "g", "cwd": "/tmp",
"sandbox": None, "max_retries": 0, "retry_count": 0,
"turns": [], "status": "completed"}
saved, fake = self._run_main(task, "new goal")
self.assertEqual(saved["status"], "completed")
self.assertEqual(fake.resumed, [])
if __name__ == "__main__":
unittest.main()