|
@@ -36,10 +36,12 @@ The consolidated goal-era arc (plan 15, re-baselined for C+D):
|
|
|
complete, run task done
|
|
complete, run task done
|
|
|
7. the goal agent's terminal sessions (SSE replay), the audit trail
|
|
7. the goal agent's terminal sessions (SSE replay), the audit trail
|
|
|
(the DERIVED goal narrative, typed session_commands, the
|
|
(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
|
|
Prerequisites (see README.md "Integration testing"): the web app
|
|
|
running with SPRY_SEED_ADMIN_USERNAME=admin,
|
|
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"))
|
|
os.path.join(WORKSPACE, "..", "mock-capture.jsonl"))
|
|
|
DEMO = os.path.join(WORKSPACE, "demo")
|
|
DEMO = os.path.join(WORKSPACE, "demo")
|
|
|
HOSTNAME = socket.gethostname().split(".")[0]
|
|
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
|
|
passes = 0
|
|
|
failures = 0
|
|
failures = 0
|
|
@@ -305,9 +307,6 @@ def mock_argv():
|
|
|
"# two commits and one file — the blast radius is greeting.txt alone\n"
|
|
"# two commits and one file — the blast radius is greeting.txt alone\n"
|
|
|
+ (heredoc % 'saddle block question "Demo only" "Whole workspace"') % (
|
|
+ (heredoc % 'saddle block question "Demo only" "Whole workspace"') % (
|
|
|
"Which scope should the greeting cover? The demo repo is small; confirm the blast radius."),
|
|
"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
|
|
# W1 (the worker's first round): a DEGENERATE repetition (plan
|
|
|
# 15 D2) — the validator catches it before feeding; nothing
|
|
# 15 D2) — the validator catches it before feeding; nothing
|
|
|
# executes, one rejection receipt, the round is consumed
|
|
# 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
|
|
# the question's verdict ("Human selected: Demo only") -> plan v1
|
|
|
"--on-answer", (heredoc % "saddle plan") % (
|
|
"--on-answer", (heredoc % "saddle plan") % (
|
|
|
"# Plan v1\n\n1. Add greeting.txt to the demo repo."),
|
|
"# 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
|
|
# the approval verdict -> task fan-out + suspend
|
|
|
"--on-plan-approved", fanout,
|
|
"--on-plan-approved", fanout,
|
|
|
# the batched "Goal update" resume -> merge, prune, push, complete
|
|
# 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"
|
|
"# Goal completion report\n\n- achieved: greeting.txt added via one worker task\n"
|
|
|
"- verified: the goal branch is pushed to origin"),
|
|
"- verified: the goal branch is pushed to origin"),
|
|
|
"--on-query", "The hostname is %s." % HOSTNAME,
|
|
"--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
|
|
# receipt forensics: every prompt the mock received, so the
|
|
|
# harness can assert the D-side receipts reached the model
|
|
# harness can assert the D-side receipts reached the model
|
|
|
"--capture", CAPTURE_PATH,
|
|
"--capture", CAPTURE_PATH,
|
|
@@ -518,7 +525,10 @@ def main():
|
|
|
"the pending plan block renders on the goal page")
|
|
"the pending plan block renders on the goal page")
|
|
|
pending = goal_state(auth, goal1)["plan_pending_block"]
|
|
pending = goal_state(auth, goal1)["plan_pending_block"]
|
|
|
check("Plan v1" in pending["body"], "the plan block carries the submitted markdown")
|
|
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,
|
|
check(wait_goal_status(auth, goal1, "planning", timeout=30) is not None,
|
|
|
"rejecting with feedback returns the goal to planning")
|
|
"rejecting with feedback returns the goal to planning")
|
|
|
check(wait_for(lambda: goal_state(auth, goal1).get("plan_pending_block") not in
|
|
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)
|
|
revised = goal_state(auth, goal1)
|
|
|
check(len([r for r in revised["plan_revisions"] if r.get("answered")]) >= 1,
|
|
check(len([r for r in revised["plan_revisions"] if r.get("answered")]) >= 1,
|
|
|
"the rejected revision stays discoverable behind the expander")
|
|
"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,
|
|
check(wait_goal_status(auth, goal1, "active", timeout=30) is not None,
|
|
|
"approving with the agreed wording activates the goal")
|
|
"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,
|
|
"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,
|
|
timeout=30, what="the goal agent parks") is not None,
|
|
|
"the goal agent parked suspended after fanning out (suspended_at stamped)")
|
|
"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 ==")
|
|
print("== the worker's ephemeral worktree ==")
|
|
|
branch_rows = wait_for(lambda: db_query(
|
|
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,
|
|
check(wait_goal_status(auth, goal1, "completion_review", timeout=120) is not None,
|
|
|
"the resumed agent merges, pushes and enters completion review")
|
|
"the resumed agent merges, pushes and enters completion review")
|
|
|
state = goal_state(auth, goal1)
|
|
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(
|
|
merged = subprocess.run(
|
|
|
["git", "-C", goal_clone, "show", "goal/demo-run:greeting.txt"],
|
|
["git", "-C", goal_clone, "show", "goal/demo-run:greeting.txt"],
|
|
|
capture_output=True, text=True).stdout
|
|
capture_output=True, text=True).stdout
|
|
@@ -628,7 +659,7 @@ def main():
|
|
|
for b in goal_state(auth, goal1).get("push_branches", [])) or False,
|
|
for b in goal_state(auth, goal1).get("push_branches", [])) or False,
|
|
|
timeout=60, what="unpushed chip") is not None,
|
|
timeout=60, what="unpushed chip") is not None,
|
|
|
"the rewound origin flips the live push chip to unpushed")
|
|
"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"
|
|
check(any(d.get("type") == "notify" and d.get("kind") == "error"
|
|
|
and "Origin is behind" in (d.get("message") or "")
|
|
and "Origin is behind" in (d.get("message") or "")
|
|
|
for d in refusal),
|
|
for d in refusal),
|
|
@@ -649,23 +680,29 @@ def main():
|
|
|
completion = goal_state(auth, goal1)["completion_pending_block"]
|
|
completion = goal_state(auth, goal1)["completion_pending_block"]
|
|
|
|
|
|
|
|
print("== completion accept ==")
|
|
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")
|
|
"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),
|
|
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")
|
|
"the accept passes once the push flags read pushed")
|
|
|
check(wait_goal_status(auth, goal1, "complete", timeout=30) is not None,
|
|
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,
|
|
check(wait_task_status(auth, run_task, "done", timeout=30) is not None,
|
|
|
"the goal-run task closes done with the goal")
|
|
"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) ==")
|
|
print("== audit trail (the derived narrative) ==")
|
|
|
narrative = goal_state(auth, goal1)["events"]
|
|
narrative = goal_state(auth, goal1)["events"]
|
|
|
kinds = [e["kind"] for e in narrative]
|
|
kinds = [e["kind"] for e in narrative]
|
|
|
for expected in ("started", "block-raised", "plan-submitted", "plan-rejected",
|
|
for expected in ("started", "block-raised", "plan-submitted", "plan-rejected",
|
|
|
"plan-approved", "task-created", "task-done",
|
|
"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(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),
|
|
check(all(k != "task-enqueued" for k in kinds),
|
|
|
"timestamp-less bookkeeping never returns (task-enqueued is gone)")
|
|
"timestamp-less bookkeeping never returns (task-enqueued is gone)")
|
|
|
check(db_query(
|
|
check(db_query(
|
|
@@ -684,6 +721,20 @@ def main():
|
|
|
"the worker's terminal carries typed entries (%s)" % sorted(worker_types))
|
|
"the worker's terminal carries typed entries (%s)" % sorted(worker_types))
|
|
|
|
|
|
|
|
print("== the terminal watermark (C3) drives the resume ==")
|
|
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(
|
|
watermark_row = db_query(
|
|
|
"select last_terminal_watermark from goals where id = ?", (goal1,))
|
|
"select last_terminal_watermark from goals where id = ?", (goal1,))
|
|
|
worker_done_at = db_query(
|
|
worker_done_at = db_query(
|
|
@@ -693,23 +744,39 @@ def main():
|
|
|
"the goals row's terminal watermark advanced past the worker's transition"
|
|
"the goals row's terminal watermark advanced past the worker's transition"
|
|
|
" (the resume consumed it)")
|
|
" (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"]
|
|
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"]
|
|
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"]
|
|
check(not any(c.get("from_agent") and "[harness]" in c["body"]
|
|
|
for c in thread),
|
|
for c in thread),
|
|
|
"no harness-authored row renders as agent content")
|
|
"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 ==")
|
|
print("== D-side through both binaries: commentary + degenerate ==")
|
|
|
run_bash = db_query(
|
|
run_bash = db_query(
|
|
|
"select sc.command_text from session_commands sc join sessions s"
|
|
"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),
|
|
check(any(d.get("type") == "subscribe" for d in pulse),
|
|
|
"goal page subscribes to the pulse channel for live refresh")
|
|
"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")
|
|
saddles_state = slot_public(auth, "/saddles", "saddles")
|
|
|
act(auth, saddles_state["register"], {"name": "saddle-e2e-api", "host": "localhost:7199"})
|
|
act(auth, saddles_state["register"], {"name": "saddle-e2e-api", "host": "localhost:7199"})
|
|
|
api_rows = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
|
|
api_rows = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
|
|
@@ -794,7 +861,7 @@ def main():
|
|
|
act(auth, tasks_state["create"], {
|
|
act(auth, tasks_state["create"], {
|
|
|
"project_id": "1",
|
|
"project_id": "1",
|
|
|
"title": "API surface probe",
|
|
"title": "API surface probe",
|
|
|
- "description": "Typed updates, exactly-once poll",
|
|
|
|
|
|
|
+ "description": "Typed entries, exactly-once pushes",
|
|
|
"priority": "6",
|
|
"priority": "6",
|
|
|
"task_branch": "task/api-surface",
|
|
"task_branch": "task/api-surface",
|
|
|
})
|
|
})
|
|
@@ -808,73 +875,110 @@ def main():
|
|
|
api_session = db.execute("select max(id) from sessions").fetchone()[0]
|
|
api_session = db.execute("select max(id) from sessions").fetchone()[0]
|
|
|
db.close()
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|
|
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"]
|
|
check([b["options"] for b in detail["blocks"]
|
|
|
if b["block_type"] == "question"] == [["SQLite", "Postgres"]],
|
|
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",
|
|
check(detail["progress"][0]["summary"] == "Migrated the schemas",
|
|
|
"progress history newest-first")
|
|
"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]
|
|
question = [b for b in detail["blocks"] if b["block_type"] == "question"][0]
|
|
|
act(auth, question["answer_action"], {"option": "SQLite"})
|
|
act(auth, question["answer_action"], {"option": "SQLite"})
|
|
|
check([b["answer"] for b in task_state(auth, api_task)["blocks"]
|
|
check([b["answer"] for b in task_state(auth, api_task)["blocks"]
|
|
|
if b["block_type"] == "question"] == ["SQLite"],
|
|
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"],
|
|
act(auth, detail["comment"],
|
|
|
{"body": "Check the migration output", "quote": "ERROR: table demos exists"})
|
|
{"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)})
|
|
act(auth, detail["cancel"], {"task_id": str(api_task)})
|
|
|
|
|
|
|
|
print("== stm-markdown ==")
|
|
print("== stm-markdown ==")
|