resurrect.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. """The resurrect bundle (goal 8 task 4): standalone (no-project) tasks
  2. and comment-resurrected terminal items.
  3. - A task created with project_id 0 dispatches with EMPTY co-developed
  4. and reference arrays, the "standalone" project name, and completes on
  5. its completion report.
  6. - A human comment on a TERMINAL goal (complete) re-opens the goal, its
  7. run task returns to suspended-offerable, and the resuming assignment
  8. carries the comment + the re-open line in its resume notice.
  9. - A human comment on a TERMINAL plain task re-queues it, the next
  10. assignment carries the comment.
  11. - A terminal goal-owned WORKER task keeps comments closed (the page
  12. hides the composer; the service path is the goal's alone).
  13. """
  14. from harness import * # noqa: F401,F403 — the shared wire helpers
  15. def run(ctx):
  16. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  17. def create_goal(fields):
  18. act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
  19. rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
  20. if g["title"] == fields["title"]]
  21. return int(rows[0]["id"])
  22. def run_task_of(goal_id):
  23. conn = db()
  24. try:
  25. row = conn.execute(
  26. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  27. (goal_id,)).fetchone()
  28. return int(row[0]) if row else None
  29. finally:
  30. conn.close()
  31. print("== standalone task: created with no project, dispatched, done ==")
  32. state = slot_public(auth, "/tasks", "tasks")
  33. act(auth, state["create"], {
  34. "project_id": "0",
  35. "title": "Standalone research",
  36. "description": "Summarise the fleet's flaky tests.",
  37. "priority": "3",
  38. })
  39. standalone = task_id_of("Standalone research")
  40. detail = task_state(auth, standalone)
  41. check(detail["status"] == "draft" and int(detail["project_id"]) == 0,
  42. "a project_id 0 task creates in draft (project_id %s)" % detail["project_id"])
  43. page = http_get_page(auth, "/tasks/%d" % standalone)
  44. # Goal 41 T5: the context line is the breadcrumb trail now; a
  45. # standalone task carries NO project crumb (and no project link).
  46. crumbs = detail.get("crumbs", [])
  47. check(len(crumbs) == 2 and crumbs[0]["label"] == "Tasks"
  48. and crumbs[1]["href"] is None
  49. and "#%d " % standalone in crumbs[1]["label"],
  50. "a standalone task's crumbs are Tasks / #id (no project crumb): %r" % crumbs)
  51. check('class="breadcrumb"' in page and "/projects/" not in page,
  52. "the task page renders the breadcrumb trail, not a project link")
  53. check("(standalone)" in http_get_page(auth, "/tasks"),
  54. "the tasks list still marks the standalone task")
  55. check("No project (standalone)" in http_get_page(auth, "/tasks"),
  56. "the create form offers the No project (standalone) option")
  57. enqueue_task(auth, standalone)
  58. check(wait_for(lambda: len(fake.start_payloads) > 0 or False,
  59. timeout=30, what="standalone /start push") is not None,
  60. "the dispatcher pushes the standalone task to the fake saddle")
  61. payload = fake.start_payloads[-1]
  62. task_obj = payload.get("task") or {}
  63. check(task_obj.get("project_name") == "standalone",
  64. "the standalone assignment names the project 'standalone' (%s)"
  65. % task_obj.get("project_name"))
  66. check(task_obj.get("co_developed") == [] and task_obj.get("reference") == [],
  67. "the standalone assignment ships EMPTY co-developed and reference arrays")
  68. check(task_obj.get("branch_from", "x") == "",
  69. "the standalone assignment carries no branch_from")
  70. check(wait_task_done(auth, standalone) is not None,
  71. "the standalone task completes on its report")
  72. print("== terminal plain task: a comment re-queues it ==")
  73. plain = create_queued(auth, {
  74. "project_id": "1", "title": "Resurrect me",
  75. "description": "A plain project task that finishes.", "priority": "4"})
  76. check(wait_task_done(auth, plain) is not None, "the plain task completes")
  77. detail = task_state(auth, plain)
  78. act(auth, detail["comment"], {"body": "Please also cover the retry path."})
  79. detail = task_state(auth, plain)
  80. check(detail["status"] == "queued",
  81. "the comment re-queued the terminal plain task (%s)" % detail["status"])
  82. check(any("cover the retry path" in c["body"] for c in detail["comments"]),
  83. "the resurrecting comment rides the thread")
  84. conn = db()
  85. summary_kept = conn.execute(
  86. "select summary from tasks where id = ?", (plain,)).fetchone()[0]
  87. conn.close()
  88. check(summary_kept and len(summary_kept) > 0,
  89. "the earlier round's completion summary survives the re-open")
  90. check(wait_for(lambda: len(fake.start_payloads) > 1 or False,
  91. timeout=30, what="requeued dispatch") is not None,
  92. "the re-queued task dispatches again")
  93. def comment_reached_agent():
  94. for payload in fake.start_payloads:
  95. task_obj = payload.get("task") or {}
  96. if task_obj.get("id") == plain and any(
  97. "cover the retry path" in c["body"]
  98. for c in task_obj.get("comments", [])):
  99. return True
  100. return any("cover the retry path" in (c.get("text") or "")
  101. for c in fake.comment_log)
  102. # The comment reaches the agent by EITHER channel: inside an
  103. # assignment payload, or via the live /task-update push to the
  104. # RUNNING daemon (the delivered flag) — the dispatcher tick races
  105. # the dispatch, so wait for whichever lands.
  106. check(wait_for(comment_reached_agent, timeout=30,
  107. what="comment delivery") is not None,
  108. "the second round receives the resurrecting comment (assignment or live push)")
  109. check(wait_task_done(auth, plain) is not None, "the second round completes")
  110. print("== terminal goal: a comment re-opens it and resumes the agent ==")
  111. # One behaviour per ROUND: round 1 plans (parks blocked), round 2
  112. # (the approved resume) suspends, round 3 (the force-woke resume)
  113. # submits the completion report.
  114. fake.goal_script = [("plan", "# Resurrect plan\n\n1. Finish quickly"),
  115. ("suspend",),
  116. ("done", "# Completion\n\nThe report stands.")]
  117. goal = create_goal({"project_id": "1", "title": "Resurrect goal",
  118. "brief": "Completes, then re-opens on a comment.",
  119. "branch_name": "goal/resurrect"})
  120. # (Goal 27 refinements — the parking guard: the round-2 suspend
  121. # needs a live wake source under the goal.)
  122. conn = db()
  123. conn.execute("insert into tasks (project_id, goal_id, kind, title, status,"
  124. " priority, mode, creator, created_at) values (1, ?, 'task',"
  125. " 'Resurrect live worker', 'blocked', 5, 'implement', 'goal',"
  126. " strftime('%s','now'))", (goal,))
  127. conn.commit()
  128. conn.close()
  129. act(auth, goal_state(goal)["start"], {})
  130. check(wait_for(lambda: run_task_of(goal) is not None or False,
  131. timeout=30, what="goal run task appears") is not None,
  132. "starting the goal creates its run task")
  133. run_task = run_task_of(goal)
  134. check(wait_for(lambda: goal_state(goal)["status"] == "plan_review" or False,
  135. timeout=90, what="goal plans") is not None,
  136. "the goal reaches plan review")
  137. pending = goal_state(goal)["plan_pending_block"]
  138. act(auth, pending["answer_action"], {"verdict": "accept"})
  139. check(wait_for(lambda: goal_state(goal)["status"] == "active" or False,
  140. timeout=60, what="plan approved") is not None,
  141. "approving the plan activates the goal")
  142. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended" or False,
  143. timeout=60, what="agent parks") is not None,
  144. "the agent parks after the plan")
  145. # Round 3 needs a wake: force-wake the parked agent so its scripted
  146. # done round (the completion report) runs.
  147. state = goal_state(goal)
  148. act(auth, state["force_wake"], {})
  149. # The completion round: the scripted done parks the run for review.
  150. check(wait_for(lambda: goal_state(goal)["status"] == "completion_review" or False,
  151. timeout=90, what="completion review opens") is not None,
  152. "the scripted done round enters completion review")
  153. block = goal_state(goal)["completion_pending_block"]
  154. act(auth, block["answer_action"], {"verdict": "accept"})
  155. check(wait_for(lambda: goal_state(goal)["status"] == "complete" or False,
  156. timeout=30, what="goal completes") is not None,
  157. "accepting the completion completes the goal")
  158. check(wait_for(lambda: task_state(auth, run_task)["status"] == "done" or False,
  159. timeout=30, what="run task done") is not None,
  160. "the run task closed done with the goal")
  161. goal_page = http_get_page(auth, "/goals/%d" % goal)
  162. check("stm-action=\"goal.comment\"" in goal_page,
  163. "the terminal goal page keeps the comment composer open")
  164. check("commenting re-opens it" in goal_page,
  165. "the composer explains the resurrect semantics on a terminal goal")
  166. state = goal_state(goal)
  167. act(auth, state["comment"], {"body": "One more sweep of the edge cases, please."})
  168. check(wait_for(lambda: goal_state(goal)["status"] == "active" or False,
  169. timeout=30, what="goal re-opens") is not None,
  170. "the comment re-opened the completed goal")
  171. check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended" or False,
  172. timeout=30, what="run re-offerable") is not None,
  173. "the DONE run task returned to suspended (the parked-resume shape)")
  174. conn = db()
  175. report_kept = conn.execute(
  176. "select completion_report from goals where id = ?", (goal,)).fetchone()[0]
  177. seeded = conn.execute(
  178. "select count(*) from sessions where task_id = ?", (run_task,)).fetchone()[0]
  179. conn.close()
  180. check(report_kept and len(report_kept) > 0,
  181. "the completion report stays on the re-opened goal")
  182. check(seeded >= 1, "the resurrected run carries a session row (the resume anchor)")
  183. goal_comments = goal_state(goal)["comments"]
  184. check(any("edge cases" in c["body"] for c in goal_comments),
  185. "the resurrecting comment rides the goal thread")
  186. # force_wake made the parked run offerable at once: the dispatcher
  187. # hands it back and the resuming assignment carries the notice.
  188. assignments_before = len(fake.goal_assignments)
  189. check(wait_for(lambda: len(fake.goal_assignments) > assignments_before or False,
  190. timeout=60, what="resurrected agent resumes") is not None,
  191. "the resurrected goal agent resumes on its saddle")
  192. resumed = fake.goal_assignments[-1]
  193. check((resumed.get("task") or {}).get("id") == run_task,
  194. "the resuming assignment is the run task")
  195. check("re-opened by a human comment" in (resumed.get("resume_notice") or ""),
  196. "the resume notice names the comment-driven re-open")
  197. check(any("edge cases" in c["body"]
  198. for c in (resumed.get("task") or {}).get("comments", []))
  199. or any("edge cases" in (c.get("text") or "") for c in fake.comment_log),
  200. "the resuming agent receives the comment (assignment or live push)")
  201. print("== terminal goal worker: comments stay closed ==")
  202. conn = db()
  203. worker = conn.execute(
  204. "insert into tasks (project_id, goal_id, kind, title, status, priority,"
  205. " mode, creator, completed_at, created_at)"
  206. " values (1, ?, 'task', 'Dead worker', 'done', 5,"
  207. " 'implement', 'goal', strftime('%s','now'), strftime('%s','now'))",
  208. (goal,)).lastrowid
  209. conn.commit()
  210. conn.close()
  211. worker_page = http_get_page(auth, "/tasks/%d" % worker)
  212. check("Comments are closed" in worker_page,
  213. "a terminal goal-owned task page explains the closed comments")
  214. # The gate lives in the template (statum hides the branch at render;
  215. # the served markup carries no branch text to match — pages.py's
  216. # Edit-button precedent): the composer must sit inside the
  217. # goal-owned-terminal negation and the closed notice inside the
  218. # positive gate.
  219. template = open(os.path.join(REPO, "src", "pages", "task_detail.html")).read()
  220. composer_at = template.find('<form stm-if="!(task.terminal && task.goal_id > 0)"')
  221. notice_at = template.find('<p stm-if="task.terminal && task.goal_id > 0"')
  222. check(composer_at > 0 and notice_at > composer_at,
  223. "the task template gates the composer on goal-owned terminal tasks")
  224. # The service path is the goal's alone: the page-level gate is the
  225. # only barrier a human meets, and a direct AddCommentAction post on
  226. # the dead worker must NOT resurrect it (the action ignores
  227. # goal-owned terminal tasks).
  228. state = task_state(auth, worker)
  229. if state.get("comment"):
  230. act(auth, state["comment"], {"body": "Sneaky resurrection attempt"})
  231. check(task_state(auth, worker)["status"] == "done",
  232. "a comment on a terminal goal worker does not resurrect it")
  233. else:
  234. check(True, "a terminal goal worker exposes no comment action at all")