"""The pages bundle (moved verbatim from the pre-split web_stack_test.py; see test/harness.py for the shared machinery and the runner).""" from harness import * # noqa: F401,F403 — the shared wire helpers # ============================================================================= # Bundle: pages — block presentation, diff URLs, computed display state, # terminal rendering (rejected entries, ANSI SGR) and task-page layout. # Fixture-driven: tasks and sessions are inserted as rows; blocks are # raised through the push-era wire (POST /api/saddle/results with a # blocked state) or inserted directly for terminal-task fixtures; only # the rejected-ride scenario dispatches. # ============================================================================= def run(ctx): auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token app = ctx.app def raise_block(session_id, question, kind="question", options=None, body=None): """The push-era block raise: a blocked round-results push carrying the block payload (the poll-era /update channel is gone — 404 below pins that).""" block = {"question": question, "type": kind} if options is not None: block["options"] = options if body is not None: block["body"] = body return saddle_api("POST", "/api/saddle/results", fake_token, {"session_id": session_id, "state": "blocked", "block": block}) check(True, "login yields an auth slot") print("== the poll-era wire is gone (arch 19) ==") # The daemon never called update/poll (verified against saddle/src); # the routes 404 for any caller, authenticated or not. status, _ = saddle_api("POST", "/api/saddle/%d/update" % fake_id, fake_token, {"type": "status", "session_id": 1, "data": {"state": "working"}}) check(status == 404, "POST /api/saddle/{id}/update is gone (404)") status, _ = saddle_api("GET", "/api/saddle/%d/poll" % fake_id, fake_token) check(status == 404, "GET /api/saddle/{id}/poll is gone (404)") print("== blocks: push-era raises + body presentation ==") conn = db() conn.execute("insert into tasks (project_id, title, status, priority," " created_at)" " values (1, 'Block display task', 'working', 5," " strftime('%s','now'))") plan_task = conn.execute("select max(id) from tasks").fetchone()[0] conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at, outcome)" " values (?, ?, strftime('%s','now'), strftime('%s','now'), 'done')", (plan_task, fake_id)) conn.commit() session_id = conn.execute("select max(id) from sessions").fetchone()[0] conn.close() status, _ = raise_block(session_id, "Which database?", kind="question", options=["SQLite", "Postgres"], body="# Storage choice") check(status == 200, "question block raised through the results push") print("== B1: highlight-to-comment quote dialog ==") status, page = http("GET", "/tasks/%d" % plan_task, timeout=15) check(status == 200 and "quote-comment-dialog" in page and "quote-text" in page, "task page carries the quote-comment dialog the popover opens") check('name="quote"' in page and 'name="return_to"' in page, "quote dialog posts the quote and return-to fields") status, page = http("GET", "/tasks/%d/log" % plan_task, timeout=15) check(status == 200 and "data-log-feed" in page and "data-log-more" in page, "the event log page renders the feed and the load-more control") print("== B3: review section hides the duplicate body ==") status, _ = raise_block(session_id, "Review the diff", kind="review", body="# The change\n\nReworks the flange module.") check(status == 200, "review block raised through the results push") detail = task_state(auth, plan_task) review_block = [b for b in detail["blocks"] if b["block_type"] == "review"][0] check(detail.get("review_pending") is True, "review section shows while the review is unanswered") check(review_block.get("show_body") is False, "pending review block hides its duplicate body preview") check(review_block.get("body", "").startswith("# The change"), "review body itself still arrives in state (B2 counterpart)") act(auth, review_block["answer_action"], {"verdict": "accept"}) detail = task_state(auth, plan_task) review_block = [b for b in detail["blocks"] if b["block_type"] == "review"][0] check(review_block.get("show_body") is True, "answered review shows the body again (banner gone)") check(review_block.get("answer") == "Review approved. Continue with the task.", "the review accept form's verdict marker resolves to the stable wording") # Plan 16 S2/R4: no verdict constant ships in any template — the # accept forms carry only the verdict marker. task_page_html = http_get_page(auth, "/tasks/%d" % plan_task) check("Plan approved. Proceed with implementation." not in task_page_html and "Review approved. Continue with the task." not in task_page_html, "the task page ships no verdict constants (server-owned wording)") check('name="verdict" value="accept"' in task_page_html, "the accept forms post the verdict marker") # Plan 16 S5 (U1): verdict-shaped blocks keep their explicit # buttons + free-text path; a question with NULL options renders # the question form (radio "other" + free reply), never a # free-text-only verdict. check("Request changes" in task_page_html and 'name="feedback"' in task_page_html, "the review verdict keeps Request changes with free-text feedback (U1)") check('value="__other__"' in task_page_html and 'name="other"' in task_page_html, "question blocks render the option/other question form (U1)") status, _ = raise_block(session_id, "Free-text thought?", kind="question") check(status == 200, "a NULL-options question block raises") bare_question = [b for b in task_state(auth, plan_task)["blocks"] if b["question"] == "Free-text thought?"][0] check(bare_question.get("options") == [] and bare_question.get("block_type") == "question", "a NULL-options question stays a question (no options, not a verdict)") print("== B2: question block body above options ==") # Contract side (A2 will restore bodies end-to-end): a question # block raised with a heredoc body carries it; the body preview # rides above the options (template order) and survives answering # alongside the selected answer. (Blocks list newest-first; the # U1 fixture above raised a second, options-less question.) detail = task_state(auth, plan_task) question = [b for b in detail["blocks"] if b["block_type"] == "question" and b["body"] == "# Storage choice"][0] check(question.get("body") == "# Storage choice" and question.get("show_body") is True, "question block keeps its body markdown with the preview on") act(auth, question["answer_action"], {"option": "SQLite"}) detail = task_state(auth, plan_task) question = [b for b in detail["blocks"] if b["block_type"] == "question" and b["body"] == "# Storage choice"][0] check(question.get("answered") is True and question.get("answer") == "SQLite" and question.get("body") == "# Storage choice" and question.get("show_body") is True, "answered question shows body + selected answer") print("== B5: compare-changes SSH -> HTTPS ==") def diff_url_for_project(git_url, suffix): state = slot_public(auth, "/projects", "projects") act(auth, state["create"], {"name": "diff-%s" % suffix, "git_url": git_url}) project_id = int([p["id"] for p in slot_public(auth, "/projects", "projects")["projects"] if p["name"] == "diff-%s" % suffix][0]) conn = db() conn.execute("insert into tasks (project_id, title, status, task_branch," " branch_from, created_at) values (?, ?, 'working', 'feature/x'," " 'main', strftime('%s','now'))", (project_id, "Diff %s" % suffix)) conn.commit() task_id = conn.execute("select max(id) from tasks").fetchone()[0] conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at," " outcome) values (?, ?, strftime('%s','now'), strftime('%s','now')," " 'done')", (task_id, fake_id)) conn.commit() session = conn.execute("select max(id) from sessions").fetchone()[0] conn.close() status, _ = raise_block(session, "Review the diff", kind="review") return task_state(auth, task_id).get("review_diff_url") or "" check(diff_url_for_project("ssh://fabrica.unitatem.net:222/clanker/clanker-saddle.git", "fabrica") == "https://fabrica.unitatem.net/clanker/clanker-saddle/compare/main...feature/x", "ssh://git@host:port/owner/repo.git translates (port dropped)") check(diff_url_for_project("git@github.com:acme/widgets.git", "scp") == "https://github.com/acme/widgets/compare/main...feature/x", "git@host:owner/repo.git translates to the browse host") check(diff_url_for_project("https://example.com/plain/repo", "plain") == "https://example.com/plain/repo/compare/main...feature/x", "HTTPS URLs pass through unchanged") print("== B6: computed display state ==") # Draft tasks display as draft (plan 11) and never dispatch. act(auth, slot_public(auth, "/tasks", "tasks")["create"], { "project_id": "1", "title": "Draft display task", "priority": "5", }) draft_display = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"] if t["title"] == "Draft display task"][0]) detail = task_state(auth, draft_display) check(detail["status"] == "draft" and detail.get("display") == "draft", "new tasks default to draft and display it") check(detail.get("draft") is True and detail.get("enqueue") is not None and detail.get("delete") is not None, "draft tasks expose the enqueue + delete actions") print("== B6b: create-form branch fields (goal 16) ==") # The queue form carries both branch rows; they post through # tasks.create into the stored task (empty optional -> null, # branch_from prefill from the project's default branch). tasks_page = http_get_page(auth, "/tasks") check('name="task_branch"' in tasks_page and "Task branch name" in tasks_page, "the create form renders the Task branch name input") check('name="branch_from"' in tasks_page and "Branch from" in tasks_page, "the create form renders the Branch from input") check("default: the project's default branch" in tasks_page, "the Branch from label hints at the project default") act(auth, slot_public(auth, "/tasks", "tasks")["create"], { "project_id": "1", "title": "Branch fields task", "priority": "5", "task_branch": "task/999-branch-fields", "branch_from": "release/2026", }) detail = task_state(auth, task_id_of("Branch fields task")) check(detail["task_branch"] == "task/999-branch-fields" and detail["branch_from"] == "release/2026", "posted branch fields store on the created task") # Both optional: empty posts leave task_branch null and prefill # branch_from from the project's default branch (demo -> main). act(auth, slot_public(auth, "/tasks", "tasks")["create"], { "project_id": "1", "title": "Default branch fields task", "priority": "5", }) detail = task_state(auth, task_id_of("Default branch fields task")) check((detail.get("task_branch") is None or detail.get("task_branch") == "") and detail.get("branch_from") == "main", "empty posts leave task_branch null and prefill branch_from" " from the project default") check(quiet_window(quiet_seconds(), lambda: task_state(auth, draft_display)["status"] == "draft", "draft task never dispatches"), "a draft task is never dispatched or woken for") # Plan 12: the waiting-on-drafts block is deleted (goal tasks are # the goal agent's to sequence); a parked task displays plainly. conn = db() conn.execute("insert into tasks (project_id, title, status, priority, created_at)" " values (1, 'Parked plain task', 'suspended', 5, strftime('%s','now'))") conn.commit() conn.close() parked_id = db().execute("select max(id) from tasks").fetchone()[0] detail = task_state(auth, parked_id) check(detail.get("display") == "parked", "a suspended task displays as parked (%s)" % detail.get("display")) dash = slot_public(auth, "/", "dashboard") check(not dash.get("waiting_drafts"), "the waiting-on-drafts block is gone from the dashboard data") status, home_page = http("GET", "/", timeout=15) check(status == 200 and "Waiting on subtasks to be enqueued" not in home_page, "the dashboard renders no waiting-on-drafts section") conn = db() conn.execute("insert into tasks (project_id, title, status, priority, created_at)" " values (1, 'Parent', 'done', 5, strftime('%s','now'))") parent6 = conn.execute("select max(id) from tasks").fetchone()[0] # The post-mortem case: stored done WITH an unanswered review block. # (A terminal task consumes result pushes silently — plan 13 B2 — # so the row is inserted directly, the goalui deep-link pattern.) conn.execute("insert into task_blocks (task_id, question, type, created_at)" " values (?, 'Review the final diff', 'review'," " strftime('%s','now'))", (parent6,)) conn.commit() conn.close() detail = task_state(auth, parent6) check(detail.get("display") == "review requested" and detail.get("terminal") is True, "unanswered review overrides the stored done status (terminal flag keeps stored truth)") queue = [t for t in slot_public(auth, "/tasks", "tasks")["tasks"] if int(t["id"]) == parent6][0] check(queue["display"] == "review requested", "queue row shows the computed review-requested display") # With the review answered the display falls back to the stored # status (the plan-12 shape: no subtask-delegation states). conn = db() conn.execute("update task_blocks set answered_at = strftime('%s','now')," " answer = 'Human approved the review' where task_id = ?", (parent6,)) conn.commit() conn.close() detail = task_state(auth, parent6) check(detail.get("display") == "done" and detail.get("terminal") is True, "answered review falls back to the stored done display (%s)" % detail.get("display")) # Flags track the stored status on failure. conn = db() conn.execute("update tasks set status = 'failed', completed_at = strftime('%s','now')" " where id = ?", (parent6,)) conn.commit() conn.close() detail = task_state(auth, parent6) check(detail.get("display") == "failed" and detail.get("failed") is True and detail.get("terminal") is True, "failed task with answered review shows failed + terminal flags") print("== rejected entries (non-fatal) ==") # The push wire: a rejected entry rides a round-results push, stores # on the session and streams through SSE; the task is untouched. status, body = saddle_api("POST", "/api/saddle/chunks", fake_token, { "session_id": session_id, "events": [{"seq": 9021, "kind": "tool_result", "actor": "harness", "payload": {"command": "bash -n script.sh", "stderr": "syntax error near line 3 (recoverable)", "exit_code": 1}}]}) check(status == 200 and int(body.get("stored") or 0) == 1, "a failed tool result rides the events push") request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % plan_task) with urllib.request.urlopen(request, timeout=15) as stream: result_event = None deadline = time.monotonic() + 8 while time.monotonic() < deadline and result_event is None: line = stream.readline().decode().strip() if not line.startswith("data: ") or line == "data: ping": continue event = json.loads(line[6:]) if (event.get("kind") == "tool_result" and "syntax error near line 3" in (event.get("payload", {}).get("stderr") or "")): result_event = event check(result_event is not None, "the event stream carries tool results with their reason") # The page template renders the feed and the filters. status, page = http("GET", "/tasks/%d/log" % plan_task, timeout=15) check(status == 200 and "data-log-filter" in page and "data-log-search" in page, "the event log page renders filters and search") print("== B8: byte-exact payload delivery over the event stream ==") # Byte-exact delivery: ANSI-coloured turn text flows through the # ledger + SSE untouched (the client renderer styles them). coloured = "\x1b[1;32mbold green\x1b[0m then \x1b[38;5;196m256-red\x1b[0m\n" status, body = saddle_api("POST", "/api/saddle/chunks", fake_token, { "session_id": session_id, "events": [{"seq": 9100, "kind": "turn_text", "payload": {"turn": 9, "text": coloured}}]}) check(status == 200 and int(body.get("stored") or 0) == 1, "the coloured turn text stores as one event") request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % plan_task) with urllib.request.urlopen(request, timeout=15) as stream: coloured_event = None deadline = time.monotonic() + 8 while time.monotonic() < deadline and coloured_event is None: line = stream.readline().decode().strip() if not line.startswith("data: ") or line == "data: ping": continue event = json.loads(line[6:]) if (event.get("kind") == "turn_text" and "\x1b[1;32m" in (event.get("payload", {}).get("text") or "")): coloured_event = event check(coloured_event is not None and "\x1b[38;5;196m" in coloured_event["payload"]["text"], "payload bytes stream byte-exact to the client renderer") print("== goal 9: the yeehaw celebration rides the ledger + stream ==") # The daemon's yeehaw tool emits a `yeehaw` ledger event (actor # model, payload {message}); it must store, stream and leave the # page wiring intact for the client renderer (the comments bucket # + .yeehaw-message span are pinned by test_task_log_js.py). whoop = "Finally cracked the SSE replay - every byte landed." status, body = saddle_api("POST", "/api/saddle/chunks", fake_token, { "session_id": session_id, "events": [{"seq": 9200, "kind": "yeehaw", "actor": "model", "payload": {"message": whoop}}]}) check(status == 200 and int(body.get("stored") or 0) == 1, "a yeehaw event stores on the session ledger (%s)" % body.get("error")) request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % plan_task) with urllib.request.urlopen(request, timeout=15) as stream: yeehaw_event = None deadline = time.monotonic() + 8 while time.monotonic() < deadline and yeehaw_event is None: line = stream.readline().decode().strip() if not line.startswith("data: ") or line == "data: ping": continue event = json.loads(line[6:]) if event.get("kind") == "yeehaw": yeehaw_event = event check(yeehaw_event is not None and yeehaw_event.get("actor") == "model" and yeehaw_event.get("payload", {}).get("message") == whoop, "the yeehaw event streams with its message intact") conn = db() stored_kind = conn.execute( "select kind, actor from events where session_id = ? and seq = 9200", (session_id,)).fetchone() conn.close() check(stored_kind is not None and stored_kind[0] == "yeehaw" and stored_kind[1] == "model", "the ledger row keeps the yeehaw kind + model actor") status, page = http("GET", "/tasks/%d/log" % plan_task, timeout=15) check(status == 200 and "data-log-feed" in page and "yeehaw" not in page.split("