Files
jj.wang efbc5f6e27 Core: cancel moved runs, retry bookkeeping, progress cap, safer classification
- A run whose item leaves In Progress is killed, gets a [worker] cancel comment,
  frees its terminal slot and serialize group and keeps its thread id; if the
  kill fails the slot stays taken and the cancel is retried next tick.
- Transient retries are the same logical round: they do not count toward
  max_rounds_per_thread and do not trigger a max-rounds rebuild.
- New project setting max_progress_rounds (default 12): after that many
  consecutive [progress] rounds the item goes to Waiting (answerer) with a note.
- retry/rebuild markers are consumed only after the run launched, so a failed
  start no longer leaves an In Progress item that is never picked again; a
  rebuild is no longer reported as a retry.
- State is saved after every finished round and every launched run.
- Failure classification: transient patterns only look at the tail, and an
  exit-0 failure is logic (never retried) unless the tail shows a transient error.
- First-line tag: tolerate BOM and a leading code fence / rule line.
- Start comment notes when a handoff summary was attached.
2026-10-21 07:13:18 +00:00

59 lines
2.7 KiB
Python

import unittest
from agentd.core import build_prompt, handoff_summary
ISSUE = {"name": "T-1 do it", "description_html": "<p>goal &amp; scope</p>"}
COMMENTS = [
{"id": "c1", "created_at": "t1", "comment_html": "<p>please also check X</p>"},
{"id": "c2", "created_at": "t2", "comment_html": "<p>[worker] 第 1 轮开始</p>"},
{"id": "c3", "created_at": "t3", "comment_html": "<p>[codex] 第 1 轮结束</p><pre>[question] A or B?</pre>"},
{"id": "c4", "created_at": "t4", "comment_html": "<p>B, please</p>"},
]
class PromptTest(unittest.TestCase):
def test_fresh_prompt_has_everything_but_worker_comments(self):
p = build_prompt("GW-1", ISSUE, COMMENTS, set(), False, protocol="PROTO", intro="INTRO")
self.assertTrue(p.startswith("INTRO"))
self.assertIn("## 协议\nPROTO", p)
self.assertIn("## 工作项 GW-1: T-1 do it\n\ngoal & scope", p)
self.assertIn("please also check X", p)
self.assertIn("[question] A or B?", p)
self.assertNotIn("[worker]", p)
self.assertTrue(p.rstrip().endswith("不要等待人工回复。"))
def test_resumed_prompt_only_new_comments(self):
p = build_prompt("GW-1", ISSUE, COMMENTS, {"c1", "c2", "c3"}, True, protocol="PROTO", intro="INTRO")
self.assertNotIn("PROTO", p)
self.assertNotIn("INTRO", p)
self.assertNotIn("please also check X", p)
self.assertIn("B, please", p)
self.assertIn("同一线程", p)
def test_resumed_without_new_comments_asks_to_continue(self):
p = build_prompt("GW-1", ISSUE, COMMENTS, {"c1", "c2", "c3", "c4"}, True)
self.assertIn("没有新评论", p)
p = build_prompt("GW-1", ISSUE, COMMENTS, {"c1", "c2", "c3", "c4"}, True, retry_note="RETRY")
self.assertIn("RETRY", p)
self.assertNotIn("没有新评论", p)
def test_new_comment_filter_is_by_id_not_timestamp(self):
late_but_old = [{"id": "c9", "created_at": "t0", "comment_html": "<p>clock-skewed reply</p>"},
{"id": "c1", "created_at": "t9", "comment_html": "<p>edited old one</p>"}]
p = build_prompt("GW-1", ISSUE, late_but_old, {"c1"}, True)
self.assertIn("clock-skewed reply", p)
self.assertNotIn("edited old one", p)
def test_handoff(self):
rec = {"runs": 3, "tags": ["progress", "progress", "question"], "last_message": "[question] A or B?"}
h = handoff_summary(rec, "线程已续 3 轮")
self.assertIn("3 轮", h)
self.assertIn("progress, progress, question", h)
self.assertIn("[question] A or B?", h)
p = build_prompt("GW-1", ISSUE, COMMENTS, set(), False, handoff=h)
self.assertIn("## 交接摘要\n" + h, p)
if __name__ == "__main__":
unittest.main()