|
|
@@ -395,6 +395,17 @@ def db():
|
|
|
return sqlite3.connect(DB_PATH)
|
|
|
|
|
|
|
|
|
+def http_get_page(auth, uri):
|
|
|
+ """The compiled page HTML for a statum route (auth via the slot
|
|
|
+ header), for asserting page-level embeds the slot state cannot
|
|
|
+ see."""
|
|
|
+ status, body = http("GET", uri, headers={
|
|
|
+ "X-Statum-Slot": "key=%s; as_at=%s" % (auth["key"], auth["as_at"])})
|
|
|
+ if status != 200:
|
|
|
+ raise SystemExit("GET %s -> HTTP %d" % (uri, status))
|
|
|
+ return body
|
|
|
+
|
|
|
+
|
|
|
def saddle_card(auth, name):
|
|
|
rows = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
|
|
|
if s["name"] == name]
|
|
|
@@ -1587,10 +1598,10 @@ def bundle_pages(ctx):
|
|
|
and page.index("<h3>Progress</h3>") < page.index("status-head"),
|
|
|
"the progress feed renders above the status card")
|
|
|
check(page.index("grid-2") < page.index("status-head")
|
|
|
- and page.index("status-head") < page.index("<h3>Description</h3>"),
|
|
|
+ and page.index("status-head") < page.index("<h3>Mandate</h3>"),
|
|
|
"status and description sit side by side inside the grid (status first)")
|
|
|
- check("<h3>Description</h3>" in page
|
|
|
- and page.index("<h3>Description</h3>") < page.index("plan-expander"),
|
|
|
+ check("<h3>Mandate</h3>" in page
|
|
|
+ and page.index("<h3>Mandate</h3>") < page.index("plan-expander"),
|
|
|
"the plan detail hides behind its expander below the grid")
|
|
|
# Answered blocks fold behind the history expander (this fixture's
|
|
|
# three blocks are all answered by now).
|
|
|
@@ -1779,9 +1790,14 @@ def bundle_pages(ctx):
|
|
|
"the lifecycle buttons ride the status card inside the grid")
|
|
|
status, queue_page = http("GET", "/tasks", timeout=15)
|
|
|
check(status == 200
|
|
|
- and queue_page.index("new-task-expander") < queue_page.index("filter-status")
|
|
|
- < queue_page.index("<table"),
|
|
|
- "the New Task expander sits above the filter controls and the table")
|
|
|
+ and queue_page.index("filter-status") < queue_page.index("<table")
|
|
|
+ and "filter-goal" in queue_page,
|
|
|
+ "the filter controls (status + goal) sit above the queue table")
|
|
|
+ check("New task" in queue_page and "task-mode" in queue_page
|
|
|
+ and "task-enqueue-after" in queue_page
|
|
|
+ and 'name="plan_auto_approve"' not in queue_page,
|
|
|
+ "the create expander offers the mode + auto-enqueue-after selects"
|
|
|
+ " (no subtask checkboxes)")
|
|
|
|
|
|
print("== yeehaw: ingestion, festive card, badge, timeline ==")
|
|
|
# `saddle yeehaw <message>` arrives as a round-results entry with the
|
|
|
@@ -5091,6 +5107,108 @@ def bundle_push(ctx):
|
|
|
check(task_state(auth, task_id_of("Parked-late child"))["status"] == "draft",
|
|
|
"the parked parent's child lands in draft (no auto-enqueue)")
|
|
|
|
|
|
+ print("== plan 12: goal notifications — blocks/success/failure always ==")
|
|
|
+ # Goal blocks, completion approval and failure push to EVERY
|
|
|
+ # subscribed user (the mirror on the goal-run task stays quiet).
|
|
|
+ def goal_state(goal_id):
|
|
|
+ return slot_public(auth, "/goals/%d" % goal_id, "goal")
|
|
|
+
|
|
|
+ push_goal = None
|
|
|
+ act(auth, slot_public(auth, "/goals", "goals")["create"], {
|
|
|
+ "project_id": "1", "title": "Push goal", "brief": "Notifies everyone.",
|
|
|
+ "branch_name": "goal/push"})
|
|
|
+ conn = db()
|
|
|
+ row = conn.execute("select id from goals where title = 'Push goal'").fetchone()
|
|
|
+ conn.close()
|
|
|
+ push_goal = int(row[0])
|
|
|
+ fake.goal_script = [("plan", "# Push plan\n\n1. Ring the bells")]
|
|
|
+ main_before = len(push.hits("/main"))
|
|
|
+ other_before = len(push.hits("/other"))
|
|
|
+ act(auth, goal_state(push_goal)["start"], {})
|
|
|
+ check(wait_for(lambda: goal_state(push_goal)["status"] == "plan_review" or False,
|
|
|
+ timeout=60, what="push goal enters plan review") is not None,
|
|
|
+ "the push goal reaches plan review")
|
|
|
+ check(wait_for(lambda: len(push.hits("/main")) > main_before
|
|
|
+ and len(push.hits("/other")) > other_before, timeout=15,
|
|
|
+ what="goal plan-review push") is not None,
|
|
|
+ "a goal plan review notifies every subscribed user globally")
|
|
|
+ _, plan_payload = decrypt_webpush(push.hits("/other")[-1], other_browser,
|
|
|
+ other_public, other_auth)
|
|
|
+ check("Push goal" in plan_payload["title"] and "plan" in plan_payload["title"]
|
|
|
+ and plan_payload["url"] == "/goals/%d" % push_goal,
|
|
|
+ "the goal block payload carries the goal title + click URL (%s)"
|
|
|
+ % plan_payload)
|
|
|
+ main_before = len(push.hits("/main"))
|
|
|
+ fake.goal_script = [("suspend",)]
|
|
|
+ act(auth, goal_state(push_goal)["plan_pending_block"]["answer_action"],
|
|
|
+ {"answer": "Plan approved. Proceed with implementation."})
|
|
|
+ check(wait_for(lambda: goal_state(push_goal)["status"] == "active" or False,
|
|
|
+ timeout=30, what="push goal active") is not None,
|
|
|
+ "the push goal activates")
|
|
|
+ check(quiet_window(3, lambda: len(push.hits("/main")) == main_before,
|
|
|
+ "no push for an internal resume"),
|
|
|
+ "the goal agent's own park/resume does not notify (internal)")
|
|
|
+
|
|
|
+ # `saddle failure` in goal context fails the goal — global push.
|
|
|
+ fake.goal_script = [("fail", "the harness collapsed")]
|
|
|
+ other_before2 = len(push.hits("/other"))
|
|
|
+ main_before2 = len(push.hits("/main"))
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into goal_events (goal_id, kind, text, created_at)"
|
|
|
+ " values (?, 'task-done', 'Task #999 Warm — done',"
|
|
|
+ " strftime('%s','now','-5 seconds'))", (push_goal,))
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ check(wait_for(lambda: goal_state(push_goal)["status"] == "failed" or False,
|
|
|
+ timeout=60, what="goal failure lands") is not None,
|
|
|
+ "the goal agent's failure fails the goal")
|
|
|
+ check(wait_for(lambda: len(push.hits("/other")) > other_before2
|
|
|
+ and len(push.hits("/main")) > main_before2, timeout=15,
|
|
|
+ what="goal failure push") is not None,
|
|
|
+ "a goal failure notifies every subscribed user globally")
|
|
|
+ _, fail_payload = decrypt_webpush(push.hits("/other")[-1], other_browser,
|
|
|
+ other_public, other_auth)
|
|
|
+ check("Push goal" in fail_payload["title"] and "failed" in fail_payload["title"]
|
|
|
+ and fail_payload["url"] == "/goals/%d" % push_goal,
|
|
|
+ "the goal failure payload carries the goal title + click URL (%s)"
|
|
|
+ % fail_payload)
|
|
|
+
|
|
|
+ # Completion approval pushes "complete" globally (success always).
|
|
|
+ act(auth, slot_public(auth, "/goals", "goals")["create"], {
|
|
|
+ "project_id": "1", "title": "Push done goal", "brief": "Completes.",
|
|
|
+ "branch_name": "goal/push-done"})
|
|
|
+ conn = db()
|
|
|
+ row = conn.execute("select id from goals where title = 'Push done goal'").fetchone()
|
|
|
+ conn.close()
|
|
|
+ done_goal = int(row[0])
|
|
|
+ fake.goal_script = [("plan", "# Done plan")]
|
|
|
+ act(auth, goal_state(done_goal)["start"], {})
|
|
|
+ check(wait_for(lambda: goal_state(done_goal)["status"] == "plan_review" or False,
|
|
|
+ timeout=60, what="done goal plan review") is not None,
|
|
|
+ "the done goal reaches plan review")
|
|
|
+ fake.goal_script = [("done", "# Completion\n\nShipped.")]
|
|
|
+ act(auth, goal_state(done_goal)["plan_pending_block"]["answer_action"],
|
|
|
+ {"answer": "Plan approved. Proceed with implementation."})
|
|
|
+ check(wait_for(lambda: goal_state(done_goal)["status"] == "completion_review"
|
|
|
+ or False, timeout=60, what="done goal completion review") is not None,
|
|
|
+ "the approved done goal reaches completion review")
|
|
|
+ main_before = len(push.hits("/main"))
|
|
|
+ act(auth, goal_state(done_goal)["completion_pending_block"]["answer_action"],
|
|
|
+ {"answer": "Review approved. Continue with the task."})
|
|
|
+ check(wait_for(lambda: goal_state(done_goal)["status"] == "complete" or False,
|
|
|
+ timeout=30, what="done goal completes") is not None,
|
|
|
+ "the done goal completes")
|
|
|
+ check(wait_for(lambda: len(push.hits("/main")) > main_before, timeout=15,
|
|
|
+ what="goal success push") is not None,
|
|
|
+ "a goal completion notifies every subscribed user globally")
|
|
|
+ _, done_goal_payload = decrypt_webpush(push.hits("/main")[-1], browser,
|
|
|
+ browser_public, rotated_auth)
|
|
|
+ check("Push done goal" in done_goal_payload["title"]
|
|
|
+ and "complete" in done_goal_payload["title"]
|
|
|
+ and done_goal_payload["url"] == "/goals/%d" % done_goal,
|
|
|
+ "the goal success payload carries the goal title + click URL (%s)"
|
|
|
+ % done_goal_payload)
|
|
|
+
|
|
|
print("== task E: unsubscribe removes the subscription ==")
|
|
|
dash = slot_public(auth, "/", "dashboard")
|
|
|
check(dash.get("push_subscribed") is True,
|
|
|
@@ -5547,6 +5665,97 @@ def bundle_goals(ctx):
|
|
|
check(wait_task_done(auth, successor, timeout=90) is not None,
|
|
|
"the auto-enqueued successor completes")
|
|
|
|
|
|
+ print("== plan 12: the web surfaces — pile, timeline, queue, task page ==")
|
|
|
+ # A goal parked in plan review feeds the unified dashboard pile;
|
|
|
+ # goal events feed the timeline.
|
|
|
+ goal3 = create_goal({"project_id": "1", "title": "Dashboard goal",
|
|
|
+ "brief": "Feeds the pile.", "branch_name": "goal/dash"})
|
|
|
+ fake.goal_script = [("plan", "# Dash plan\n\n1. Probe the pile")]
|
|
|
+ act(auth, goal_state(goal3)["start"], {})
|
|
|
+ check(wait_for(lambda: goal_state(goal3)["status"] == "plan_review" or False,
|
|
|
+ timeout=60, what="dashboard goal enters plan review") is not None,
|
|
|
+ "the dashboard goal reaches plan review")
|
|
|
+ check(wait_for(lambda: any(b.get("item_type") == "goal"
|
|
|
+ and int(b.get("goal_id") or 0) == goal3
|
|
|
+ for b in slot_public(auth, "/", "dashboard")["pinned"])
|
|
|
+ or False, timeout=30, what="goal block pins on the dashboard") is not None,
|
|
|
+ "the unified dashboard pile carries the goal's plan review")
|
|
|
+ pile = slot_public(auth, "/", "dashboard")["pinned"]
|
|
|
+ check(all(b.get("item_type") in ("task", "goal") for b in pile),
|
|
|
+ "pile items carry type badges (task/goal interleaved)")
|
|
|
+ goal_pile = [b for b in pile if b.get("item_type") == "goal"][0]
|
|
|
+ check(goal_pile["block_type"] == "plan" and "Dashboard goal" in goal_pile["goal_title"],
|
|
|
+ "the goal pile card names the goal and block kind")
|
|
|
+ # Answer it: the pile drains.
|
|
|
+ act(auth, goal_state(goal3)["plan_pending_block"]["answer_action"],
|
|
|
+ {"answer": "Plan approved. Proceed with implementation."})
|
|
|
+ check(wait_for(lambda: not any(b.get("item_type") == "goal"
|
|
|
+ and int(b.get("goal_id") or 0) == goal3
|
|
|
+ for b in slot_public(auth, "/", "dashboard")["pinned"])
|
|
|
+ or False, timeout=30, what="goal pile card drains") is not None,
|
|
|
+ "answering the review drains the goal's pile card")
|
|
|
+ activity = slot_public(auth, "/", "dashboard")["activity"]
|
|
|
+ check(any(a["kind"] == "goal-plan-submitted" and int(a.get("goal_id") or 0) == goal3
|
|
|
+ and a["goal_title"] == "Dashboard goal" for a in activity),
|
|
|
+ "the dashboard timeline carries goal events linked to the goal")
|
|
|
+
|
|
|
+ # The task queue: goal filter options, mode badges, goal links,
|
|
|
+ # enqueue-after dropdown.
|
|
|
+ queue = slot_public(auth, "/tasks", "tasks")
|
|
|
+ check(any(int(g["id"]) == goal3 and g["title"] == "Dashboard goal"
|
|
|
+ for g in queue["goal_options"]),
|
|
|
+ "the queue's goal filter lists the goals")
|
|
|
+ check(queue["tasks"][0].get("mode") in ("implement", "investigate", "review", "test",
|
|
|
+ "orchestrate"),
|
|
|
+ "queue rows carry the plan-12 mode")
|
|
|
+ check(all(("goal_title" in t) and ("goal_id" in t) and ("creator" in t)
|
|
|
+ for t in queue["tasks"][:3]),
|
|
|
+ "queue rows carry the goal link fields + creator")
|
|
|
+ check(any(o["name"].startswith("#") for o in queue["enqueue_after_options"]),
|
|
|
+ "the create form's enqueue-after dropdown lists the global human tasks")
|
|
|
+ check(not any(int(o["id"]) == fan_task for o in queue["enqueue_after_options"]),
|
|
|
+ "the enqueue-after dropdown excludes the goal agent's tasks")
|
|
|
+
|
|
|
+ # The task page: mandate framing, goal reference section, the
|
|
|
+ # terminal session picker fix, and the description prefill.
|
|
|
+ task_page_html = http_get_page(auth, "/tasks/%d" % fan_task)
|
|
|
+ check("Mandate" in task_page_html and 'stm-markdown="task.description"' in task_page_html,
|
|
|
+ "the task page frames the description as the mandate")
|
|
|
+ conn = db()
|
|
|
+ agent_session_row = conn.execute(
|
|
|
+ "select id from sessions where task_id = ?", (run_task,)).fetchone()
|
|
|
+ conn.close()
|
|
|
+ terminal_state = slot_public(auth, "/tasks/%d/terminal" % run_task, "terminal")
|
|
|
+ session_rows = terminal_state["sessions"]
|
|
|
+ check(len(session_rows) >= 4
|
|
|
+ and all(("raw" in s) for s in session_rows),
|
|
|
+ "the terminal state lists every goal-run session with per-session raw flags")
|
|
|
+ check(int(terminal_state["session_id"]) == int(session_rows[-1]["id"]),
|
|
|
+ "the terminal default selects the LATEST session (was: the first)")
|
|
|
+ terminal_html = http_get_page(auth, "/tasks/%d/terminal" % run_task)
|
|
|
+ check("requestedSessionId" in terminal_html and "data-session-id" in terminal_html,
|
|
|
+ "the terminal client owns the ?session_id= selection (query params never reach entrypoints)")
|
|
|
+ detail = task_state(auth, fan_task)
|
|
|
+ check(detail.get("goal_id") == goal1 and detail.get("goal_title") == "Ship the flange"
|
|
|
+ and detail.get("creator") == "goal",
|
|
|
+ "a goal task's page carries the goal reference section data")
|
|
|
+ run_detail = task_state(auth, run_task)
|
|
|
+ check(run_detail.get("is_goal_run") is True and run_detail.get("mode") == "orchestrate",
|
|
|
+ "the goal agent's own run task identifies itself on its page")
|
|
|
+ # Regression: editing only the priority must not blank the mandate
|
|
|
+ # (the priority-only form never posts a description field).
|
|
|
+ act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
|
|
|
+ "project_id": "1", "title": "Mandate keeper", "priority": "4",
|
|
|
+ "description": "Keep this mandate intact."})
|
|
|
+ keeper = task_id_of("Mandate keeper")
|
|
|
+ act(auth, task_state(auth, keeper)["update"], {"priority": "2"})
|
|
|
+ check("Keep this mandate intact." == task_state(auth, keeper)["description"],
|
|
|
+ "a priority-only edit leaves the mandate untouched")
|
|
|
+ # And the full edit form prefills the mandate (carried fix).
|
|
|
+ task_page_html = http_get_page(auth, "/tasks/%d" % keeper)
|
|
|
+ check('stm-attribute.value="task.description"' in task_page_html,
|
|
|
+ "the edit expander pre-populates the mandate textarea")
|
|
|
+
|
|
|
|
|
|
BUNDLES_ORDER = ("pages", "dispatch", "subtasks", "lineage",
|
|
|
"allocation", "lifecycle", "answers", "push", "goals")
|