|
@@ -0,0 +1,227 @@
|
|
|
|
|
+"""The resurrect bundle (goal 8 task 4): standalone (no-project) tasks
|
|
|
|
|
+and comment-resurrected terminal items.
|
|
|
|
|
+
|
|
|
|
|
+- A task created with project_id 0 dispatches with EMPTY co-developed
|
|
|
|
|
+ and reference arrays, the "standalone" project name, and completes on
|
|
|
|
|
+ its completion report.
|
|
|
|
|
+- A human comment on a TERMINAL goal (complete) re-opens the goal, its
|
|
|
|
|
+ run task returns to suspended-offerable, and the resuming assignment
|
|
|
|
|
+ carries the comment + the re-open line in its resume notice.
|
|
|
|
|
+- A human comment on a TERMINAL plain task re-queues it, the next
|
|
|
|
|
+ assignment carries the comment.
|
|
|
|
|
+- A terminal goal-owned WORKER task keeps comments closed (the page
|
|
|
|
|
+ hides the composer; the service path is the goal's alone).
|
|
|
|
|
+"""
|
|
|
|
|
+from harness import * # noqa: F401,F403 — the shared wire helpers
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def run(ctx):
|
|
|
|
|
+ auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
|
|
|
|
|
+
|
|
|
|
|
+ def create_goal(fields):
|
|
|
|
|
+ act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
|
|
|
|
|
+ rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
|
|
|
|
|
+ if g["title"] == fields["title"]]
|
|
|
|
|
+ return int(rows[0]["id"])
|
|
|
|
|
+
|
|
|
|
|
+ def run_task_of(goal_id):
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ try:
|
|
|
|
|
+ row = conn.execute(
|
|
|
|
|
+ "select id from tasks where goal_id = ? and kind = 'goal_run'",
|
|
|
|
|
+ (goal_id,)).fetchone()
|
|
|
|
|
+ return int(row[0]) if row else None
|
|
|
|
|
+ finally:
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+ print("== standalone task: created with no project, dispatched, done ==")
|
|
|
|
|
+ state = slot_public(auth, "/tasks", "tasks")
|
|
|
|
|
+ act(auth, state["create"], {
|
|
|
|
|
+ "project_id": "0",
|
|
|
|
|
+ "title": "Standalone research",
|
|
|
|
|
+ "description": "Summarise the fleet's flaky tests.",
|
|
|
|
|
+ "priority": "3",
|
|
|
|
|
+ })
|
|
|
|
|
+ standalone = task_id_of("Standalone research")
|
|
|
|
|
+ detail = task_state(auth, standalone)
|
|
|
|
|
+ check(detail["status"] == "draft" and int(detail["project_id"]) == 0,
|
|
|
|
|
+ "a project_id 0 task creates in draft (project_id %s)" % detail["project_id"])
|
|
|
|
|
+ page = http_get_page(auth, "/tasks/%d" % standalone)
|
|
|
|
|
+ check("(standalone)" in page and "/projects/0" not in page,
|
|
|
|
|
+ "the task page renders the (standalone) marker, not a project link")
|
|
|
|
|
+ check("No project (standalone)" in http_get_page(auth, "/tasks"),
|
|
|
|
|
+ "the create form offers the No project (standalone) option")
|
|
|
|
|
+ enqueue_task(auth, standalone)
|
|
|
|
|
+ check(wait_for(lambda: len(fake.start_payloads) > 0 or False,
|
|
|
|
|
+ timeout=30, what="standalone /start push") is not None,
|
|
|
|
|
+ "the dispatcher pushes the standalone task to the fake saddle")
|
|
|
|
|
+ payload = fake.start_payloads[-1]
|
|
|
|
|
+ task_obj = payload.get("task") or {}
|
|
|
|
|
+ check(task_obj.get("project_name") == "standalone",
|
|
|
|
|
+ "the standalone assignment names the project 'standalone' (%s)"
|
|
|
|
|
+ % task_obj.get("project_name"))
|
|
|
|
|
+ check(task_obj.get("co_developed") == [] and task_obj.get("reference") == [],
|
|
|
|
|
+ "the standalone assignment ships EMPTY co-developed and reference arrays")
|
|
|
|
|
+ check(task_obj.get("branch_from", "x") == "",
|
|
|
|
|
+ "the standalone assignment carries no branch_from")
|
|
|
|
|
+ check(wait_task_done(auth, standalone) is not None,
|
|
|
|
|
+ "the standalone task completes on its report")
|
|
|
|
|
+
|
|
|
|
|
+ print("== terminal plain task: a comment re-queues it ==")
|
|
|
|
|
+ plain = create_queued(auth, {
|
|
|
|
|
+ "project_id": "1", "title": "Resurrect me",
|
|
|
|
|
+ "description": "A plain project task that finishes.", "priority": "4"})
|
|
|
|
|
+ check(wait_task_done(auth, plain) is not None, "the plain task completes")
|
|
|
|
|
+ detail = task_state(auth, plain)
|
|
|
|
|
+ act(auth, detail["comment"], {"body": "Please also cover the retry path."})
|
|
|
|
|
+ detail = task_state(auth, plain)
|
|
|
|
|
+ check(detail["status"] == "queued",
|
|
|
|
|
+ "the comment re-queued the terminal plain task (%s)" % detail["status"])
|
|
|
|
|
+ check(any("cover the retry path" in c["body"] for c in detail["comments"]),
|
|
|
|
|
+ "the resurrecting comment rides the thread")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ summary_kept = conn.execute(
|
|
|
|
|
+ "select summary from tasks where id = ?", (plain,)).fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(summary_kept and len(summary_kept) > 0,
|
|
|
|
|
+ "the earlier round's completion summary survives the re-open")
|
|
|
|
|
+ check(wait_for(lambda: len(fake.start_payloads) > 1 or False,
|
|
|
|
|
+ timeout=30, what="requeued dispatch") is not None,
|
|
|
|
|
+ "the re-queued task dispatches again")
|
|
|
|
|
+
|
|
|
|
|
+ def comment_reached_agent():
|
|
|
|
|
+ for payload in fake.start_payloads:
|
|
|
|
|
+ task_obj = payload.get("task") or {}
|
|
|
|
|
+ if task_obj.get("id") == plain and any(
|
|
|
|
|
+ "cover the retry path" in c["body"]
|
|
|
|
|
+ for c in task_obj.get("comments", [])):
|
|
|
|
|
+ return True
|
|
|
|
|
+ return any("cover the retry path" in (c.get("text") or "")
|
|
|
|
|
+ for c in fake.comment_log)
|
|
|
|
|
+ # The comment reaches the agent by EITHER channel: inside an
|
|
|
|
|
+ # assignment payload, or via the live /task-update push to the
|
|
|
|
|
+ # RUNNING daemon (the delivered flag) — the dispatcher tick races
|
|
|
|
|
+ # the dispatch, so wait for whichever lands.
|
|
|
|
|
+ check(wait_for(comment_reached_agent, timeout=30,
|
|
|
|
|
+ what="comment delivery") is not None,
|
|
|
|
|
+ "the second round receives the resurrecting comment (assignment or live push)")
|
|
|
|
|
+ check(wait_task_done(auth, plain) is not None, "the second round completes")
|
|
|
|
|
+
|
|
|
|
|
+ print("== terminal goal: a comment re-opens it and resumes the agent ==")
|
|
|
|
|
+ # One behaviour per ROUND: round 1 plans (parks blocked), round 2
|
|
|
|
|
+ # (the approved resume) suspends, round 3 (the force-woke resume)
|
|
|
|
|
+ # submits the completion report.
|
|
|
|
|
+ fake.goal_script = [("plan", "# Resurrect plan\n\n1. Finish quickly"),
|
|
|
|
|
+ ("suspend",),
|
|
|
|
|
+ ("done", "# Completion\n\nThe report stands.")]
|
|
|
|
|
+ goal = create_goal({"project_id": "1", "title": "Resurrect goal",
|
|
|
|
|
+ "brief": "Completes, then re-opens on a comment.",
|
|
|
|
|
+ "branch_name": "goal/resurrect"})
|
|
|
|
|
+ act(auth, goal_state(goal)["start"], {})
|
|
|
|
|
+ check(wait_for(lambda: run_task_of(goal) is not None or False,
|
|
|
|
|
+ timeout=30, what="goal run task appears") is not None,
|
|
|
|
|
+ "starting the goal creates its run task")
|
|
|
|
|
+ run_task = run_task_of(goal)
|
|
|
|
|
+ check(wait_for(lambda: goal_state(goal)["status"] == "plan_review" or False,
|
|
|
|
|
+ timeout=90, what="goal plans") is not None,
|
|
|
|
|
+ "the goal reaches plan review")
|
|
|
|
|
+ pending = goal_state(goal)["plan_pending_block"]
|
|
|
|
|
+ act(auth, pending["answer_action"], {"verdict": "accept"})
|
|
|
|
|
+ check(wait_for(lambda: goal_state(goal)["status"] == "active" or False,
|
|
|
|
|
+ timeout=60, what="plan approved") is not None,
|
|
|
|
|
+ "approving the plan activates the goal")
|
|
|
|
|
+ check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended" or False,
|
|
|
|
|
+ timeout=60, what="agent parks") is not None,
|
|
|
|
|
+ "the agent parks after the plan")
|
|
|
|
|
+ # Round 3 needs a wake: force-wake the parked agent so its scripted
|
|
|
|
|
+ # done round (the completion report) runs.
|
|
|
|
|
+ state = goal_state(goal)
|
|
|
|
|
+ act(auth, state["force_wake"], {})
|
|
|
|
|
+ # The completion round: the scripted done parks the run for review.
|
|
|
|
|
+ check(wait_for(lambda: goal_state(goal)["status"] == "completion_review" or False,
|
|
|
|
|
+ timeout=90, what="completion review opens") is not None,
|
|
|
|
|
+ "the scripted done round enters completion review")
|
|
|
|
|
+ block = goal_state(goal)["completion_pending_block"]
|
|
|
|
|
+ act(auth, block["answer_action"], {"verdict": "accept"})
|
|
|
|
|
+ check(wait_for(lambda: goal_state(goal)["status"] == "complete" or False,
|
|
|
|
|
+ timeout=30, what="goal completes") is not None,
|
|
|
|
|
+ "accepting the completion completes the goal")
|
|
|
|
|
+ check(wait_for(lambda: task_state(auth, run_task)["status"] == "done" or False,
|
|
|
|
|
+ timeout=30, what="run task done") is not None,
|
|
|
|
|
+ "the run task closed done with the goal")
|
|
|
|
|
+
|
|
|
|
|
+ goal_page = http_get_page(auth, "/goals/%d" % goal)
|
|
|
|
|
+ check("stm-action=\"goal.comment\"" in goal_page,
|
|
|
|
|
+ "the terminal goal page keeps the comment composer open")
|
|
|
|
|
+ check("commenting re-opens it" in goal_page,
|
|
|
|
|
+ "the composer explains the resurrect semantics on a terminal goal")
|
|
|
|
|
+ state = goal_state(goal)
|
|
|
|
|
+ act(auth, state["comment"], {"body": "One more sweep of the edge cases, please."})
|
|
|
|
|
+ check(wait_for(lambda: goal_state(goal)["status"] == "active" or False,
|
|
|
|
|
+ timeout=30, what="goal re-opens") is not None,
|
|
|
|
|
+ "the comment re-opened the completed goal")
|
|
|
|
|
+ check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended" or False,
|
|
|
|
|
+ timeout=30, what="run re-offerable") is not None,
|
|
|
|
|
+ "the DONE run task returned to suspended (the parked-resume shape)")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ report_kept = conn.execute(
|
|
|
|
|
+ "select completion_report from goals where id = ?", (goal,)).fetchone()[0]
|
|
|
|
|
+ seeded = conn.execute(
|
|
|
|
|
+ "select count(*) from sessions where task_id = ?", (run_task,)).fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(report_kept and len(report_kept) > 0,
|
|
|
|
|
+ "the completion report stays on the re-opened goal")
|
|
|
|
|
+ check(seeded >= 1, "the resurrected run carries a session row (the resume anchor)")
|
|
|
|
|
+ goal_comments = goal_state(goal)["comments"]
|
|
|
|
|
+ check(any("edge cases" in c["body"] for c in goal_comments),
|
|
|
|
|
+ "the resurrecting comment rides the goal thread")
|
|
|
|
|
+ # force_wake made the parked run offerable at once: the dispatcher
|
|
|
|
|
+ # hands it back and the resuming assignment carries the notice.
|
|
|
|
|
+ assignments_before = len(fake.goal_assignments)
|
|
|
|
|
+ check(wait_for(lambda: len(fake.goal_assignments) > assignments_before or False,
|
|
|
|
|
+ timeout=60, what="resurrected agent resumes") is not None,
|
|
|
|
|
+ "the resurrected goal agent resumes on its saddle")
|
|
|
|
|
+ resumed = fake.goal_assignments[-1]
|
|
|
|
|
+ check((resumed.get("task") or {}).get("id") == run_task,
|
|
|
|
|
+ "the resuming assignment is the run task")
|
|
|
|
|
+ check("re-opened by a human comment" in (resumed.get("resume_notice") or ""),
|
|
|
|
|
+ "the resume notice names the comment-driven re-open")
|
|
|
|
|
+ check(any("edge cases" in c["body"]
|
|
|
|
|
+ for c in (resumed.get("task") or {}).get("comments", []))
|
|
|
|
|
+ or any("edge cases" in (c.get("text") or "") for c in fake.comment_log),
|
|
|
|
|
+ "the resuming agent receives the comment (assignment or live push)")
|
|
|
|
|
+
|
|
|
|
|
+ print("== terminal goal worker: comments stay closed ==")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ worker = conn.execute(
|
|
|
|
|
+ "insert into tasks (project_id, goal_id, kind, title, status, priority,"
|
|
|
|
|
+ " mode, creator, completed_at, created_at)"
|
|
|
|
|
+ " values (1, ?, 'task', 'Dead worker', 'done', 5,"
|
|
|
|
|
+ " 'implement', 'goal', strftime('%s','now'), strftime('%s','now'))",
|
|
|
|
|
+ (goal,)).lastrowid
|
|
|
|
|
+ conn.commit()
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ worker_page = http_get_page(auth, "/tasks/%d" % worker)
|
|
|
|
|
+ check("Comments are closed" in worker_page,
|
|
|
|
|
+ "a terminal goal-owned task page explains the closed comments")
|
|
|
|
|
+ # The gate lives in the template (statum hides the branch at render;
|
|
|
|
|
+ # the served markup carries no branch text to match — pages.py's
|
|
|
|
|
+ # Edit-button precedent): the composer must sit inside the
|
|
|
|
|
+ # goal-owned-terminal negation and the closed notice inside the
|
|
|
|
|
+ # positive gate.
|
|
|
|
|
+ template = open(os.path.join(REPO, "src", "pages", "task_detail.html")).read()
|
|
|
|
|
+ composer_at = template.find('<form stm-if="!(task.terminal && task.goal_id > 0)"')
|
|
|
|
|
+ notice_at = template.find('<p stm-if="task.terminal && task.goal_id > 0"')
|
|
|
|
|
+ check(composer_at > 0 and notice_at > composer_at,
|
|
|
|
|
+ "the task template gates the composer on goal-owned terminal tasks")
|
|
|
|
|
+ # The service path is the goal's alone: the page-level gate is the
|
|
|
|
|
+ # only barrier a human meets, and a direct AddCommentAction post on
|
|
|
|
|
+ # the dead worker must NOT resurrect it (the action ignores
|
|
|
|
|
+ # goal-owned terminal tasks).
|
|
|
|
|
+ state = task_state(auth, worker)
|
|
|
|
|
+ if state.get("comment"):
|
|
|
|
|
+ act(auth, state["comment"], {"body": "Sneaky resurrection attempt"})
|
|
|
|
|
+ check(task_state(auth, worker)["status"] == "done",
|
|
|
|
|
+ "a comment on a terminal goal worker does not resurrect it")
|
|
|
|
|
+ else:
|
|
|
|
|
+ check(True, "a terminal goal worker exposes no comment action at all")
|