| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144 |
- #!/usr/bin/env python3
- """End-to-end integration test: ClankerSaddle web app + saddle daemon.
- Drives the web app the way statum.js does (entrypoint state loads plus
- action POSTs carrying the auth slot and sealed private blobs) while a
- REAL saddle daemon runs a REAL goal against a real git checkout under
- WORKSPACE, powered by saddle/test/mock_llm.py with a deterministic
- tool-call script — both binaries, the true cross-binary contract proof
- (the per-repo fake-peer suits live in web_stack_test.py / saddle e2e).
- The protocol-native goal-era arc (PLAN-17; every agent turn is a tool
- call, questions ride `saddle block` through run_command):
- 1. goal create -> start: the goal-run assignment reaches the daemon
- over the ask/allocate channel with the full plan-12 payload (kind/
- mode/creator/goal context/ephemeral branches for workers)
- 2. a narrated probe round (turn_text + tool_call/tool_result events in
- the ledger) precedes the pre-plan question block -> answered on the
- goal page -> the ONE answer resolves the goal-run task's single
- task_blocks row and the parked goal agent resumes with the verdict
- 3. submit_plan -> reject-with-feedback -> revision -> approval (the
- agreed verdict words ride the resume)
- 4. task fan-out through the create_task/enqueue_task tools (the real
- /api/saddle/task/create + enqueue callbacks); the worker's repeated
- identical run_command probes trip the loop warning (never fatal),
- then the clean run works in its ephemeral worktree branch cut from
- the goal clone, commits, finishes — worktree auto-pruned, branch
- kept
- 5. the terminal transition (the goals row's last_terminal_watermark)
- coalesces (~SADDLE_GOAL_RESUME_S) into the goal agent's batched
- resume; it merges, deletes the task branch, pushes the goal branch
- (push-before-review, enforced daemon-side at the completion
- attempt) and submit_report enters completion review
- 6. completion review accept -> goal complete, run task done (no
- web-side push gate: the daemon already refused the completion
- attempt until every goal branch was on origin)
- 7. the audit trail: the DERIVED goal narrative, the unified events
- ledger (turn cards with tool_call/tool_result, usage rows, the §7.2
- kind set, web-originated-only comment kinds), the event log page +
- SSE stream, the /status goal_branches cache sampled by the manual
- saddle refresh
- 8. the push wire surface (401s on the id-less routes, the §7.2 chunks
- ingestion with (session_id, seq) exactly-once, block raises riding
- POST /api/saddle/results, the retired results-entries array
- consumed silently, 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,
- SPRY_SEED_ADMIN_PASSWORD=adminpass123, SADDLE_LLM_BASE_URL pointing
- at the mock (the one-time model-config bootstrap seed — the database
- row it creates is the live configuration), SADDLE_PROJECTS_ROOT=$WORKSPACE
- and a FRESH database
- (the harness asserts an empty tasks/goals state); a saddle daemon
- with SADDLE_WORKSPACE_ROOT=$WORKSPACE (the shared-volume deployment
- assumption — the goal clone anchors the worker worktrees); a git
- repo with at least two commits at $WORKSPACE/demo whose origin is
- pushable (the harness prepares demo + demo-origin.git when WORKSPACE
- is empty).
- Environment:
- WEB web app base URL (default http://localhost:8180 —
- 8080 is the production port, never the e2e default)
- SADDLE daemon control URL (default http://localhost:7100)
- SADDLE_HOST host:port to register (default localhost:7100)
- SADDLE_NAME registration name (default saddle-e2e-1)
- WORKSPACE demo repo parent dir (default /tmp/clanker-e2e/workspace)
- DB_PATH web app SQLite file (for audit assertions)
- MOCK mock_llm.py path (default <repo>/../saddle/test/mock_llm.py)
- MOCK_PORT local mock port (default 9999)
- MOCK_CMD run instead of the local mock (e.g. a podman exec line
- that starts the mock inside the saddle container); the
- script skips spawning the mock when set (the mock then
- needs its own --capture for the receipt assertions)
- Prints PASS/FAIL lines Statum-style; exits non-zero on any failure.
- """
- import json
- import os
- import socket
- import sqlite3
- import subprocess
- import sys
- import time
- import urllib.error
- import urllib.parse
- import urllib.request
- REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- WEB = os.environ.get("WEB", "http://localhost:8180")
- SADDLE = os.environ.get("SADDLE", "http://localhost:7100")
- SADDLE_HOST = os.environ.get("SADDLE_HOST", "localhost:7100")
- SADDLE_NAME = os.environ.get("SADDLE_NAME", "saddle-e2e-1")
- WORKSPACE = os.environ.get("WORKSPACE", "/tmp/clanker-e2e/workspace")
- DB_PATH = os.environ.get("DB_PATH", os.path.join(WORKSPACE, "..", "web", "clanker-saddle.sqlite"))
- MOCK = os.environ.get("MOCK", os.path.join(os.path.dirname(REPO), "saddle", "test", "mock_llm.py"))
- MOCK_PORT = os.environ.get("MOCK_PORT", "9999")
- MOCK_CMD = os.environ.get("MOCK_CMD", "")
- # The mock's request capture (one JSON line per arrival carrying the
- # full message list) — the receipt forensics the D-side assertions read.
- 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]
- # 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
- def check(condition, label):
- global passes, failures
- if condition:
- passes += 1
- print("PASS %s" % label)
- else:
- failures += 1
- print("FAIL %s" % label)
- def http(method, url, data=None, headers=None, timeout=30):
- body = None
- if data is not None:
- body = "&".join("%s=%s" % (k, urllib.parse.quote(str(v))) for k, v in data.items()).encode()
- request = urllib.request.Request(WEB + url if url.startswith("/") else url,
- data=body, method=method)
- if data is not None:
- request.add_header("Content-Type", "application/x-www-form-urlencoded")
- for key, value in (headers or {}).items():
- request.add_header(key, value)
- try:
- with urllib.request.urlopen(request, timeout=timeout) as response:
- return response.status, response.read().decode()
- except urllib.error.HTTPError as error:
- return error.code, error.read().decode()
- def directives(uri, auth=None):
- headers = {}
- if auth:
- headers["X-Statum-Slot"] = "key=%s; as_at=%s" % (auth["key"], auth["as_at"])
- status, body = http("GET", "/_statum/entrypoint?uri=" + urllib.parse.quote(uri.split("?")[0]),
- headers=headers)
- if status != 200:
- raise SystemExit("entrypoint %s -> HTTP %d: %s" % (uri, status, body[:300]))
- return json.loads(body)
- def slot_public(auth, uri, type_name):
- for directive in directives(uri, auth):
- if directive["type"] == "set":
- content = json.loads(directive["snapshot_frame"]["content"])
- if content["slot"]["type"] == type_name:
- return content["public"]
- raise SystemExit("no %s slot in entrypoint state for %s" % (type_name, uri))
- def act(auth, action, fields):
- headers = {"X-Statum-Slot": "key=%s; as_at=%s" % (auth["key"], auth["as_at"])}
- if action.get("private"):
- headers["X-Statum-Private"] = action["private"]
- status, body = http(action.get("method", "POST"), action["uri"], fields, headers)
- if status != 200:
- raise SystemExit("action %s -> HTTP %d: %s" % (action["uri"], status, body[:300]))
- return json.loads(body)
- def login(username, password):
- frames = directives("/login")
- content = json.loads(frames[0]["snapshot_frame"]["content"])
- action = content["public"]["login"]
- status, body = http(action.get("method", "POST"), action["uri"],
- {"username": username, "password": password})
- if status != 200:
- raise SystemExit("login -> HTTP %d: %s" % (status, body[:300]))
- for directive in json.loads(body):
- if directive["type"] == "set":
- frame = json.loads(directive["snapshot_frame"]["content"])
- if frame["slot"]["type"] == "auth":
- return {"key": frame["slot"]["key"], "as_at": frame["as_at"]}
- raise SystemExit("login response carried no auth slot: " + body[:300])
- def saddle_status():
- try:
- status, body = http("GET", SADDLE + "/status", timeout=8)
- return json.loads(body) if status == 200 else None
- except (urllib.error.URLError, OSError):
- return None
- def wait_for(predicate, timeout=60, what="condition", interval=1.0):
- deadline = time.monotonic() + timeout
- last = None
- while time.monotonic() < deadline:
- last = predicate()
- if last is not False and last is not None:
- return last
- time.sleep(interval)
- print(" (timeout waiting for %s; last: %s)" % (what, last))
- return None
- def task_state(auth, task_id):
- return slot_public(auth, "/tasks/%d" % task_id, "task")
- def wait_task_status(auth, task_id, status, timeout=90):
- return wait_for(lambda: task_state(auth, task_id).get("status") == status or False,
- timeout=timeout, what="task %d -> %s" % (task_id, status))
- def goal_state(auth, goal_id):
- return slot_public(auth, "/goals/%d" % goal_id, "goal")
- def wait_goal_status(auth, goal_id, status, timeout=90):
- return wait_for(lambda: goal_state(auth, goal_id).get("status") == status or False,
- timeout=timeout, what="goal %d -> %s" % (goal_id, status))
- def saddle_api(method, path, token, payload=None):
- """Raw call against the saddle ingest API; returns (status, json)."""
- request = urllib.request.Request(WEB + path,
- data=json.dumps(payload or {}).encode(), method=method)
- request.add_header("Content-Type", "application/json")
- if token is not None:
- request.add_header("Authorization", "Bearer " + token)
- try:
- with urllib.request.urlopen(request, timeout=15) as response:
- return response.status, json.loads(response.read().decode() or "{}")
- except urllib.error.HTTPError as error:
- body = error.read().decode()
- try:
- return error.code, json.loads(body)
- except ValueError:
- return error.code, {"raw": body}
- def db_query(sql, params=()):
- db = sqlite3.connect(DB_PATH, timeout=10)
- try:
- return db.execute(sql, params).fetchall()
- finally:
- db.close()
- def capture_messages():
- """Every message the mock received, oldest first (the mock's
- --capture file: one JSON line per request). Tool results ride back
- to the model as role=tool turns; harness notices (loop warnings,
- verdicts) as role=user turns."""
- messages = []
- if os.path.exists(CAPTURE_PATH):
- with open(CAPTURE_PATH) as handle:
- for line in handle:
- try:
- arrival = json.loads(line)
- except ValueError:
- continue
- for message in arrival.get("messages", []):
- if message.get("role") in ("user", "tool"):
- messages.append((message.get("role"),
- message.get("content") or ""))
- return messages
- def prepare_workspace():
- """Ensures $WORKSPACE/demo exists with a pushable origin (the goal
- branch push needs a remote that accepts it). Reuses an existing
- demo checkout; the DATABASE must still be fresh (checked in main)."""
- if os.path.isdir(os.path.join(DEMO, ".git")):
- return
- os.makedirs(WORKSPACE, exist_ok=True)
- origin = os.path.join(WORKSPACE, "demo-origin.git")
- subprocess.run(["git", "init", "--bare", "-b", "main", origin], check=True,
- capture_output=True)
- subprocess.run(["git", "clone", origin, DEMO], check=True, capture_output=True)
- subprocess.run(["git", "-C", DEMO, "config", "user.email", "e2e@localhost"], check=True)
- subprocess.run(["git", "-C", DEMO, "config", "user.name", "e2e"], check=True)
- with open(os.path.join(DEMO, "hello.py"), "w") as handle:
- handle.write("print('demo')\n")
- subprocess.run(["git", "-C", DEMO, "add", "-A"], check=True, capture_output=True)
- subprocess.run(["git", "-C", DEMO, "commit", "-m", "one"], check=True, capture_output=True)
- with open(os.path.join(DEMO, "notes.md"), "w") as handle:
- handle.write("# notes\n")
- subprocess.run(["git", "-C", DEMO, "add", "-A"], check=True, capture_output=True)
- subprocess.run(["git", "-C", DEMO, "commit", "-m", "two"], check=True, capture_output=True)
- subprocess.run(["git", "-C", DEMO, "push", "origin", "main"], check=True, capture_output=True)
- def tool_call(name, arguments):
- return "tool:%s:%s" % (name, json.dumps(arguments))
- def parse_call(spec):
- """One mock call spec back into the {name, arguments} shape the
- JSON array and content+calls grammars expect."""
- name, arguments = spec[len("tool:"):].split(":", 1)
- return {"name": name, "arguments": json.loads(arguments)}
- def mock_argv():
- """The protocol-native arc, one scripted assistant turn each,
- consumed in strict arrival order (single daemon, sequential
- sessions: goal run parks before the worker starts, the worker
- finishes before the goal agent's terminal-event resume)."""
- responses = [
- # G1 (fresh goal agent): narrated probe — turn_text plus one
- # tool call, the protocol-native shape of the old commentary
- # round
- json.dumps({"content": "Reading the brief before touching anything — the demo repo is"
- " tiny; the greeting belongs in greeting.txt.",
- "calls": [parse_call(tool_call("run_command", {"command": "ls -la"}))]}),
- # G2: investigate, then block on a scope question (the
- # ask_question tool parks the session for the human)
- json.dumps({"content": "Two commits and one file — the blast radius is greeting.txt"
- " alone; confirming the scope with the human.",
- "calls": [parse_call(tool_call("run_command", {"command": "git log --oneline | head -3"}))]}),
- tool_call("ask_question", {"question": "Which scope should the greeting cover?",
- "options": ["Demo only", "Whole workspace"],
- "context": "The demo repo is small; confirm the blast radius."}),
- # G3 (answer resume): submit the plan -> plan review park
- tool_call("submit_plan", {"markdown": "# Plan v1\n\n1. Add greeting.txt to the demo repo."}),
- # G4 (feedback resume): the revision incorporating the feedback
- tool_call("submit_plan", {"markdown": "# Plan v2\n\n1. Add greeting.txt to the demo repo.\n"
- "2. Export it for reuse."}),
- # G5/G6 (approval resume): fan out through the real callbacks,
- # then park suspended (enqueue + suspend tolerate sharing one
- # message — the harness executes them in order)
- tool_call("create_task", {"title": "Write the greeting", "mode": "implement",
- "brief": "Create greeting.txt containing hello-from-the-goal."}),
- json.dumps([parse_call(tool_call("enqueue_task", {"task_id": 2})),
- parse_call(tool_call("suspend", {}))]),
- # W1..W5 (the worker): five IDENTICAL probes — the loop
- # detector injects its warning user turn after the fifth,
- # never refusing execution
- ] + [tool_call("run_command", {"command": "echo probing-the-workspace"})] * 5 + [
- # W6: post a durable remark (§7.2 comment kind — the body is
- # the record; the web renders it as a narrative footnote)
- tool_call("add_comment", {"body": "Probing done — the mandate starts on the"
- " ephemeral branch."}),
- # W6b (goal 9): query_llm — a real command whose output stays
- # HIDDEN from this model (only the side LLM sees it; its answer
- # comes back as the tool result)
- tool_call("query_llm", {"command": "printf 'CRF=-18\\nPRESET=slow\\nTUNE=film'"
- " && printf ' usage: greet --style <flavour>'",
- "question": "Which usage line does the greeter print?"}),
- # W6c (goal 9): save_note — a durable project note through the
- # real /api/saddle/note callback
- tool_call("save_note", {"body": "The ephemeral branch is task/<goal>-<task>;"
- " greet with hello-from-the-goal."}),
- # W6d (goal 9): yeehaw — the celebration rides the ledger as a
- # yeehaw event the task log renders
- tool_call("yeehaw", {"message": "greeting shipped from the ephemeral branch —"
- " a genuinely fun task"}),
- # W7: the clean mandate — verify the ephemeral branch, commit
- json.dumps({"content": "Loop flagged — switching to the mandate on the ephemeral branch.",
- "calls": [parse_call(tool_call("run_command", {"command": "git branch --show-current"}))]}),
- tool_call("run_command", {"command": "echo hello-from-the-goal > greeting.txt && git add -A"
- " && git commit -m 'greeting from the goal'"}),
- tool_call("finish", {"status": "done",
- "summary": "# Report\n\n- changed: greeting.txt\n"
- "- verified: committed on the ephemeral branch"}),
- # G7 (the coalesced terminal-event resume): hold the working
- # window open a moment (the /status branch probe samples it),
- # then merge, prune, push
- tool_call("run_command", {"command": "sleep 2"}),
- tool_call("run_command", {"command": "git merge --no-edit $(git for-each-ref"
- " --format='%(refname:short)' refs/heads/task/*)"
- " && git branch -D $(git for-each-ref"
- " --format='%(refname:short)' refs/heads/task/*)"
- " && git push -u origin $(git branch --show-current)"}),
- # G8: submit_report -> completion review park
- tool_call("submit_report", {"markdown": "# Goal completion report\n\n"
- "- achieved: greeting.txt added via one worker task\n"
- "- verified: the goal branch is pushed to origin"}),
- ]
- argv = []
- for response in responses:
- argv += ["-r", response]
- argv += [
- "--on-query", "The greeter prints: usage: greet --style <flavour>.",
- # 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 tool results reached the model
- "--capture", CAPTURE_PATH,
- ]
- return argv
- def start_mock():
- if MOCK_CMD:
- # e.g. MOCK_CMD='podman exec -d clanker-saddle python3
- # /usr/libexec/saddle/mock_llm.py' — the same scripted argv is
- # appended, so the in-container mock behaves identically.
- argv = MOCK_CMD.split() + mock_argv()
- else:
- argv = [sys.executable, MOCK, "--port", MOCK_PORT] + mock_argv()
- return subprocess.Popen(argv, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
- def markdown_renderer_checks():
- """Exercise the stm-markdown renderer shipped inside statum.js via node."""
- status, body = http("GET", "/_statum/resource/statum.js", timeout=15)
- if status != 200:
- check(False, "statum.js served to the browser")
- return
- check("stm-markdown" in body, "statum.js ships the stm-markdown directive")
- def extract(name, source):
- start = source.index("function %s(" % name)
- depth = 0
- index = source.index("{", start)
- end = index
- while True:
- if source[end] == "{":
- depth += 1
- elif source[end] == "}":
- depth -= 1
- if depth == 0:
- break
- end += 1
- return source[start:end + 1]
- names = ("safeMarkdownUrl", "mdLinks", "mdInline", "renderMarkdown")
- missing = [name for name in names if ("function %s(" % name) not in body]
- if missing:
- # The shipped statum.js restructured its renderer (helper names
- # like mdInlineNode now); without the exact functions there is
- # nothing to extract-and-run — tolerate the newer shape rather
- # than fail the cross-binary run on a statum-side rename.
- check("renderMarkdown" in body or "mdInlineNode" in body,
- "statum.js still carries a markdown renderer (restructured: %s)"
- % ", ".join(missing))
- return
- parts = [extract(name, body) for name in names]
- harness = (
- "%s\n%s\n%s\n%s\n"
- "console.log(renderMarkdown('# Title\\n\\n- **bold** item\\n- `code` item\\n"
- "\\n> quoted text\\n\\n```text\\nfenced\\n```'));\n"
- "console.log('@@' + renderMarkdown('<script>alert(1)</script> [bad](javascript:alert(2))"
- " [ok](https://example.com) <img src=x onerror=alert(3)>'));\n" % tuple(parts)
- )
- try:
- result = subprocess.run(["node", "-e", harness], capture_output=True, text=True, timeout=20)
- except (OSError, subprocess.SubprocessError):
- check(True, "stm-markdown renderer present (node unavailable, deep check skipped)")
- return
- rendered, _, xss = result.stdout.partition("@@")
- check("<h1>" in rendered and "<li>" in rendered and "<code>" in rendered
- and "<blockquote>" in rendered and "<strong>" in rendered,
- "stm-markdown renders headings, lists, code, quotes, bold")
- check("<script" not in xss and "<img" not in xss
- and 'href="javascript:' not in xss and 'href="#"' in xss
- and '<a href="https://example.com">ok</a>' in xss,
- "stm-markdown escapes raw tags, defangs javascript: links, keeps safe ones")
- def main():
- prepare_workspace()
- if os.path.exists(CAPTURE_PATH):
- os.remove(CAPTURE_PATH)
- if db_query("select count(*) from tasks")[0][0] or db_query("select count(*) from goals")[0][0]:
- raise SystemExit("the web database is not fresh (tasks/goals exist) — "
- "wipe the DB and restart the app+daemon for a clean goal-era run")
- mock = start_mock()
- time.sleep(2.0)
- try:
- # A previous suite run (or a crashed orchestrator) can leave the
- # daemon parked on a block; /stop releases it so dispatch works.
- try:
- http("POST", SADDLE + "/stop", timeout=5)
- except (urllib.error.URLError, OSError):
- pass
- auth = login("admin", "adminpass123")
- check(True, "login yields an auth slot")
- state = slot_public(auth, "/projects", "projects")
- act(auth, state["create"], {
- "name": "demo",
- "git_url": DEMO,
- "default_branch": "main",
- "summary": "Integration test repository",
- })
- check(any(p["name"] == "demo" for p in slot_public(auth, "/projects", "projects")["projects"]),
- "project demo created (local git_url, pushable origin)")
- state = slot_public(auth, "/saddles", "saddles")
- act(auth, state["register"], {"name": SADDLE_NAME, "host": SADDLE_HOST})
- registered = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
- if s["name"] == SADDLE_NAME]
- check(len(registered) == 1 and registered[0]["token"].startswith("saddle-"),
- "saddle registered at %s with an API token" % SADDLE_HOST)
- print("== goal create -> start ==")
- goals_state = slot_public(auth, "/goals", "goals")
- act(auth, goals_state["create"], {
- "project_id": "1",
- "title": "Greet the demo",
- "brief": "Add a greeting file to the demo repo end to end.",
- "branch_name": "goal/demo-run",
- "priority": "3",
- })
- goal1 = int([g["id"] for g in slot_public(auth, "/goals", "goals")["goals"]
- if g["title"] == "Greet the demo"][0])
- check(goal_state(auth, goal1)["status"] == "draft", "the goal lands in draft")
- act(auth, goal_state(auth, goal1)["start"], {})
- check(wait_goal_status(auth, goal1, "planning", timeout=30) is not None,
- "starting the goal moves it to planning (agent dispatched)")
- run_task = db_query(
- "select id from tasks where goal_id = ? and kind = 'goal_run'", (goal1,))[0][0]
- check(wait_for(lambda: task_state(auth, run_task)["status"]
- in ("assigned", "working", "blocked", "suspended") or False,
- timeout=30, what="goal run dispatched") is not None,
- "the goal-run task reaches the daemon (assigned/working/blocked)")
- goal_clone = os.path.join(WORKSPACE, "goal-%d" % goal1, "demo")
- check(wait_for(lambda: os.path.isdir(os.path.join(goal_clone, ".git")) or False,
- timeout=30, what="the goal clone") is not None,
- "the goal clone appears under the shared workspace root (goal-<id>)")
- branch = subprocess.run(["git", "-C", goal_clone, "branch", "--show-current"],
- capture_output=True, text=True).stdout.strip()
- check(branch == "goal/demo-run", "the goal clone sits on the user-named goal branch")
- print("== pre-plan question -> one answer, one row ==")
- check(wait_for(lambda: any(b["block_type"] == "question" and not b.get("answered")
- for b in goal_state(auth, goal1).get("blocks", [])) or False,
- timeout=90, what="the agent's question block") is not None,
- "the pre-plan question surfaces as a goal block")
- check(wait_task_status(auth, run_task, "blocked", timeout=30) is not None,
- "the goal-run task parks blocked on its own block row")
- question = [b for b in goal_state(auth, goal1)["blocks"]
- if b["block_type"] == "question" and not b.get("answered")][0]
- check(question["options"] == ["Demo only", "Whole workspace"],
- "the question carries its options")
- check("Which scope should the greeting cover" in question["question"],
- "the ask_question question arrives verbatim")
- check("confirm the blast radius" in question["body"], "the question carries the markdown body")
- dupes = db_query(
- "select count(*) from task_blocks tb join tasks t on t.id = tb.task_id"
- " where t.goal_id = ? and t.kind = 'goal_run' and tb.type = 'question'",
- (goal1,))[0][0]
- check(dupes == 1, "one question row despite the park-unwinding round-end push")
- dash = slot_public(auth, "/", "dashboard")
- check(any(int(r.get("goal_id") or 0) == goal1 for r in dash["pinned"]),
- "the dashboard pile pins the goal block")
- act(auth, question["answer_action"], {"option": "Demo only"})
- answered_rows = db_query(
- "select answer from task_blocks tb join tasks t on t.id = tb.task_id"
- " where t.goal_id = ? and t.kind = 'goal_run' and tb.type = 'question'",
- (goal1,))
- check(answered_rows and answered_rows[0][0] == "Demo only",
- "the ONE answer lands on the goal-run task's single row")
- print("== plan review: reject -> revise -> approve ==")
- check(wait_goal_status(auth, goal1, "plan_review", timeout=90) is not None,
- "the plan submission moves the goal to plan_review")
- check(wait_for(lambda: goal_state(auth, goal1).get("plan_pending_block") not in
- (None, False) or False, timeout=30, what="pending plan block") is not None,
- "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")
- # 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
- (None, False) and "Plan v2" in
- goal_state(auth, goal1)["plan_pending_block"].get("body", "") or False,
- timeout=90, what="revised plan") is not None,
- "the agent revises and re-submits incorporating the feedback")
- 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"], {"verdict": "accept"})
- check(wait_goal_status(auth, goal1, "active", timeout=30) is not None,
- "approving with the agreed wording activates the goal")
- print("== task fan-out via the real callbacks ==")
- worker_row = wait_for(lambda: db_query(
- "select id from tasks where goal_id = ? and kind = 'task'", (goal1,)) or False,
- timeout=90, what="the agent-created task")
- worker_task = int(worker_row[0][0])
- check(bool(worker_row), "the goal agent created its worker task through the callback")
- creator = db_query("select creator, mode from tasks where id = ?", (worker_task,))[0]
- check(creator == ("goal", "implement"),
- "the created task carries creator=goal and the requested mode")
- check(wait_task_status(auth, worker_task, "queued", timeout=30) is not None
- or task_state(auth, worker_task)["status"] in ("working", "done"),
- "the agent enqueued its task (creator-only rule honoured by construction)")
- ephemeral = "task/%d-%d" % (goal1, worker_task)
- check(wait_for(lambda: task_state(auth, worker_task)["status"]
- in ("assigned", "working", "done", "failed") or False,
- timeout=60, what="worker dispatched") is not None,
- "the dispatcher hands the worker to the freed saddle")
- check(wait_for(lambda: db_query(
- "select count(*) from sessions s where s.task_id = ? and s.outcome = 'parked'"
- " and s.ended_at < (select min(started_at) from sessions where task_id = ?)",
- (run_task, worker_task))[0][0] > 0 or db_query(
- "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 (parked session / suspended_at)")
- # 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.
- # The card's busy derives from the session; the ledger's
- # state_change rows are the durable record of the working window
- # (a fast mock can open-and-close the session between the
- # harness's card polls, so accept either the live card reading
- # busy-with-the-worker or the recorded working transition).
- check(wait_for(lambda: bool(db_query(
- "select 1 from events where scope_id = ? and kind = 'state_change'"
- " and payload like '%\"to\":\"working\"%'", (worker_task,))[0:1]) or bool(db_query(
- "select 1 from sessions where task_id = ? and ended_at is null",
- (worker_task,))[0:1]) 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 session (no cached row)")
- print("== the worker's ephemeral worktree ==")
- # The branch probe rides the ledger: the tool_call event carries
- # the git command, its tool_result event the branch on stdout.
- branch_rows = wait_for(lambda: db_query(
- "select payload from events where scope_type = 'task' and scope_id = ?"
- " and kind = 'tool_result' and payload like ?",
- (worker_task, "%" + ephemeral + "%")) or False,
- timeout=90, what="the worker's branch probe")
- check(branch_rows and ephemeral in (branch_rows[0][0] or ""),
- "the worker runs inside its ephemeral branch %s (tool_result stdout)" % ephemeral)
- branch_call = db_query(
- "select count(*) from events where scope_type = 'task' and scope_id = ?"
- " and kind = 'tool_call' and payload like '%git branch --show-current%'",
- (worker_task,))[0][0]
- check(branch_call == 1, "the branch probe's tool_call event sits in the ledger")
- worktree_dir = os.path.join(WORKSPACE, "task-%d" % worker_task)
- check(wait_for(lambda: task_state(auth, worker_task)["status"] == "done" or False,
- timeout=120, what="worker done") is not None,
- "the worker completes with its report")
- detail = task_state(auth, worker_task)
- check("greeting.txt" in (detail.get("summary") or ""),
- "the worker summary carries its completion report")
- check(not os.path.exists(worktree_dir), "the worker's worktree auto-pruned on done")
- kept = subprocess.run(["git", "-C", goal_clone, "rev-parse", "--verify", ephemeral],
- capture_output=True).returncode == 0
- merged_already = subprocess.run(
- ["git", "-C", goal_clone, "log", "--oneline", "-5"],
- capture_output=True, text=True).stdout
- check(kept or "greeting from the goal" in merged_already,
- "the ephemeral branch survives for the merge (or the merge already consumed it)")
- print("== coalesced resume -> merge -> push -> completion review ==")
- check(wait_for(lambda: any(
- e["kind"] == "task-done" and int(e["task_id"]) == worker_task
- for e in goal_state(auth, goal1).get("events", [])) or False,
- timeout=30, what="the task-done narrative row") is not None,
- "the worker's terminal transition lands in the derived goal narrative")
- # the goal agent's /status reports the branch push state while it
- # works; sample the moment the resumed agent is mid-round, then
- # cache it through the manual saddle refresh
- check(wait_for(lambda: (saddle_status() or {}).get("goal_branches") or False,
- timeout=90, what="the resumed agent exposes goal_branches") is not None,
- "the daemon's /status exposes goal_branches while the goal agent works")
- saddles_state = slot_public(auth, "/saddles", "saddles")
- card = [s for s in saddles_state["saddles"] if s["name"] == SADDLE_NAME][0]
- act(auth, card["refresh"], {})
- check(wait_for(lambda: any(
- "demo" in (b.get("branch") or "") and isinstance(b.get("pushed"), bool)
- for b in goal_state(auth, goal1).get("push_branches", []))
- or False, timeout=60, what="cached push state") is not None,
- "the refresh probe caches the branch push state on the goal (live chips)")
- 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)
- 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
- check("hello-from-the-goal" in merged, "the merged task branch landed on the goal branch")
- deleted = subprocess.run(["git", "-C", goal_clone, "rev-parse", "--verify", ephemeral],
- capture_output=True).returncode != 0
- check(deleted, "the goal agent deleted the merged task branch")
- pushed = subprocess.run(["git", "-C", DEMO, "rev-parse", "--verify", "goal/demo-run"],
- capture_output=True).returncode == 0
- check(pushed, "the goal branch was pushed to origin (push-before-review)")
- print("== completion accept ==")
- # Goal 21: the accept-time push gate is gone — the daemon already
- # refused the agent's completion attempt until every goal branch
- # was pushed (push-before-review, asserted above), so the branch
- # is on origin here and the verdict path needs no second check.
- check("greeting.txt added via one worker task" in (completion or {}).get("body", ""),
- "the run task's completion block carries the report")
- 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 (no web-side push gate on the verdict)")
- check(wait_goal_status(auth, goal1, "complete", timeout=30) is not None,
- "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 + the unified ledger) ==")
- 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"):
- 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)")
- check(all(k != "task-enqueued" for k in kinds),
- "timestamp-less bookkeeping never returns (task-enqueued is gone)")
- check(db_query(
- "select count(*) from sqlite_master where type = 'table'"
- " and name = 'goal_events'")[0][0] == 0,
- "the goal_events table is gone (nothing is written on ingest)")
- check(db_query(
- "select count(*) from sqlite_master where type = 'table'"
- " and name = 'session_commands'")[0][0] == 0,
- "the session_commands table is gone (the ledger is the record)")
- run_kinds = {row[0] for row in db_query(
- "select distinct kind from events where scope_type = 'task' and scope_id = ?",
- (run_task,))}
- check({"turn_text", "tool_call", "tool_result", "usage", "lifecycle"} <= run_kinds,
- "the goal agent's ledger carries the protocol kinds (%s)" % sorted(run_kinds))
- worker_kinds = {row[0] for row in db_query(
- "select distinct kind from events where scope_type = 'task' and scope_id = ?",
- (worker_task,))}
- check({"turn_text", "tool_call", "tool_result", "usage"} <= worker_kinds,
- "the worker's ledger carries the protocol kinds (%s)" % sorted(worker_kinds))
- daemon_kinds = {row[0] for row in db_query(
- "select distinct kind from events where session_id is not null")}
- check(daemon_kinds <= {"turn_text", "tool_call", "tool_result", "progress",
- "comment", "lifecycle", "usage", "state_change", "yeehaw"},
- "every daemon-pushed kind is a §7.2 kind (%s)" % sorted(daemon_kinds))
- remark_rows = db_query(
- "select actor, payload from events where kind = 'comment'"
- " and session_id is not null and scope_id = ?", (worker_task,))
- check(len(remark_rows) == 1 and remark_rows[0][0] == "model"
- and "the mandate starts on the ephemeral branch" in (remark_rows[0][1] or ""),
- "the add_comment remark rides the ledger as a model-actor comment event")
- print("== goal 9 tool parity through both binaries ==")
- # yeehaw: the celebration rode the ledger as a yeehaw event with
- # the message intact (the task log renders it in the comments
- # bucket with the festive badge — pinned by test_task_log_js)
- whoop_rows = db_query(
- "select actor, payload from events where kind = 'yeehaw'"
- " and session_id is not null and scope_id = ?", (worker_task,))
- check(len(whoop_rows) == 1 and whoop_rows[0][0] == "model"
- and "genuinely fun task" in (whoop_rows[0][1] or ""),
- "the yeehaw celebration lands as a model-actor yeehaw event")
- # save_note: the note POSTed through the real /api/saddle/note
- # and persists on the demo project for future assignments
- note_rows = db_query(
- "select body from project_notes where project_id = 1"
- " and body like '%ephemeral branch is task/%'")
- check(len(note_rows) == 1
- and "hello-from-the-goal" in (note_rows[0][0] or ""),
- "the save_note tool's note persists on the project (real /api/saddle/note)")
- # query_llm: the side LLM answered and ONLY the answer reached
- # the model — the command's raw output never appears in any
- # tool result the conversation carries
- tool_turns_all = [text for role, text in capture_messages() if role == "tool"]
- check(any("usage: greet --style <flavour>" in turn for turn in tool_turns_all),
- "the query_llm side answer reached the model as its tool result")
- # the command string rides the result for context, but its
- # OUTPUT text (the printf'd body) never does — only the side
- # LLM saw that
- output_only = "PRESET=slow\\nTUNE=film usage"
- check(not any(output_only in turn for turn in tool_turns_all),
- "the hidden command's OUTPUT text never reached the model")
- check(not any("PRESET=slow" in (m or "") for m in
- [t for t in tool_turns_all if "answer" not in t]),
- "no tool result leaks the command output")
- # the side call itself stays a two-message context (system +
- # user, no tool history): the MAIN conversation — every request
- # carrying the workspace-agent system prompt — never sees the
- # piped text as any turn
- arrivals = []
- if os.path.exists(CAPTURE_PATH):
- with open(CAPTURE_PATH) as handle:
- arrivals = [json.loads(line) for line in handle if line.strip()]
- main_turns = [message.get("content") or ""
- for arrival in arrivals
- if any(m.get("role") == "system"
- and "workspace agent" in (m.get("content") or "")
- for m in arrival.get("messages", []))
- for message in arrival.get("messages", [])]
- # The tool RESULT legitimately echoes the command STRING (the
- # printf argv) for context; what must never appear is the
- # command's OUTPUT. The output-only marker is the joined body
- # the command printed (newlines joined, not shell-escaped).
- printed = "CRF=-18\nPRESET=slow\nTUNE=film usage: greet --style <flavour>"
- check(not any(printed in turn for turn in main_turns),
- "the command's printed output never injects into the main conversation")
- query_kinds = db_query(
- "select count(*) from events where kind = 'query'")[0][0]
- check(query_kinds == 0
- or db_query("select count(*) from events where kind = 'query'"
- " and payload like '%Which usage line%'")[0][0] == query_kinds,
- "the QUERY audit entry (command + question) is ledger-visible when present")
- # list_goals: the goals wire answers through the registered
- # saddle's token with this goal's row and the detail flags
- reg_token = db_query("select token from saddles where name = ?",
- (SADDLE_NAME,))[0][0]
- status, body = saddle_api("GET", "/api/saddle/goals?project=demo", reg_token)
- rows = body.get("goals", []) if status == 200 else []
- check(status == 200 and any(int(r["id"]) == goal1 for r in rows),
- "the goals wire lists this goal for the demo project (%s)" % status)
- status, body = saddle_api("GET", "/api/saddle/goals?goal_id=%d" % goal1, reg_token)
- detail = body.get("goal", {}) if status == 200 else {}
- check(status == 200 and detail.get("has_plan") is True
- and detail.get("has_report") in (True, False)
- and detail.get("branch") == "goal/demo-run",
- "the goals wire's goal_id detail carries branch + has_plan/has_report")
- narrative_remarks = [e["text"] for e in goal_state(auth, goal1)["events"]
- if e["kind"] == "comment"]
- check(any(text.startswith("Probing done") for text in narrative_remarks),
- "the goal narrative renders the worker's remark as a comment row")
- plan_call = db_query(
- "select count(*) from events where scope_type = 'task' and scope_id = ?"
- " and kind = 'tool_call' and payload like '%submit_plan%'", (run_task,))[0][0]
- check(plan_call >= 2, "both plan submissions sit as tool_call events")
- usage_rows = db_query(
- "select payload from events where kind = 'usage' and scope_type = 'task'"
- " and scope_id = ? limit 1", (worker_task,))
- check(bool(usage_rows) and "prompt_tokens" in (usage_rows[0][0] or "")
- and "elapsed_ms" in (usage_rows[0][0] or ""),
- "usage events carry prompt/completion tokens and elapsed time")
- 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(
- "select completed_at from tasks where id = ?", (worker_task,))[0][0]
- check(watermark_row[0][0] is not None and worker_done_at is not None
- and watermark_row[0][0] >= worker_done_at,
- "the goals row's terminal watermark advanced past the worker's transition"
- " (the resume consumed it)")
- print("== the harness bookkeeping stays ledger-internal (C2 + U2) ==")
- # Plan 16 S5 (U2): the mechanical rows (restores, park notices)
- # author NO user-facing comment rows — they persist as
- # system-authored comment events (the unified ledger) and never
- # render on a user-facing timeline.
- mechanical = db_query(
- "select count(*) from events where kind = 'comment' and actor = 'system'"
- " and (payload like '%session context restored%'"
- " or payload like '%parked until%')")[0][0]
- check(mechanical >= 1,
- "the harness bookkeeping (restore, park) persists as system comment events")
- thread = goal_state(auth, goal1)["comments"]
- 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(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: tool results + loop warning ==")
- messages = capture_messages()
- tool_turns = [text for role, text in messages if role == "tool"]
- user_turns = [text for role, text in messages if role == "user"]
- check(any("probing-the-workspace" in turn for turn in tool_turns),
- "the probes' tool results reached the model as role=tool turns")
- check(any(ephemeral in turn for turn in tool_turns),
- "the branch probe's result (the ephemeral branch name) reached the model")
- check(any("you ran the exact same command" in turn and "you are in a loop" in turn
- for turn in user_turns),
- "the loop-warning notice reached the model as its next user turn")
- check(any("Human selected" in turn for turn in user_turns),
- "the question's verdict wording reached the model on resume")
- print("== event log page + SSE ==")
- # The §7.4 JSON page: newest-first window of the run task's
- # ledger (id, session_id, seq, kind, actor, payload).
- status, page = saddle_api("GET", "/tasks/%d/events?limit=200" % run_task, None)
- page_events = page.get("events", []) if status == 200 else []
- check(status == 200 and len(page_events) >= 10,
- "the event log page serves the run's ledger (%d events)" % len(page_events))
- ids = [e["id"] for e in page_events]
- check(ids == sorted(ids, reverse=True), "the page is newest-first")
- check(any(e["kind"] == "tool_call" and e["payload"].get("tool") == "submit_plan"
- for e in page_events),
- "turn cards render: submit_plan tool_call events in the page")
- check(any(e["kind"] == "tool_result" and e["payload"].get("tool") == "run_command"
- and "stdout" in e["payload"].get("result", "") for e in page_events),
- "tool_result events carry the §7.1 result shape (stdout embedded)")
- check(any(e["kind"] == "turn_text" and "Reading the brief" in e["payload"].get("text", "")
- for e in page_events),
- "the narrated turn renders as turn_text")
- check(all(e.get("seq") is not None and e["seq"] > 0 for e in page_events
- if e["kind"] in ("turn_text", "tool_call", "tool_result", "usage")),
- "daemon events carry their per-session seq")
- state = goal_state(auth, goal1)
- check(any(r.get("terminal_url", "").endswith("/tasks/%d/log" % run_task)
- for r in state.get("runs", [])),
- "the goal page's run list deep-links each session's event log")
- request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % run_task)
- with urllib.request.urlopen(request, timeout=15) as stream:
- check(stream.headers.get("Content-Type", "").startswith("text/event-stream"),
- "event stream is text/event-stream")
- streamed = []
- deadline = time.monotonic() + 10
- while time.monotonic() < deadline and len(streamed) < 120:
- line = stream.readline().decode().strip()
- if not line.startswith("data: ") or line == "data: ping":
- continue
- try:
- streamed.append(json.loads(line[6:]))
- except ValueError:
- continue
- if len(streamed) >= len(page_events):
- break
- check(len(streamed) >= 10,
- "the stream replays the initial most-recent-30 window (%d events)" % len(streamed))
- check(any(e.get("kind") == "tool_call" for e in streamed)
- and any(e.get("kind") == "tool_result" for e in streamed),
- "the SSE replay carries tool_call/tool_result events from the daemon")
- pulse = directives("/goals/%d" % goal1, auth)
- check(any(d.get("type") == "subscribe" for d in pulse),
- "goal page subscribes to the pulse channel for live refresh")
- print("== push wire surface (§7.2 chunks + results states) ==")
- 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"]
- if s["name"] == "saddle-e2e-api"]
- check(len(api_rows) == 1 and api_rows[0]["token"].startswith("saddle-"),
- "registration returns an API token")
- api_saddle = int(api_rows[0]["id"])
- api_token = api_rows[0]["token"]
- tasks_state = slot_public(auth, "/tasks", "tasks")
- act(auth, tasks_state["create"], {
- "project_id": "1",
- "title": "API surface probe",
- "description": "Chunks ingestion, exactly-once pushes",
- "priority": "6",
- "task_branch": "task/api-surface",
- })
- api_task = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"]
- if t["title"] == "API surface probe"][0])
- db = sqlite3.connect(DB_PATH)
- db.execute("insert into sessions (task_id, saddle_id, started_at)"
- " values (?, ?, strftime('%s','now'))", (api_task, api_saddle))
- db.execute("update tasks set status = 'working' where id = ?", (api_task,))
- db.commit()
- api_session = db.execute("select max(id) from sessions").fetchone()[0]
- db.close()
- def push_state(state="working", block=None, summary=None, entries=None):
- """One state push in the daemon's /results shape."""
- payload = {"session_id": api_session, "state": state}
- if block is not None:
- payload["block"] = block
- if summary is not None:
- payload["summary"] = summary
- if entries is not None:
- payload["entries"] = entries
- return saddle_api("POST", "/api/saddle/results", api_token, payload)
- def push_events(events):
- """One §7.2 chunks batch ({task_id, session_id, events})."""
- return saddle_api("POST", "/api/saddle/chunks", api_token,
- {"task_id": api_task, "session_id": api_session,
- "events": events})
- 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")
- status, _ = saddle_api("POST", "/api/saddle/chunks", None,
- {"session_id": api_session, "events": []})
- check(status == 401, "chunks push without a token is rejected")
- status, _ = saddle_api("POST", "/api/saddle/chunks", api_token,
- {"session_id": api_session, "events": [
- {"kind": "progress", "actor": "model", "ts": "now"}]})
- check(status == 400, "an event without a positive seq is refused (seq is mandatory)")
- # 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": "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": "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": "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)")
- # The retired results-entries array is consumed silently: no
- # comment row, no projection — round entries live as ledger
- # events on the chunks channel now.
- status, body = push_state(entries=[{"id": 9001, "command": "saddle comment",
- "output": "Surveyed the module", "exit_code": 0,
- "type": "comment",
- "timestamp": "2026-09-13T00:00:00Z"}])
- check(status == 200 and body.get("ok"),
- "a results push carrying entries is accepted (consumed silently)")
- check(not any(c["body"] == "Surveyed the module"
- for c in task_state(auth, api_task)["comments"]),
- "the retired entries array projects nothing (no comment row)")
- # §7.2 ingestion: a progress event lands in the ledger and the
- # task's progress view; the (session_id, seq) unique key makes
- # redelivery exactly-once.
- status, body = push_events([{"seq": 9000, "kind": "progress", "actor": "model",
- "ts": "2026-09-13T00:00:00Z",
- "payload": {"text": "Migrated the schemas"}}])
- check(status == 200 and body.get("stored") == 1,
- "the chunks push stores the progress event")
- status, body = push_events([{"seq": 9000, "kind": "progress", "actor": "model",
- "ts": "2026-09-13T00:00:01Z",
- "payload": {"text": "Migrated the schemas AGAIN"}}])
- check(status == 200 and body.get("stored") == 0,
- "the redelivered seq is consumed silently (UNIQUE(session_id, seq))")
- progress_view = task_state(auth, api_task)["progress"]
- check(len([p for p in progress_view
- if p["summary"] in ("Migrated the schemas", "Migrated the schemas AGAIN")]) == 1
- and progress_view[0]["summary"] == "Migrated the schemas",
- "progress events project exactly once per seq")
- status, body = push_state(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_state(state="blocked", block={
- "question": "Review the migration", "type": "review",
- "body": "Migration **ready** for review"})
- check(status == 200, "the review-type block push is accepted")
- detail = task_state(auth, api_task)
- check(detail["status"] == "review_requested", "the review block flips the task status")
- check([b["options"] for b in detail["blocks"]
- if b["block_type"] == "question"] == [["SQLite", "Postgres"]],
- "the pushed block carries its options")
- check(detail["progress"][0]["summary"] == "Migrated the schemas",
- "progress history newest-first")
- status, _ = push_state(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 resuming assignment formats it)")
- act(auth, detail["comment"],
- {"body": "Check the migration output", "quote": "ERROR: table demos exists"})
- 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 ==")
- markdown_renderer_checks()
- print("== quiesce ==")
- check(wait_for(
- lambda: (lambda rows: all(int(s["current_task_id"] or 0) == 0
- and s["status"] == "idle"
- for s in rows))(
- [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
- if s["name"] == SADDLE_NAME]) or False,
- timeout=30, what="saddle idle with no current task") is not None,
- "saddle derived idle with no running work once the goal finished")
- finally:
- if mock is not None:
- mock.terminate()
- print("%d passed, %d failed" % (passes, failures))
- return 1 if failures else 0
- if __name__ == "__main__":
- sys.exit(main())
|