|
@@ -5700,6 +5700,79 @@ def bundle_goals(ctx):
|
|
|
"brief": "b", "branch_name": "goal/doomed"})
|
|
"brief": "b", "branch_name": "goal/doomed"})
|
|
|
check(reuse > goal2, "a cancelled goal's branch name is reusable")
|
|
check(reuse > goal2, "a cancelled goal's branch name is reusable")
|
|
|
|
|
|
|
|
|
|
+ print("== plan 13 B3: goal failure cascades to non-terminal tasks ==")
|
|
|
|
|
+ # A goal holding a draft, a queued task and a working task: the
|
|
|
|
|
+ # agent's failure cascades like a cancellation — drafts deleted (no
|
|
|
|
|
+ # orphan stays dispatchable), queued/working cancelled — and the
|
|
|
|
|
+ # narrative records the cascade.
|
|
|
|
|
+ collapse = create_goal({"project_id": "1", "title": "Collapse goal",
|
|
|
|
|
+ "brief": "Fails with tasks in flight.",
|
|
|
|
|
+ "branch_name": "goal/collapse"})
|
|
|
|
|
+ fake.goal_script = [("suspend",)]
|
|
|
|
|
+ act(auth, goal_state(collapse)["start"], {})
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ run3 = conn.execute("select id from tasks where goal_id = ? and kind = 'goal_run'",
|
|
|
|
|
+ (collapse,)).fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+ check(wait_for(lambda: task_state(auth, run3)["status"] == "suspended" or False,
|
|
|
|
|
+ timeout=60, what="collapse goal agent parks") is not None,
|
|
|
|
|
+ "the collapse goal's agent parks first")
|
|
|
|
|
+ conn = db()
|
|
|
|
|
+ run3_session = conn.execute(
|
|
|
|
|
+ "select id from sessions where task_id = ? and ended_at is null",
|
|
|
|
|
+ (run3,)).fetchone()[0]
|
|
|
|
|
+ conn.close()
|
|
|
|
|
+
|
|
|
|
|
+ def agent_task(title):
|
|
|
|
|
+ status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
|
|
|
|
|
+ "session_id": run3_session, "title": title, "mandate": "m"})
|
|
|
|
|
+ task_id = int(body["task_id"])
|
|
|
|
|
+ saddle_api("POST", "/api/saddle/task/enqueue", fake_token,
|
|
|
|
|
+ {"session_id": run3_session, "task_id": task_id})
|
|
|
|
|
+ return task_id
|
|
|
|
|
+
|
|
|
|
|
+ status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
|
|
|
|
|
+ "session_id": run3_session, "title": "Collapse draft", "mandate": "m"})
|
|
|
|
|
+ collapse_draft = int(body["task_id"]) # stays draft (never enqueued)
|
|
|
|
|
+ collapse_queued = agent_task("Collapse queued")
|
|
|
|
|
+ collapse_worker = agent_task("Collapse worker")
|
|
|
|
|
+ # Both queued tasks race for the one saddle; freeze the daemon the
|
|
|
|
|
+ # moment the first dispatches so one stays genuinely WORKING (and
|
|
|
|
|
+ # the other queued) when the goal fails.
|
|
|
|
|
+ fake.hold_state = True
|
|
|
|
|
+ def collapse_working():
|
|
|
|
|
+ for task_id in (collapse_queued, collapse_worker):
|
|
|
|
|
+ if task_state(auth, task_id)["status"] == "working":
|
|
|
|
|
+ return task_id
|
|
|
|
|
+ return False
|
|
|
|
|
+ running_one = wait_for(collapse_working, timeout=60, what="a collapse task works")
|
|
|
|
|
+ check(running_one is not None,
|
|
|
|
|
+ "a collapse task reaches working before the failure")
|
|
|
|
|
+ queued_one = collapse_worker if running_one == collapse_queued else collapse_queued
|
|
|
|
|
+
|
|
|
|
|
+ status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
|
|
|
|
|
+ {"session_id": run3_session, "state": "failed",
|
|
|
|
|
+ "summary": "the foundation crumbled"})
|
|
|
|
|
+ check(status == 200 and goal_state(collapse)["status"] == "failed",
|
|
|
|
|
+ "the agent's failure fails the goal")
|
|
|
|
|
+ check(task_state(auth, run3)["status"] == "failed",
|
|
|
|
|
+ "the failure fails the goal agent's run task")
|
|
|
|
|
+ check(wait_for(lambda: task_state(auth, queued_one)["status"] == "failed"
|
|
|
|
|
+ and task_state(auth, running_one)["status"] == "failed" or False,
|
|
|
|
|
+ timeout=30, what="failure cascade settles") is not None,
|
|
|
|
|
+ "the failure cascade cancels the queued and working tasks")
|
|
|
|
|
+ check(not any(t["id"] == collapse_draft
|
|
|
|
|
+ for t in slot_public(auth, "/tasks", "tasks")["tasks"]),
|
|
|
|
|
+ "the failure cascade deletes the goal's draft tasks")
|
|
|
|
|
+ state = goal_state(collapse)
|
|
|
|
|
+ check(any(e["kind"] == "cascade" for e in state["events"]),
|
|
|
|
|
+ "the narrative records the failure cascade")
|
|
|
|
|
+ check(state["draft_tasks"] == 0 and state["queued_tasks"] == 0,
|
|
|
|
|
+ "nothing under the failed goal stays dispatchable (no orphan drafts)")
|
|
|
|
|
+ check(len([e for e in state["events"] if e["kind"] == "goal-failed"]) == 1,
|
|
|
|
|
+ "the failing goal records exactly one goal-failed event")
|
|
|
|
|
+ fake.hold_state = False
|
|
|
|
|
+
|
|
|
print("== plan 12: auto-enqueue-after for human tasks ==")
|
|
print("== plan 12: auto-enqueue-after for human tasks ==")
|
|
|
act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
|
"project_id": "1", "title": "Predecessor task", "priority": "4"})
|
|
"project_id": "1", "title": "Predecessor task", "priority": "4"})
|