浏览代码

qa: integration_test re-baselined onto the post-R3 world — the harness left behind by the R stages (75 passes + stale) is green again end-to-end against both real binaries, twice on fresh environments: 114/0 (was 100 checks pre-R3).

The legacy-wire section rides the push era: 401s pin the id-less POST /api/saddle/results bearer (no/invalid token), the deleted /update + /poll pair 404s for any caller (the subtask/plan refusal arms of plan 12 die with the channel, matching the unit pins), typed updates become typed entries on results pushes (comment authors its agent row output-first, query persists with its piped input, progress lands newest-first, review flips review_requested), the block raise is a blocked push carrying the payload, exactly-once moves to the daemon-global entry ids (a redelivered id is consumed silently, one projection), and a park-unwinding working push provably never resurrects the parked round. Verdicts ride the block answer actions as before; the harness now posts S2 shapes (verdict=accept markers, feedback text) instead of shipping the stable constants.

S1 canonical-on-accept: the pre-accept report reads the completion_pending_block body with goals.completion_report pinned EMPTY; after the accept, the winning block's body is pinned equal to the canonised completion_report. U2: the mechanical bookkeeping (restores, park notices) is pinned to the RAW terminal (typed system session_commands) and out of every user-facing thread and the narrative kinds. D4: every run/worker session closes exactly once — parks stamp outcome 'parked', none dangle. Item 23: a mid-run pin derives busy from the worker's open session (the mock now lingers 0.5s per reply — a local instant mock could open-and-close a worker inside one sampling interval), the quiesce pin stays the derived idle, and the fresh-DB schema diff the joint QA asked for asserts the gone surfaces stay gone (saddles.current_task_id/allocated_at, tasks.working_branch, api_tokens).

The mock's plan revision rides --on-plan-feedback (the feedback verdict now routes there since the server wraps it in the stable prefix) with the Plan v2 body moved out of the fallback script — the worker's degenerate first round stays its FIRST round. No product change: the S1/U2/D4/derivation behaviours were verified correct against both binaries (a planted open session provably reads busy/current-task through the derived view); every failure in the 75-pass baseline was a stale assertion.
clanker 1 月之前
父节点
当前提交
903361929c
共有 2 个文件被更改,包括 190 次插入 和 84 次删除
  1. 4 2
      README.md
  2. 186 82
      test/integration_test.py

+ 4 - 2
README.md

@@ -279,8 +279,10 @@ callbacks → the worker's ephemeral worktree (auto-pruned, branch kept)
 push-before-review → completion review accept (gated on the live
 per-repo push flags) → goal complete, plus the `/status`
 `goal_branches` live probe flags, SSE over the agent's raw terminal,
-the DERIVED goal narrative, the legacy ingest API surface and the
-stm-markdown renderer. Host run (the harness prepares a `demo` repo
+the DERIVED goal narrative, the push wire surface (typed entries and
+block raises riding POST /api/saddle/results, the deleted poll-era
+endpoints 404ing) and the stm-markdown renderer. Host run (the harness
+prepares a `demo` repo
 with a pushable bare origin when the workspace is empty; the database
 must be FRESH — the harness refuses a used one):
 

+ 186 - 82
test/integration_test.py

@@ -36,10 +36,12 @@ The consolidated goal-era arc (plan 15, re-baselined for C+D):
    complete, run task done
 7. the goal agent's terminal sessions (SSE replay), the audit trail
    (the DERIVED goal narrative, typed session_commands, the
-   harness-authored system rows), the /status goal_branches cache
-   sampled by the manual saddle refresh
-8. the legacy ingest API surface (401s, typed updates, exactly-once
-   poll handover) and the stm-markdown renderer
+   harness-authored bookkeeping riding the RAW terminal only), the
+   /status goal_branches cache sampled by the manual saddle refresh
+8. the push wire surface (401s on the id-less routes, typed entries
+   and block raises riding POST /api/saddle/results with per-entry
+   exactly-once dedup, the deleted poll-era endpoints 404ing) and the
+   stm-markdown renderer
 
 Prerequisites (see README.md "Integration testing"): the web app
 running with SPRY_SEED_ADMIN_USERNAME=admin,
@@ -95,8 +97,8 @@ CAPTURE_PATH = os.environ.get("CAPTURE_PATH",
                               os.path.join(WORKSPACE, "..", "mock-capture.jsonl"))
 DEMO = os.path.join(WORKSPACE, "demo")
 HOSTNAME = socket.gethostname().split(".")[0]
-PLAN_APPROVED = "Plan approved. Proceed with implementation."
-REVIEW_APPROVED = "Review approved. Continue with the task."
+# The verdict wording is SERVER-OWNED (plan 16 S2/R4): the harness
+# posts `verdict` markers / `feedback` text and never the constants.
 
 passes = 0
 failures = 0
@@ -305,9 +307,6 @@ def mock_argv():
         "# two commits and one file — the blast radius is greeting.txt alone\n"
         + (heredoc % 'saddle block question "Demo only" "Whole workspace"') % (
             "Which scope should the greeting cover? The demo repo is small; confirm the blast radius."),
-        # R3 (plan revision after feedback): the rejected plan, re-submitted
-        (heredoc % "saddle plan") % (
-            "# Plan v2\n\n1. Add greeting.txt to the demo repo.\n2. Export it for reuse."),
         # W1 (the worker's first round): a DEGENERATE repetition (plan
         # 15 D2) — the validator catches it before feeding; nothing
         # executes, one rejection receipt, the round is consumed
@@ -327,6 +326,10 @@ def mock_argv():
         # the question's verdict ("Human selected: Demo only") -> plan v1
         "--on-answer", (heredoc % "saddle plan") % (
             "# Plan v1\n\n1. Add greeting.txt to the demo repo."),
+        # the plan-feedback verdict -> the revision (the export step
+        # incorporates the harness's feedback)
+        "--on-plan-feedback", (heredoc % "saddle plan") % (
+            "# Plan v2\n\n1. Add greeting.txt to the demo repo.\n2. Export it for reuse."),
         # the approval verdict -> task fan-out + suspend
         "--on-plan-approved", fanout,
         # the batched "Goal update" resume -> merge, prune, push, complete
@@ -342,6 +345,10 @@ def mock_argv():
             "# Goal completion report\n\n- achieved: greeting.txt added via one worker task\n"
             "- verified: the goal branch is pushed to origin"),
         "--on-query", "The hostname is %s." % HOSTNAME,
+        # Every reply lingers half a second: the local mock answers so
+        # instantly that a worker's whole run (its DERIVED-busy open
+        # window) can close inside one sampling interval otherwise.
+        "--delay", "0.5",
         # receipt forensics: every prompt the mock received, so the
         # harness can assert the D-side receipts reached the model
         "--capture", CAPTURE_PATH,
@@ -518,7 +525,10 @@ def main():
               "the pending plan block renders on the goal page")
         pending = goal_state(auth, goal1)["plan_pending_block"]
         check("Plan v1" in pending["body"], "the plan block carries the submitted markdown")
-        act(auth, pending["answer_action"], {"answer": "Feedback: also export the greeting"})
+        # Plan 16 S2/R4: the goal page posts a `feedback` reply / a
+        # `verdict` marker — the server owns the stable wording the
+        # daemon's resume matching reads.
+        act(auth, pending["answer_action"], {"feedback": "also export the greeting"})
         check(wait_goal_status(auth, goal1, "planning", timeout=30) is not None,
               "rejecting with feedback returns the goal to planning")
         check(wait_for(lambda: goal_state(auth, goal1).get("plan_pending_block") not in
@@ -529,7 +539,7 @@ def main():
         revised = goal_state(auth, goal1)
         check(len([r for r in revised["plan_revisions"] if r.get("answered")]) >= 1,
               "the rejected revision stays discoverable behind the expander")
-        act(auth, revised["plan_pending_block"]["answer_action"], {"answer": PLAN_APPROVED})
+        act(auth, revised["plan_pending_block"]["answer_action"], {"verdict": "accept"})
         check(wait_goal_status(auth, goal1, "active", timeout=30) is not None,
               "approving with the agreed wording activates the goal")
 
@@ -554,6 +564,21 @@ def main():
             "select suspended_at is not null from tasks where id = ?", (run_task,))[0][0] or False,
             timeout=30, what="the goal agent parks") is not None,
             "the goal agent parked suspended after fanning out (suspended_at stamped)")
+        # Plan 16 item 23: busy DERIVES from the open session (the
+        # schema carries no work cache) — while the worker runs, the
+        # card reads busy with the worker as its current task. The
+        # open window can be sub-second (the local mock answers
+        # instantly), so the card read shares the predicate iteration
+        # with the open-session probe.
+        check(wait_for(lambda: bool(db_query(
+            "select 1 from sessions where task_id = ? and ended_at is null",
+            (worker_task,))[0:1]) and (lambda card: card is not None
+                                       and card["status"] == "busy"
+                                       and int(card["current_task_id"] or 0) == worker_task)(
+            next((s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
+                  if s["name"] == SADDLE_NAME), None)) or False,
+            timeout=90, interval=0.2, what="derived busy while the worker runs") is not None,
+              "the saddle card derives busy from the worker's open session (no cached row)")
 
         print("== the worker's ephemeral worktree ==")
         branch_rows = wait_for(lambda: db_query(
@@ -598,8 +623,14 @@ def main():
         check(wait_goal_status(auth, goal1, "completion_review", timeout=120) is not None,
               "the resumed agent merges, pushes and enters completion review")
         state = goal_state(auth, goal1)
-        check("greeting.txt added via one worker task" in state.get("completion_report", ""),
-              "the completion report renders on the goal page")
+        completion = state["completion_pending_block"]
+        # Plan 16 S1 (canonical-on-accept): pending, the report lives
+        # ONLY in the block — goals.completion_report stays empty
+        # until the accept verdict writes the winning body.
+        check("greeting.txt added via one worker task" in (completion or {}).get("body", ""),
+              "the completion report renders on the goal page (the pending block's body)")
+        check(not (state.get("completion_report") or ""),
+              "goals.completion_report stays clean before the verdict (canonical-on-accept)")
         merged = subprocess.run(
             ["git", "-C", goal_clone, "show", "goal/demo-run:greeting.txt"],
             capture_output=True, text=True).stdout
@@ -628,7 +659,7 @@ def main():
             for b in goal_state(auth, goal1).get("push_branches", [])) or False,
             timeout=60, what="unpushed chip") is not None,
               "the rewound origin flips the live push chip to unpushed")
-        refusal = act(auth, completion["answer_action"], {"answer": REVIEW_APPROVED})
+        refusal = act(auth, completion["answer_action"], {"verdict": "accept"})
         check(any(d.get("type") == "notify" and d.get("kind") == "error"
                   and "Origin is behind" in (d.get("message") or "")
                   for d in refusal),
@@ -649,23 +680,29 @@ def main():
         completion = goal_state(auth, goal1)["completion_pending_block"]
 
         print("== completion accept ==")
-        check(completion and completion.get("body", "") == state.get("completion_report", ""),
+        check("greeting.txt added via one worker task" in (completion or {}).get("body", ""),
               "the run task's completion block carries the report")
-        accept = act(auth, completion["answer_action"], {"answer": REVIEW_APPROVED})
+        winning_body = (completion or {}).get("body", "")
+        accept = act(auth, completion["answer_action"], {"verdict": "accept"})
         check(not any(d.get("type") == "notify" and d.get("kind") == "error" for d in accept),
               "the accept passes once the push flags read pushed")
         check(wait_goal_status(auth, goal1, "complete", timeout=30) is not None,
-              "accepting with the agreed wording completes the goal")
+              "accepting the verdict marker completes the goal (server-owned wording)")
         check(wait_task_status(auth, run_task, "done", timeout=30) is not None,
               "the goal-run task closes done with the goal")
+        final = goal_state(auth, goal1)
+        check(final.get("completion_report") == winning_body,
+              "the accept canonises the winning block's body as goals.completion_report (S1)")
 
         print("== audit trail (the derived narrative) ==")
         narrative = goal_state(auth, goal1)["events"]
         kinds = [e["kind"] for e in narrative]
         for expected in ("started", "block-raised", "plan-submitted", "plan-rejected",
                          "plan-approved", "task-created", "task-done",
-                         "completion-submitted", "goal-approved", "system"):
+                         "completion-submitted", "goal-approved"):
             check(expected in kinds, "the derived narrative records %s" % expected)
+        check("system" not in kinds,
+              "mechanical system rows never reach the user-facing narrative (U2: raw log only)")
         check(all(k != "task-enqueued" for k in kinds),
               "timestamp-less bookkeeping never returns (task-enqueued is gone)")
         check(db_query(
@@ -684,6 +721,20 @@ def main():
               "the worker's terminal carries typed entries (%s)" % sorted(worker_types))
 
         print("== the terminal watermark (C3) drives the resume ==")
+        # The fresh-DB schema diff (the joint-QA gate): the removed
+        # surfaces stay removed — the saddle work cache (item 23), the
+        # /update-era columns (arch 19) and the legacy token table
+        # (arch 24).
+        saddle_cols = {row[1] for row in db_query("pragma table_info('saddles')")}
+        check(not ({"current_task_id", "allocated_at"} & saddle_cols),
+              "the saddles work-cache columns are gone (sessions are truth)")
+        task_cols = {row[1] for row in db_query("pragma table_info('tasks')")}
+        check("working_branch" not in task_cols,
+              "tasks.working_branch is gone (only the deleted /update done wrote it)")
+        check(db_query(
+            "select count(*) from sqlite_master where type = 'table'"
+            " and name = 'api_tokens'")[0][0] == 0,
+            "the api_tokens table is gone (saddles.token is the single credential store)")
         watermark_row = db_query(
             "select last_terminal_watermark from goals where id = ?", (goal1,))
         worker_done_at = db_query(
@@ -693,23 +744,39 @@ def main():
               "the goals row's terminal watermark advanced past the worker's transition"
               " (the resume consumed it)")
 
-        print("== system rows on the ONE thread (C2) ==")
+        print("== the harness bookkeeping rides the RAW terminal (C2 + U2) ==")
+        # Plan 16 S5 (U2): the mechanical rows (restores, park
+        # notices) author NO comment rows — they persist as typed
+        # 'system' session_commands (the raw terminal log) and never
+        # render on a user-facing timeline.
+        raw_system = db_query(
+            "select sc.command_text, sc.output_text from session_commands sc"
+            " join sessions s on s.id = sc.session_id where s.task_id = ?"
+            " and sc.type = 'system'", (run_task,))
+        check(any("session context restored" in (row[1] or "") for row in raw_system)
+              and any("parked until an owned task" in (row[1] or "") for row in raw_system),
+              "the harness bookkeeping (restore, park) persists in the raw terminal log")
         thread = goal_state(auth, goal1)["comments"]
-        system_rows = [c for c in thread if c.get("is_system")]
-        check(system_rows and all(c["author"] == "system" for c in system_rows)
-              and not any(c.get("from_agent") for c in system_rows)
-              and any("session context restored" in c["body"] for c in system_rows)
-              and any("parked until an owned task" in c["body"] for c in system_rows),
-              "the harness bookkeeping (restore, park) rides the thread as system rows,"
-              " never agent rows")
+        check(not any("session context restored" in c["body"] or "parked until" in c["body"]
+                      for c in thread),
+              "mechanical rows never render on the goal thread (U2)")
         run_thread = task_state(auth, run_task)["comments"]
-        check(any(c.get("is_system") and "session context restored" in c["body"]
-                  for c in run_thread),
-              "the task page reads the same thread (the system rows included)")
+        check(not any("session context restored" in c["body"] or "parked until" in c["body"]
+                      for c in run_thread),
+              "the task page reads the same filtered thread (no mechanical rows)")
         check(not any(c.get("from_agent") and "[harness]" in c["body"]
                       for c in thread),
               "no harness-authored row renders as agent content")
 
+        print("== parks close their sessions (D4) ==")
+        outcomes = db_query(
+            "select outcome, ended_at is not null from sessions where task_id in (?, ?)",
+            (run_task, worker_task))
+        check(bool(outcomes)
+              and all(row[0] in ("parked", "done") and row[1] for row in outcomes),
+              "every run/worker session closed exactly once — parks stamp outcome 'parked',"
+              " none dangle open (%s)" % sorted({row[0] for row in outcomes}))
+
         print("== D-side through both binaries: commentary + degenerate ==")
         run_bash = db_query(
             "select sc.command_text from session_commands sc join sessions s"
@@ -780,7 +847,7 @@ def main():
         check(any(d.get("type") == "subscribe" for d in pulse),
               "goal page subscribes to the pulse channel for live refresh")
 
-        print("== legacy ingest API surface ==")
+        print("== push wire surface (the poll-era endpoints deleted) ==")
         saddles_state = slot_public(auth, "/saddles", "saddles")
         act(auth, saddles_state["register"], {"name": "saddle-e2e-api", "host": "localhost:7199"})
         api_rows = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
@@ -794,7 +861,7 @@ def main():
         act(auth, tasks_state["create"], {
             "project_id": "1",
             "title": "API surface probe",
-            "description": "Typed updates, exactly-once poll",
+            "description": "Typed entries, exactly-once pushes",
             "priority": "6",
             "task_branch": "task/api-surface",
         })
@@ -808,73 +875,110 @@ def main():
         api_session = db.execute("select max(id) from sessions").fetchone()[0]
         db.close()
 
-        status, _ = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, None,
-                               {"type": "comment", "session_id": api_session, "data": {"body": "x"}})
-        check(status == 401, "update without a token is rejected")
-        status, _ = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, "saddle-not-a-token",
-                               {"type": "comment", "session_id": api_session, "data": {"body": "x"}})
-        check(status == 401, "update with an invalid token is rejected")
-
-        status, body = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                                  {"type": "comment", "session_id": api_session,
-                                   "data": {"body": "Surveyed the module"}})
-        check(status == 200 and body.get("ok"), "comment update accepted with token")
-        status, body = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                                  {"type": "block", "session_id": api_session, "data": {
-                                      "question": "Which database?",
-                                      "type": "question",
-                                      "options": ["SQLite", "Postgres"],
-                                      "body": "# Pick one"}})
-        check(status == 200 and body.get("block_id"), "typed block returns block_id")
+        def push(entry_id, entry_type, state="working", command="", output="",
+                 block=None, **extra):
+            """One round-results push in the daemon's shape (the
+            push-era wire the real daemon speaks — the poll-era
+            /update channel is gone, 404-pinned below)."""
+            payload = {"session_id": api_session, "state": state}
+            if entry_type is not None:
+                entry = {"id": entry_id, "command": command, "output": output,
+                         "exit_code": 0, "type": entry_type,
+                         "timestamp": "2026-09-13T00:00:00Z"}
+                entry.update(extra)
+                payload["entries"] = [entry]
+            if block is not None:
+                payload["block"] = block
+            return saddle_api("POST", "/api/saddle/results", api_token, payload)
+
+        status, _ = saddle_api("POST", "/api/saddle/results", None,
+                               {"session_id": api_session, "state": "working"})
+        check(status == 401, "results push without a token is rejected")
+        status, _ = saddle_api("POST", "/api/saddle/results", "saddle-not-a-token",
+                               {"session_id": api_session, "state": "working"})
+        check(status == 401, "results push with an invalid token is rejected")
+
+        # The poll-era ingest pair is GONE (arch 19): both routes 404
+        # for any caller, authenticated or not — the subtask/plan
+        # refusal arms (plan 12 removal) died with the channel.
         status, _ = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                               {"type": "output", "session_id": api_session, "data": {
-                                   "command": "cat notes.md | saddle query 'meaning?'",
-                                   "output": "42",
-                                   "type": "query",
-                                   "input": "# notes\nmeaning of life"}})
-        check(status == 200, "query output update accepted")
+                               {"type": "status", "session_id": api_session,
+                                "data": {"state": "working"}})
+        check(status == 404, "POST /api/saddle/{id}/update is gone (404)")
+        status, _ = saddle_api("GET", "/api/saddle/%d/poll" % api_saddle, api_token)
+        check(status == 404, "GET /api/saddle/{id}/poll is gone (404)")
         status, _ = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                               {"type": "progress", "session_id": api_session,
-                                "data": {"summary": "Migrated the schemas"}})
-        check(status == 200, "progress update accepted")
+                               {"type": "subtask", "session_id": api_session,
+                                "data": {"title": "Ghost child"}})
+        check(status == 404,
+              "the /update subtask arm is gone with the channel (404, plan 12 removal)")
         status, _ = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                               {"type": "review", "session_id": api_session,
-                                "data": {"summary": "Migration **ready** for review"}})
-        check(status == 200, "review update accepted")
-        status, body = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                                  {"type": "plan", "session_id": api_session,
-                                   "data": {"plan": "# Plan\n- migrate schemas"}})
-        check(status == 400 and "gone" in (body.get("error") or ""),
-              "the task-plan update type fails loudly (plan 12 removal)")
-        status, body = saddle_api("POST", "/api/saddle/%d/update" % api_saddle, api_token,
-                                  {"type": "subtask", "session_id": api_session, "data": {}})
-        check(status == 400 and "gone" in (body.get("error") or ""),
-              "the subtask update type fails loudly (plan 12 removal)")
+                               {"type": "plan", "session_id": api_session,
+                                "data": {"plan": "# nope"}})
+        check(status == 404,
+              "the /update plan arm is gone with the channel (404, plans are goal-only)")
+
+        status, body = push(9001, "comment", command="saddle comment",
+                            output="Surveyed the module")
+        check(status == 200 and body.get("ok"), "comment entry accepted on the results push")
+        check(any(c.get("from_agent") and c["body"] == "Surveyed the module"
+                  for c in task_state(auth, api_task)["comments"]),
+              "the comment entry authors an agent row (output-first)")
+        status, body = push(None, None, state="blocked", block={
+            "question": "Which database?", "type": "question",
+            "options": ["SQLite", "Postgres"], "body": "# Pick one"})
+        check(status == 200 and body.get("ok"),
+              "the blocked push raises the question through the results channel")
+        parked = db_query("select outcome from sessions where id = ?", (api_session,))[0][0]
+        check(parked == "parked",
+              "the raise parked the round's session (outcome 'parked', D4)")
+        status, body = push(9002, "query", command="cat notes.md | saddle query 'meaning?'",
+                            output="42", input="# notes\nmeaning of life")
+        check(status == 200, "query entry accepted on the results push")
+        query_rows = db_query(
+            "select sc.type, sc.piped_input from session_commands sc"
+            " where sc.session_id = ? and sc.command_text like '%saddle query%'",
+            (api_session,))
+        check(query_rows and query_rows[0][0] == "query"
+              and query_rows[0][1] == "# notes\nmeaning of life",
+              "the query entry persists typed with its piped input")
+        status, body = push(9003, "progress", command="saddle progress-update",
+                            output="Migrated the schemas")
+        check(status == 200, "progress entry accepted on the results push")
+        status, body = push(9003, "progress", command="saddle progress-update",
+                            output="Migrated the schemas AGAIN")
+        check(status == 200, "the redelivered entry id is consumed silently (200, no projection)")
+        check(len([p for p in task_state(auth, api_task)["progress"]
+                   if p["summary"] in ("Migrated the schemas", "Migrated the schemas AGAIN")])
+              == 1 and task_state(auth, api_task)["progress"][0]["summary"] == "Migrated the schemas",
+              "entries project exactly once per daemon-global id (push-era exactly-once)")
+        status, body = push(9004, "review", command="saddle review",
+                            output="Migration **ready** for review")
+        check(status == 200, "review entry accepted on the results push")
 
         detail = task_state(auth, api_task)
-        check(detail["status"] == "review_requested", "review flips the task status")
+        check(detail["status"] == "review_requested", "the review entry flips the task status")
         check([b["options"] for b in detail["blocks"]
                if b["block_type"] == "question"] == [["SQLite", "Postgres"]],
-              "question block carries its options")
+              "the pushed block carries its options")
         check(detail["progress"][0]["summary"] == "Migrated the schemas",
               "progress history newest-first")
+        status, _ = push(None, None, state="working")
+        check(status == 200 and task_state(auth, api_task)["status"] == "review_requested",
+              "a park-unwinding working push never resurrects the parked round")
 
         question = [b for b in detail["blocks"] if b["block_type"] == "question"][0]
         act(auth, question["answer_action"], {"option": "SQLite"})
         check([b["answer"] for b in task_state(auth, api_task)["blocks"]
                if b["block_type"] == "question"] == ["SQLite"],
-              "option answer stored verbatim (the resume notice formats it)")
+              "option answer stored verbatim (the resuming assignment formats it)")
         act(auth, detail["comment"],
             {"body": "Check the migration output", "quote": "ERROR: table demos exists"})
-        status, body = saddle_api("GET", "/api/saddle/%d/poll" % api_saddle, api_token)
-        check(any(a["answer"] == "SQLite" for a in body.get("answers", [])),
-              "poll hands over the block answer")
-        check(any(c["body"] == "Check the migration output" and "ERROR" in c["quote"]
-                  for c in body.get("comments", [])),
-              "poll hands over the quoted user comment")
-        status, body = saddle_api("GET", "/api/saddle/%d/poll" % api_saddle, api_token)
-        check(body.get("answers") == [] and body.get("comments") == [],
-              "poll delivers answers and comments exactly once")
+        check(any(c["body"] == "Check the migration output" and "ERROR" in (c.get("quote") or "")
+                  and not c.get("from_agent")
+                  for c in task_state(auth, api_task)["comments"]),
+              "the quoted user comment lands on the task thread (delivery rides the"
+              " assignment carrier / dispatcher pass — unit-pinned)")
         act(auth, detail["cancel"], {"task_id": str(api_task)})
 
         print("== stm-markdown ==")