|
|
@@ -1488,6 +1488,126 @@ def main():
|
|
|
check(status == 400 and "cycle" in body.get("error", ""),
|
|
|
"depending on an ancestor rejected as a cycle")
|
|
|
|
|
|
+
|
|
|
+ print("== R4 B2: subtask auto-approve ==")
|
|
|
+ # Settle earlier stragglers first: the round-3/B1 suspended parents
|
|
|
+ # resume (a fresh /start) once their last subtask completes — those
|
|
|
+ # pushes would consume the one-shot suspend flag below.
|
|
|
+ check(wait_task_done(auth, push_parent, timeout=90) is not None
|
|
|
+ and wait_task_done(auth, push_child_two, timeout=90) is not None
|
|
|
+ and wait_task_done(auth, cb_parent, timeout=90) is not None,
|
|
|
+ "earlier suspended parents settle before the auto-approve run")
|
|
|
+
|
|
|
+ # Parent parks; its toggle then covers all descendants. Children are
|
|
|
+ # created with an unmet 'gpu' requirement so they stay queued (and
|
|
|
+ # untouched) until the requirement is lifted for the final-resume leg.
|
|
|
+ fake.suspend_after_start = True
|
|
|
+ act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
|
+ "project_id": "1",
|
|
|
+ "title": "Auto parent",
|
|
|
+ "priority": "2",
|
|
|
+ })
|
|
|
+ auto_parent = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"]
|
|
|
+ if t["title"] == "Auto parent"][0])
|
|
|
+ check(wait_for(lambda: task_state(auth, auto_parent).get("status") == "suspended"
|
|
|
+ or False, timeout=60, what="auto parent parks") is not None,
|
|
|
+ "auto parent parks")
|
|
|
+
|
|
|
+ detail = task_state(auth, auto_parent)
|
|
|
+ check(detail.get("auto_approve") is False,
|
|
|
+ "auto-approve defaults to off")
|
|
|
+ act(auth, detail["toggle_auto_approve"], {})
|
|
|
+ check(task_state(auth, auto_parent).get("auto_approve") is True,
|
|
|
+ "toggle turns auto-approve on")
|
|
|
+
|
|
|
+ gpu_option = int([o["id"] for o in slot_public(auth, "/tasks", "tasks")["capability_options"]
|
|
|
+ if o["name"] == "gpu"][0])
|
|
|
+ status, body = saddle_api("POST", "/api/saddle/subtask", r4_token,
|
|
|
+ {"parent_task_id": auto_parent,
|
|
|
+ "title": "Auto child", "depends_on": []})
|
|
|
+ auto_child = int(body["task_id"])
|
|
|
+ status, body = saddle_api("POST", "/api/saddle/subtask", r4_token,
|
|
|
+ {"parent_task_id": auto_child, "title": "Auto grandchild"})
|
|
|
+ auto_grand = int(body["task_id"])
|
|
|
+ act(auth, task_state(auth, auto_child)["attach_capability"],
|
|
|
+ {"capability_id": str(gpu_option)})
|
|
|
+ act(auth, task_state(auth, auto_grand)["attach_capability"],
|
|
|
+ {"capability_id": str(gpu_option)})
|
|
|
+
|
|
|
+ # Transitive coverage: the GRANDCHILD's review is auto-answered
|
|
|
+ # (any ancestor set), answered-by 'auto', timeline entry recorded.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into sessions (task_id, saddle_id, started_at)"
|
|
|
+ " values (?, ?, strftime('%s','now'))", (auto_grand, fake_id))
|
|
|
+ conn.commit()
|
|
|
+ grand_session = conn.execute("select max(id) from sessions").fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
|
|
|
+ {"type": "review", "session_id": grand_session, "data": {
|
|
|
+ "summary": "the flange is wired"}})
|
|
|
+ grand_detail = task_state(auth, auto_grand)
|
|
|
+ grand_review = [b for b in grand_detail["blocks"] if b["block_type"] == "review"][0]
|
|
|
+ check(grand_review.get("answered") is True
|
|
|
+ and grand_review.get("answer") == "Auto-approved (subtask auto-approve is on)"
|
|
|
+ and grand_review.get("answered_by") == "auto",
|
|
|
+ "covered grandchild review auto-answered as 'auto'")
|
|
|
+ conn = db()
|
|
|
+ timeline_auto = conn.execute(
|
|
|
+ "select count(*) from session_commands where session_id = ?"
|
|
|
+ " and type = 'auto-approve'", (grand_session,)).fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ check(timeline_auto >= 1, "auto action visible in the timeline")
|
|
|
+
|
|
|
+ # Questions and RFCs still block for a human.
|
|
|
+ saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
|
|
|
+ {"type": "block", "session_id": grand_session, "data": {
|
|
|
+ "question": "Which torque spec?", "type": "question"}})
|
|
|
+ saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
|
|
|
+ {"type": "block", "session_id": grand_session, "data": {
|
|
|
+ "question": "RFC: approve the design", "type": "rfc",
|
|
|
+ "body": "# Design"}})
|
|
|
+ grand_detail = task_state(auth, auto_grand)
|
|
|
+ check(all(b.get("answered") is not True for b in grand_detail["blocks"]
|
|
|
+ if b["block_type"] in ("question", "rfc")),
|
|
|
+ "questions and RFCs are never auto-answered")
|
|
|
+
|
|
|
+ # The toggle covers DESCENDANTS only: the parent's own review waits.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at,"
|
|
|
+ " outcome) values (?, ?, strftime('%s','now'), strftime('%s','now'),"
|
|
|
+ " 'done')", (auto_parent, fake_id))
|
|
|
+ conn.commit()
|
|
|
+ parent_session = conn.execute("select max(id) from sessions").fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token,
|
|
|
+ {"type": "review", "session_id": parent_session, "data": {
|
|
|
+ "summary": "the whole change"}})
|
|
|
+ parent_detail = task_state(auth, auto_parent)
|
|
|
+ parent_review = [b for b in parent_detail["blocks"] if b["block_type"] == "review"][0]
|
|
|
+ check(parent_review.get("answered") is not True,
|
|
|
+ "the top-level parent's own review is never auto-approved")
|
|
|
+
|
|
|
+ # All descendants terminal → the suspended parent resumes with the
|
|
|
+ # final-review instruction (lift the capability requirement first).
|
|
|
+ act(auth, task_state(auth, auto_child)["capabilities"][0]["detach"], {})
|
|
|
+ act(auth, task_state(auth, auto_grand)["capabilities"][0]["detach"], {})
|
|
|
+
|
|
|
+ def pushes_for(task_id):
|
|
|
+ return len([p for p in fake.start_payloads
|
|
|
+ if int(p.get("task", {}).get("id") or 0) == task_id])
|
|
|
+
|
|
|
+ before_pushes = pushes_for(auto_parent)
|
|
|
+ check(wait_for(lambda: pushes_for(auto_parent) > before_pushes or False,
|
|
|
+ timeout=90, what="auto parent resumes") is not None,
|
|
|
+ "terminal subtasks resume the suspended parent")
|
|
|
+ notice = ""
|
|
|
+ for p in fake.start_payloads:
|
|
|
+ if int(p.get("task", {}).get("id") or 0) == auto_parent:
|
|
|
+ notice = p.get("resume_notice") or ""
|
|
|
+ check("All subtasks are complete" in notice
|
|
|
+ and "ONE final review" in notice and "saddle review" in notice,
|
|
|
+ "resume notice ends with the final-review instruction")
|
|
|
+
|
|
|
finally:
|
|
|
# Exit-code diagnostic: distinguishes a crashed app (negative
|
|
|
# returncode, e.g. -11 SIGSEGV) from a clean shutdown. When
|