feat(docker): 超时不再只剩一句 docker run timeout,留存 partial 与容器日志

2026-10-29 任务 A 教训的收尾:超时路径直接 kill+rm,
--rm 容器一删日志就没了,事后只剩一句 timeout,复发无法
定位(#157 只补了 worker 终态审计,没补容器侧证据)。
修法:_docker_timeout_evidence 先 kill 不 rm,抓 docker
logs 尾 200 行,再 rm;同时取 TimeoutExpired 自带的
partial stdout/stderr(旧代码直接丢了)。三步都 best-effort,
任一步失败都不影响返回 failed。dispatch 与三条 goal 路径
(goal/goal-resume/goal-resume-breakpoint)四处超时点同款
处理:stdout = timeout + partial(优先)或 logs,另存
container_log_tail 与 timeout_partial_stdout 字段;--rm
命令本身不动,不留僵尸容器。
单测 test_docker_sandbox 31 项(含新增超时证据 1 项,
在两个测试类里各跑一次)。
This commit is contained in:
host committed 2026-10-30 02:47:11 +08:00
1 parent 700641fa37
commit 6e78449994
2 files changed
+132 -15

No files matched your search

+35
View File
@@ -218,6 +218,41 @@ class DockerCommandTestCase(unittest.TestCase):
self.assertIn(("rm", "-f"), [(c[1], c[2]) for c in seen
if len(c) > 2])
def test_timeout_keeps_partial_stdout_and_container_logs(self):
# 旧超时路径只剩一句 'docker run timeout':TimeoutExpired
# 自带的 partial stdout 与 kill 后抓的 docker logs 尾都
# 被丢了。新路径:先 kill 不 rm,抓 logs,再 rm。
seen = []
def fake(cmd, timeout=None):
seen.append(list(cmd))
if len(cmd) > 1 and cmd[1] == "run":
exc = subprocess.TimeoutExpired(cmd, timeout)
exc.stdout = "partial-host-output"
exc.stderr = "partial-err"
raise exc
if len(cmd) > 1 and cmd[1] == "logs":
return SimpleNamespace(returncode=0,
stdout="container-tail-log",
stderr="")
return SimpleNamespace(returncode=0, stdout="", stderr="")
with mock.patch("cloud.sandbox.shutil.which",
return_value="/usr/bin/docker"):
ex = DockerExecutor("/fake/host.py", runner=fake,
env={"XCM_API_KEY": "placeholder"},
timeout=5)
result = ex.dispatch_task(
{"id": "t10", "prompt": "slow", "worktree": "/tmp/wt"})
self.assertEqual(result["status"], "failed")
self.assertIn("partial-host-output", result["stdout"])
# partial 已有内容时 stdout 优先用 partial,logs 另存字段。
self.assertEqual(result["container_log_tail"],
"container-tail-log")
# 顺序:kill → logs → rm,logs 必须在 rm 之前抓到。
kinds = [c[1] for c in seen if len(c) > 1]
self.assertLess(kinds.index("logs"), kinds.index("rm"))
def test_kill_container_calls_kill_and_rm(self):
seen = []
with mock.patch("cloud.sandbox.shutil.which",