import unittest from agentd.core import classify_failure, classify_first_line, parse_budget from agentd.executors import ExecResult from agentd.executors.claude_code import parse_output from agentd.plane import brief_to_html, html_to_text, text_to_html class FirstLineTest(unittest.TestCase): def test_tags(self): self.assertEqual(classify_first_line("[review] done\nmore"), "review") self.assertEqual(classify_first_line("\n\n [Question] which one?"), "question") self.assertEqual(classify_first_line("**[blocked]** no device"), "blocked") self.assertEqual(classify_first_line("[progress]"), "progress") def test_edge_cases(self): self.assertEqual(classify_first_line("\ufeff[review] ok"), "review") self.assertEqual(classify_first_line("```\n[progress] still going\n```"), "progress") self.assertEqual(classify_first_line("```text\n[blocked] no device\n```"), "blocked") self.assertEqual(classify_first_line("---\n[question] which?"), "question") self.assertEqual(classify_first_line("[progress]: step 2 of 5, next: tests"), "progress") self.assertEqual(classify_first_line("> `[review]` merged"), "review") self.assertIsNone(classify_first_line("note first\n[review] later")) # tag must lead msg = html_to_text(text_to_html("[codex] 第 1 轮结束", pre="\n [review] ok\n")) self.assertEqual(msg, "[codex] 第 1 轮结束\n\n [review] ok") #
-wrapped body keeps its lines
self.assertEqual(classify_first_line(html_to_text("\n[progress] x
")), "progress")
def test_untagged(self):
self.assertIsNone(classify_first_line("done, see [review] below"))
self.assertIsNone(classify_first_line("[done] finished"))
self.assertIsNone(classify_first_line(""))
self.assertIsNone(classify_first_line(None))
class HtmlTest(unittest.TestCase):
def test_html_to_text(self):
self.assertEqual(html_to_text("a & b
c
d
"), "a & b\nc\nd")
self.assertEqual(html_to_text("x < y\nz"), "x < y\nz") self.assertEqual(html_to_text(None), "") self.assertEqual(html_to_text("
a
\n\n\n\nb
"), "a\n\nb") def test_text_to_html_escapes(self): self.assertEqual(text_to_html("a\nc", pre="a<b>
c
<x>") self.assertEqual(html_to_text(text_to_html("[worker] hi & bye")), "[worker] hi & bye") def test_brief(self): self.assertEqual(brief_to_html("
x
"), "x
") self.assertEqual(brief_to_html("a < b"), "a < b") class FailureTest(unittest.TestCase): def kind(self, tail, exit_code=1, timed_out=False, cfg=None): return classify_failure(ExecResult("", exit_code, None, [], tail), timed_out, cfg or {}) def test_transient_patterns(self): for tail in ("request timed out", "Connection reset by peer", "connection refused", "kex_exchange_identification: banner exchange: Connection to UNKNOWN port 65535: invalid format", "HTTP 429 Too Many Requests", "unexpected status 502 Bad Gateway", "503 Service Unavailable", "stream disconnected before completion"): self.assertEqual(self.kind(tail), ("crash", True), tail) def test_not_transient(self): self.assertEqual(self.kind("SyntaxError: invalid syntax"), ("crash", False)) self.assertEqual(self.kind("", exit_code=0), ("logic", False)) def test_timeout_and_env(self): self.assertEqual(self.kind("whatever", timed_out=True), ("timeout", False)) self.assertEqual(self.kind("window lost focus", cfg={"env_patterns": ["lost focus"]}), ("env", False)) def test_logic_is_never_transient_and_prose_is_ignored(self): self.assertEqual(self.kind("HTTP 429 Too Many Requests", exit_code=0), ("crash", True)) r = ExecResult("tests timed out", 1, None, [], "") self.assertEqual(classify_failure(r, False, {}), ("crash", False)) r = ExecResult("foreground window lost", 1, None, [], "") self.assertEqual(classify_failure(r, False, {"env_patterns": ["window lost"]}), ("env", False)) def test_custom_transient_patterns_replace_defaults(self): self.assertEqual(self.kind("timed out", cfg={"transient_patterns": ["flaky"]}), ("crash", False)) self.assertEqual(self.kind("flaky relay", cfg={"transient_patterns": ["flaky"]}), ("crash", True)) def test_budget(self): self.assertEqual(parse_budget("budget:2h"), 2) self.assertEqual(parse_budget("budget:30m"), 0.5) self.assertEqual(parse_budget("budget:1.5"), 1.5) self.assertIsNone(parse_budget("budget:soon")) class ClaudeOutputTest(unittest.TestCase): def test_parse(self): obj = parse_output('{"type":"result","subtype":"success","is_error":false,"result":"[review] ok","session_id":"s1"}') self.assertEqual((obj["result"], obj["session_id"]), ("[review] ok", "s1")) self.assertEqual(parse_output('noise\n{"result":"x","session_id":"s2"}')["session_id"], "s2") self.assertEqual(parse_output(""), {}) if __name__ == "__main__": unittest.main()