allocation.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. """The allocation bundle (moved verbatim from the pre-split web_stack_test.py;
  2. see test/harness.py for the shared machinery and the runner)."""
  3. from harness import * # noqa: F401,F403 — the shared wire helpers
  4. # =============================================================================
  5. # Bundle: allocation — the ask/wake/allocate machinery: concurrent ask
  6. # race, token revocation, workspace affinity, the lost-wake guard, the
  7. # slow-ask regression, assignment capability context + the tag
  8. # catalogue, and the pinned-blocks/activity dashboard.
  9. # =============================================================================
  10. def run(ctx):
  11. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  12. app = ctx.app
  13. r4_token = fake_token
  14. # gpu rides the primary fake from the start (the routing bundle's
  15. # early attach, replayed): its attach row precedes lostwake's, so
  16. # the assignment's saddle_tags order matches the assertion in the
  17. # capability-context section below.
  18. saddles_state = slot_public(auth, "/saddles", "saddles")
  19. act(auth, saddles_state["create_capability"], {"name": "gpu"})
  20. act(auth, saddle_card(auth, "fake-saddle")["attach_capability"],
  21. {"capability_id": str(int([c["id"] for c in
  22. slot_public(auth, "/saddles", "saddles")["capabilities"]
  23. if c["name"] == "gpu"][0]))})
  24. print("== phase 1: ask/allocation — token auth + concurrent race ==")
  25. # Plan 10 phase 1. Registration mints per-saddle tokens; the
  26. # ask endpoint authenticates by them; concurrent asks race to
  27. # exactly one winner through the allocation transaction.
  28. status, _ = saddle_api("POST", "/api/saddle/next-task", None)
  29. check(status == 401, "next-task rejects a missing token")
  30. status, _ = saddle_api("POST", "/api/saddle/next-task", "saddle-bogus-token")
  31. check(status == 401, "next-task rejects a wrong token")
  32. status, body = saddle_api("POST", "/api/saddle/next-task", fake_token)
  33. check(status == 204, "next-task answers 204 (nothing offerable — daemon sleeps)")
  34. # Two more racers: real daemons on their own ports; fake2 reports
  35. # its ssh key at /register so the key lands on the row directly.
  36. fake2 = FakeSaddle()
  37. fake2.ssh_public_key = "ssh-ed25519 AAAAFake2RacerKey saddle-2@fedora"
  38. fake2.serve(port=free_port())
  39. fake3 = FakeSaddle()
  40. fake3.serve(port=free_port())
  41. saddles_state = slot_public(auth, "/saddles", "saddles")
  42. act(auth, saddles_state["register"], {"name": "racer-two",
  43. "host": "127.0.0.1:%d" % fake2.port})
  44. act(auth, saddles_state["register"], {"name": "racer-three",
  45. "host": "127.0.0.1:%d" % fake3.port})
  46. check(wait_for(lambda: len(fake2.register_log) > 0 and len(fake3.register_log) > 0
  47. or False, timeout=15, what="racers registered") is not None,
  48. "both racers received /register")
  49. check(wait_for(lambda: "AAAAFake2RacerKey" in
  50. [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  51. if s["name"] == "racer-two"][0].get("ssh_public_key", "") or False,
  52. timeout=15, what="register-reported key") is not None,
  53. "the ssh key reported at /register lands on the saddle row")
  54. racer_cards = {}
  55. for name, daemon in (("fake-saddle", fake), ("racer-two", fake2), ("racer-three", fake3)):
  56. card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  57. if s["name"] == name][0]
  58. racer_cards[daemon] = {"id": int(card["id"]), "token": card["token"]}
  59. # Revocation: a registered saddle's token works until the saddle
  60. # is removed, then nothing authorises its calls.
  61. fake4 = FakeSaddle()
  62. fake4.ask_on_register = False # a pre-flip daemon: never asks
  63. fake4.serve(port=free_port())
  64. saddles_state = slot_public(auth, "/saddles", "saddles")
  65. act(auth, saddles_state["register"], {"name": "old-daemon",
  66. "host": "127.0.0.1:%d" % fake4.port})
  67. old_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  68. if s["name"] == "old-daemon"][0]
  69. status, _ = saddle_api("POST", "/api/saddle/next-task", old_card["token"])
  70. check(status in (200, 204),
  71. "the old daemon's fresh token authenticates next-task")
  72. status, _ = saddle_api("POST", "/api/saddle/note", old_card["token"],
  73. {"project": "demo", "body": "from the old daemon"})
  74. check(status == 200, "note accepts any registered saddle's token")
  75. act(auth, old_card["unregister"], {})
  76. status, _ = saddle_api("POST", "/api/saddle/next-task", old_card["token"])
  77. check(status == 401, "removing the saddle revokes its token")
  78. status, _ = saddle_api("POST", "/api/saddle/note", old_card["token"],
  79. {"project": "demo", "body": "should fail"})
  80. check(status == 401, "revocation covers the note endpoint too")
  81. # Deterministic race: settle the queue first (this shard runs the
  82. # race before any parked work exists, so only in-flight asks need
  83. # draining), freeze every daemon's self-ask triggers, put all three
  84. # to sleep (204), then fire one task worth of concurrent asks.
  85. for daemon in (fake, fake2, fake3):
  86. daemon.ask_on_register = False
  87. daemon.ask_on_wake = False
  88. daemon.ask_on_terminal = False
  89. daemon.ask_on_idle_status = False
  90. daemon.settle_asks() # converted sleep: in-flight asks land (204)
  91. def queued_count():
  92. conn = db()
  93. try:
  94. return conn.execute(
  95. "select count(*) from tasks where status = 'queued'").fetchone()[0]
  96. finally:
  97. conn.close()
  98. check(wait_for(lambda: queued_count() == 0 or False,
  99. timeout=60, what="queue settles before the race") is not None,
  100. "the queue settles before the race")
  101. sleep_statuses = [daemon.ask_now()[0] for daemon in (fake, fake2, fake3)]
  102. check(sleep_statuses == [204, 204, 204],
  103. "all three racers sleep (204) before the task exists")
  104. task_id_tmp = create_queued(auth, {
  105. "project_id": "1",
  106. "title": "Race task",
  107. "priority": "2",
  108. })
  109. race_task = task_id_tmp
  110. race_results = []
  111. def race_ask(daemon):
  112. # A real racing daemon accepts the assignment it wins — the
  113. # manual ask path does exactly that on a 200.
  114. race_results.append(daemon.ask_now())
  115. threads = [threading.Thread(target=race_ask, args=(daemon,))
  116. for daemon in (fake, fake2, fake3)]
  117. for thread in threads:
  118. thread.start()
  119. for thread in threads:
  120. thread.join()
  121. race_statuses = sorted(r[0] for r in race_results)
  122. check(race_statuses == [200, 204, 204],
  123. "concurrent asks race to exactly one winner (%s)" % race_statuses)
  124. winner = [r for r in race_results if r[0] == 200][0][1]
  125. check(int(winner["task"]["id"]) == race_task, "the winner's payload carries the task")
  126. check(int(winner["session_id"]) > 0, "the assignment carries the session id")
  127. conn = db()
  128. race_sessions = conn.execute(
  129. "select count(*) from sessions where task_id = ?", (race_task,)).fetchone()[0]
  130. conn.close()
  131. check(race_sessions == 1,
  132. "exactly one session row — the transaction serialised the claims (%d)"
  133. % race_sessions)
  134. check(wait_task_done(auth, race_task, timeout=60) is not None,
  135. "the raced task completes on the winning daemon")
  136. for daemon in (fake, fake2, fake3):
  137. daemon.ask_on_register = True
  138. daemon.ask_on_wake = True
  139. daemon.ask_on_terminal = True
  140. daemon.ask_on_idle_status = True
  141. # Re-establish sleeping explicitly: the winner's post-race
  142. # re-ask timing is not deterministic, and the affinity section
  143. # below needs every racer provably asleep. Any stray offerable
  144. # drains into these asks first.
  145. def all_asleep():
  146. return all(daemon.ask_now()[0] == 204 for daemon in (fake, fake2, fake3))
  147. check(wait_for(lambda: all_asleep() or False, timeout=30,
  148. what="racers re-sleep after the race") is not None,
  149. "all racers re-sleep after the race")
  150. print("== phase 1: workspace affinity pins parked tasks ==")
  151. # A suspended parent parked on racer-two may be resumed ONLY by
  152. # racer-two: a tag-capable non-affine saddle asking gets 204, and
  153. # the wake targets the affine saddle. Only racer-two carries the
  154. # tag at dispatch time (the race's winner is nondeterministic, so
  155. # the park must not depend on which racers are free); racer-three
  156. # gains it afterwards to prove the pinning is by workspace, not
  157. # tags.
  158. saddles_state = slot_public(auth, "/saddles", "saddles")
  159. act(auth, saddles_state["create_capability"], {"name": "affinity-tag"})
  160. affinity_cap = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")["capabilities"]
  161. if c["name"] == "affinity-tag"][0])
  162. racer2_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  163. if s["name"] == "racer-two"][0]
  164. racer3_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  165. if s["name"] == "racer-three"][0]
  166. act(auth, racer2_card["attach_capability"], {"capability_id": str(affinity_cap)})
  167. # Plan 12: the parked workspace is the goal agent's run task. The
  168. # other racers sleep (frozen ask triggers), so racer-two claims
  169. # the goal run and parks it; the goal-task event then resumes the
  170. # agent AFFINE to racer-two — racer-three (tag-capable or not)
  171. # gets 204, the dispatcher's wake targets racer-two.
  172. fake.ask_on_wake = False
  173. fake.ask_on_idle_status = False
  174. fake.settle_asks()
  175. fake3.ask_on_wake = False
  176. fake3.ask_on_idle_status = False
  177. fake3.settle_asks()
  178. act(auth, slot_public(auth, "/goals", "goals")["create"], {
  179. "project_id": "1", "title": "Affinity goal",
  180. "brief": "Parks on racer-two.", "branch_name": "goal/affinity"})
  181. conn = db()
  182. affinity_goal = conn.execute(
  183. "select id from goals where title = 'Affinity goal'").fetchone()[0]
  184. conn.close()
  185. fake2.goal_script = [("plan", "# Affinity plan\n\n1. Park")]
  186. act(auth, slot_public(auth, "/goals/%d" % affinity_goal, "goal")["start"], {})
  187. check(wait_for(lambda: slot_public(auth, "/goals/%d" % affinity_goal,
  188. "goal")["status"] == "plan_review" or False,
  189. timeout=60, what="affinity goal plans") is not None,
  190. "the affinity goal reaches plan review (racer-two hosting)")
  191. fake2.goal_script = [("suspend",)]
  192. # Freeze racer-two's ask triggers across the approval too (the
  193. # later wake measurement already does): the mirror-sync fix makes
  194. # the run resume-offerable the moment its goal row carries the
  195. # verdict, so a wake-triggered background ask would race this
  196. # manual ask_now for the claim.
  197. fake2.ask_on_wake = False
  198. fake2.ask_on_idle_status = False
  199. fake2.settle_asks()
  200. act(auth, slot_public(auth, "/goals/%d" % affinity_goal, "goal")
  201. ["plan_pending_block"]["answer_action"],
  202. {"answer": "Plan approved. Proceed with implementation."})
  203. conn = db()
  204. affinity_parent = conn.execute(
  205. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  206. (affinity_goal,)).fetchone()[0]
  207. agent_session = conn.execute(
  208. "select max(id) from sessions where task_id = ?",
  209. (affinity_parent,)).fetchone()[0]
  210. conn.close()
  211. status, resume_payload = fake2.ask_now()
  212. check(status == 200 and int(resume_payload["task"]["id"]) == affinity_parent,
  213. "the approved plan resumes the agent on racer-two (its workspace)")
  214. check(wait_for(lambda: task_state(auth, affinity_parent).get("status") == "suspended"
  215. or False, timeout=60, what="affinity agent parks") is not None,
  216. "the affinity goal agent parks on racer-two")
  217. conn = db()
  218. affinity_session = conn.execute(
  219. "select saddle_id from sessions where task_id = ?"
  220. " order by id desc limit 1",
  221. (affinity_parent,)).fetchone()[0]
  222. conn.close()
  223. check(affinity_session == int(racer2_card["id"]),
  224. "the parked workspace sits on racer-two")
  225. # Now racer-three carries the tag too — the pinning is by
  226. # workspace, not tags. With nothing due, a non-affine ask gets 204.
  227. act(auth, racer3_card["attach_capability"], {"capability_id": str(affinity_cap)})
  228. status, _ = fake3.ask_now()
  229. check(status == 204,
  230. "a tag-capable non-affine saddle is refused the parked agent (204)")
  231. # A goal task completing makes the parked agent resume-offerable —
  232. # affine to racer-two only. The other racers wake back up (they
  233. # host the worker; their claims of the parked agent get refused as
  234. # non-affine), while racer-two's ask triggers stay frozen so the
  235. # wake measurement stays clean. A wake may have hit the frozen
  236. # racers while the goal run queued (the lost-wake guard marks them
  237. # offline): the refresh probe brings them back before the worker.
  238. fake.ask_on_wake = True
  239. fake.ask_on_idle_status = True
  240. fake3.ask_on_wake = True
  241. fake3.ask_on_idle_status = True
  242. for racer_name in ("fake-saddle", "racer-three"):
  243. act(auth, saddle_card(auth, racer_name)["refresh"], {})
  244. check(wait_for(lambda name=racer_name: [s for s in
  245. slot_public(auth, "/saddles", "saddles")["saddles"]
  246. if s["name"] == name][0]["status"] == "idle"
  247. or False, timeout=15, what="racer re-onlined") is not None,
  248. "%s re-onlined after its frozen-window wake" % racer_name)
  249. fake2.ask_on_wake = False
  250. fake2.ask_on_idle_status = False
  251. wakes_before_2 = len(fake2.wake_log)
  252. fake2.settle_asks() # converted sleep: park-time ask lands (204)
  253. status, body = saddle_api("POST", "/api/saddle/task/create", fake2.token, {
  254. "session_id": agent_session, "title": "Affinity child",
  255. "mandate": "finish the affinity fixture"})
  256. check(status == 200, "the parked agent fans its worker out via the CLI")
  257. affinity_child = int(body["task_id"])
  258. saddle_api("POST", "/api/saddle/task/enqueue", fake2.token,
  259. {"session_id": agent_session, "task_id": affinity_child})
  260. check(wait_task_done(auth, affinity_child, timeout=90) is not None,
  261. "the affinity worker completes (on a non-affine racer)")
  262. check(wait_for(lambda: len(fake2.wake_log) > wakes_before_2 or False,
  263. timeout=90, what="affine wake") is not None,
  264. "the dispatcher's wake targets the affine saddle")
  265. status, resume_payload = fake2.ask_now()
  266. fake2.ask_on_wake = True # restored whatever the claim returned
  267. fake2.ask_on_idle_status = True
  268. check(status == 200 and int(resume_payload["task"]["id"]) == affinity_parent,
  269. "the affine saddle's ask claims the parked agent")
  270. check("Affinity child" in (resume_payload.get("resume_notice") or "")
  271. and "fake daemon finished" in (resume_payload.get("resume_notice") or ""),
  272. "the resume assignment carries the goal's batched report notice")
  273. check(wait_for(lambda: task_state(auth, affinity_parent).get("status")
  274. == "suspended" or False,
  275. timeout=60, what="affinity agent re-parks") is not None,
  276. "the resumed agent re-parks (script)")
  277. act(auth, slot_public(auth, "/goals/%d" % affinity_goal, "goal")["cancel"], {})
  278. check(wait_for(lambda: slot_public(auth, "/goals/%d" % affinity_goal,
  279. "goal")["status"] == "cancelled" or False,
  280. timeout=30, what="affinity goal cancelled") is not None,
  281. "the affinity goal cancels (drain)")
  282. print("== phase 1: lost wake -> offline + re-wake of the next capable ==")
  283. # racer-three sleeps through wakes (a daemon that died after its
  284. # last 204): the guard window (SADDLE_LOST_WAKE_S=2) passes with
  285. # no ask, the saddle goes offline, and the work moves to the
  286. # next capable sleeping saddle.
  287. fake3.ask_on_wake = False
  288. fake3.ask_on_idle_status = False # sleeps through everything
  289. saddles_state = slot_public(auth, "/saddles", "saddles")
  290. act(auth, saddles_state["create_capability"], {"name": "lostwake"})
  291. lostwake_cap = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")["capabilities"]
  292. if c["name"] == "lostwake"][0])
  293. racer3_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  294. if s["name"] == "racer-three"][0]
  295. act(auth, racer3_card["attach_capability"], {"capability_id": str(lostwake_cap)})
  296. # Racer-three must be asleep to be woken: park it with a manual
  297. # 204 ask (nothing offerable for it — the task below is not
  298. # created yet).
  299. status, _ = fake3.ask_now()
  300. check(status == 204, "racer-three sleeps before the lost-wake run")
  301. task_id_tmp = create_queued(auth, {
  302. "project_id": "1",
  303. "title": "Lost wake task",
  304. "priority": "2",
  305. "capability_id": [str(lostwake_cap)],
  306. })
  307. lost_task = task_id_tmp
  308. check(wait_for(lambda: len(fake3.wake_log) > 0 or False,
  309. timeout=30, what="wake sent to racer-three") is not None,
  310. "the only capable sleeping saddle is woken")
  311. check(wait_for(lambda: [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  312. if s["name"] == "racer-three"][0]["status"] == "offline"
  313. or False, timeout=30, what="lost-wake -> offline") is not None,
  314. "no ask after the wake: the saddle goes offline (lost-wake guard)")
  315. check(task_state(auth, lost_task)["status"] == "queued",
  316. "the task is untouched while its wake was pending")
  317. # Re-wake the next capable: the healthy saddle gains the tag and
  318. # picks the work up via its own wake -> ask -> allocate roundtrip.
  319. fake_card = saddle_card(auth, "fake-saddle")
  320. act(auth, fake_card["attach_capability"], {"capability_id": str(lostwake_cap)})
  321. wakes_before = len(fake.wake_log)
  322. check(wait_for(lambda: len(fake.wake_log) > wakes_before or False,
  323. timeout=30, what="re-wake of the next capable") is not None,
  324. "the next capable sleeping saddle is woken for the task")
  325. check(wait_task_done(auth, lost_task, timeout=60) is not None,
  326. "wake -> ask -> allocate: the task completes on the healthy saddle")
  327. check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == lost_task
  328. for p in fake.start_payloads) or False,
  329. timeout=10, what="lost task assignment captured") is not None,
  330. "the re-woken saddle's ask produced the assignment")
  331. fake3.ask_on_wake = True
  332. fake3.ask_on_idle_status = True
  333. print("== phase 3: capability context, catalogue, subtask tags ==")
  334. # Assignment capability context (plan 10 phase 3): every
  335. # assignment carries {saddle_tags: the allocated saddle's tags,
  336. # fleet_tags: every tag name known across the fleet} — the
  337. # daemon renders it into its system prompt. fake-saddle carries
  338. # gpu + lostwake; the catalogue is affinity-tag, gpu, lostwake.
  339. fake2.ask_on_register = False
  340. fake2.ask_on_wake = False
  341. fake2.ask_on_terminal = False
  342. fake2.ask_on_idle_status = False
  343. fake2.settle_asks() # converted sleep: in-flight trigger asks land
  344. task_id_tmp = create_queued(auth, {
  345. "project_id": "1",
  346. "title": "Capabilities context",
  347. "priority": "2",
  348. })
  349. cap_task = task_id_tmp
  350. check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == cap_task
  351. for p in fake.start_payloads) or False,
  352. timeout=60, what="capabilities-context task dispatched") is not None,
  353. "the capabilities-context task dispatches")
  354. cap_payload = [p for p in fake.start_payloads
  355. if int(p.get("task", {}).get("id") or 0) == cap_task][-1]
  356. context = cap_payload.get("capabilities") or {}
  357. check(context.get("saddle_tags") == ["gpu", "lostwake"],
  358. "assignment saddle_tags = the allocated saddle's tags (%s)"
  359. % context.get("saddle_tags"))
  360. check(context.get("fleet_tags") == ["affinity-tag", "gpu", "lostwake"],
  361. "assignment fleet_tags = every tag name known across the fleet (%s)"
  362. % context.get("fleet_tags"))
  363. check(wait_task_done(auth, cap_task, timeout=60) is not None,
  364. "capabilities-context task completes")
  365. # GET /api/saddle/capabilities feeds the daemon's `saddle
  366. # capabilities` CLI: per-saddle-token auth, tag order by name,
  367. # each tag with the saddles carrying it (name order).
  368. status, _ = saddle_api("GET", "/api/saddle/capabilities", None)
  369. check(status == 401, "capabilities endpoint rejects a missing token")
  370. status, body = saddle_api("GET", "/api/saddle/capabilities", "saddle-bogus")
  371. check(status == 401, "capabilities endpoint rejects a wrong token")
  372. status, body = saddle_api("GET", "/api/saddle/capabilities", fake_token)
  373. catalogue = {t["name"]: t["saddles"] for t in body.get("tags", [])}
  374. check(status == 200 and body.get("ok") is True
  375. and [t["name"] for t in body.get("tags", [])]
  376. == ["affinity-tag", "gpu", "lostwake"],
  377. "catalogue lists every tag by name")
  378. check(catalogue.get("gpu") == ["fake-saddle"]
  379. and catalogue.get("affinity-tag") == ["racer-three", "racer-two"]
  380. and catalogue.get("lostwake") == ["fake-saddle", "racer-three"],
  381. "each tag names its carrying saddles in name order (%s)" % catalogue)
  382. # Task CLI capabilities (`task create --capability`): validated
  383. # against the operator tag set; unknown tags reject with the valid
  384. # list in the error (the agent reads it verbatim in the CLI
  385. # output). A goal agent's parked session hosts the calls.
  386. fake.suspend_on_title = "Tag host"
  387. tag_host = create_queued(auth, {
  388. "project_id": "1", "title": "Tag host", "priority": "3"})
  389. check(wait_for(lambda: task_state(auth, tag_host)["status"] == "suspended"
  390. or False, timeout=60, what="tag host parks") is not None,
  391. "a parked plain task hosts nothing (no goal scope)")
  392. # The goal callbacks need a real goal agent: reuse the sequencing
  393. # goal pattern — a fresh parked agent under this shard.
  394. act(auth, slot_public(auth, "/goals", "goals")["create"], {
  395. "project_id": "1", "title": "Tag goal", "brief": "b",
  396. "branch_name": "goal/tags"})
  397. conn = db()
  398. tag_goal = conn.execute("select id from goals where title = 'Tag goal'").fetchone()[0]
  399. conn.close()
  400. fake.goal_script = [("suspend",)]
  401. act(auth, slot_public(auth, "/goals/%d" % tag_goal, "goal")["start"], {})
  402. conn = db()
  403. tag_run = conn.execute(
  404. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  405. (tag_goal,)).fetchone()[0]
  406. conn.close()
  407. check(wait_for(lambda: task_state(auth, tag_run)["status"] == "suspended"
  408. or False, timeout=60, what="tag goal agent parks") is not None,
  409. "the tag goal agent parks")
  410. conn = db()
  411. tag_session = conn.execute(
  412. "select max(id) from sessions where task_id = ?",
  413. (tag_run,)).fetchone()[0]
  414. conn.close()
  415. conn = db()
  416. tag_host_saddle = conn.execute(
  417. "select saddle_id from sessions where id = ?", (tag_session,)).fetchone()[0]
  418. conn.close()
  419. racer2_card_id = int([c for c in slot_public(auth, "/saddles", "saddles")["saddles"]
  420. if c["name"] == "racer-two"][0]["id"])
  421. racer3_card_id = int([c for c in slot_public(auth, "/saddles", "saddles")["saddles"]
  422. if c["name"] == "racer-three"][0]["id"])
  423. host_daemon = {int(fake_id): fake, racer2_card_id: fake2,
  424. racer3_card_id: fake3}[int(tag_host_saddle)]
  425. status, body = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  426. "session_id": tag_session, "title": "Bad tag worker",
  427. "mandate": "m", "capabilities": ["gpu", "warp-drive"]})
  428. check(status == 400, "an unknown capability tag rejects the task call")
  429. error_text = body.get("error", "")
  430. check("warp-drive" in error_text and "valid tags" in error_text
  431. and "gpu" in error_text and "lostwake" in error_text,
  432. "the rejection names the unknown tag and the valid list (%s)" % error_text)
  433. status, _ = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  434. "session_id": tag_session, "title": "Bad shape worker",
  435. "mandate": "m", "capabilities": "gpu"})
  436. check(status == 400, "a non-array capabilities member rejects")
  437. # A valid tag creates the goal worker in draft with exactly the
  438. # explicit tag attached; the goal agent enqueues it.
  439. status, body = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  440. "session_id": tag_session, "title": "Explicit tag worker",
  441. "mandate": "do the tagged work", "capabilities": ["gpu"]})
  442. check(status == 200 and body.get("ok") is True,
  443. "a valid tag creates the task synchronously")
  444. tagged_worker = int(body["task_id"])
  445. detail = task_state(auth, tagged_worker)
  446. check([c["name"] for c in detail.get("capabilities", [])] == ["gpu"],
  447. "the worker carries exactly the explicit tag")
  448. conn = db()
  449. tag_rows = conn.execute(
  450. "select c.name from task_capabilities tc"
  451. " join capabilities c on c.id = tc.capability_id"
  452. " where tc.task_id = ?", (tagged_worker,)).fetchall()
  453. conn.close()
  454. check([r[0] for r in tag_rows] == ["gpu"],
  455. "the task_capabilities rows record the explicit tag")
  456. check(detail["status"] == "draft",
  457. "the callback's task lands in draft until its agent enqueues")
  458. saddle_api("POST", "/api/saddle/task/enqueue", host_daemon.token,
  459. {"session_id": tag_session, "task_id": tagged_worker})
  460. check(wait_task_done(auth, tagged_worker, timeout=90) is not None,
  461. "the tagged worker dispatches and completes")
  462. act(auth, slot_public(auth, "/goals/%d" % tag_goal, "goal")["cancel"], {})
  463. act(auth, task_state(auth, tag_host)["cancel"], {})
  464. print("== phase 3: dashboard — pinned blocks + activity timeline ==")
  465. # Pinned OPEN blocking items as link-through cards, newest
  466. # first; no inline answer forms (the card links to the task
  467. # page). Fixture: one open question + one open review raised on a
  468. # local task (stand-ins for the auto-approve grandchild's
  469. # leftovers) join the probe RFC raised below.
  470. conn = db()
  471. conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
  472. " values (1, 'Dash fixture task', 'working', 5, strftime('%s','now'))")
  473. dash_task = conn.execute("select max(id) from tasks").fetchone()[0]
  474. conn.execute("insert into sessions (task_id, saddle_id, started_at)"
  475. " values (?, ?, strftime('%s','now'))", (dash_task, fake_id))
  476. dash_session = conn.execute("select max(id) from sessions").fetchone()[0]
  477. conn.commit()
  478. conn.close()
  479. def raise_on_dash(question, kind="question", body=None, summary=None):
  480. """Push-era raises on the dashboard fixture task (the poll-era
  481. /update channel is gone): a blocked results push carries the
  482. block payload; a review verb rides a review entry."""
  483. if kind == "review":
  484. block = {"question": question, "type": "review"}
  485. if summary is not None:
  486. block["body"] = summary
  487. return saddle_api("POST", "/api/saddle/results", fake_token, {
  488. "session_id": dash_session, "state": "blocked",
  489. "block": block})[0]
  490. block = {"question": question, "type": kind}
  491. if body is not None:
  492. block["body"] = body
  493. return saddle_api("POST", "/api/saddle/results", fake_token, {
  494. "session_id": dash_session, "state": "blocked",
  495. "block": block})[0]
  496. raise_on_dash("Dash fixture question?", kind="question")
  497. raise_on_dash("dash fixture review", kind="review", summary="dash fixture review")
  498. # The activity timeline: last <= 50 events, newest first, fed
  499. # from the existing tables. Fixture rows a few seconds old
  500. # guarantee the progress kind and a human-authored comment ride
  501. # along; the probe block below proves same-source ordering.
  502. conn = db()
  503. conn.execute("insert into task_comments (task_id, author, body, created_at)"
  504. " values (?, 'user:admin', 'timeline fixture comment by a human',"
  505. " strftime('%s','now','-5 seconds'))", (cap_task,))
  506. conn.execute(
  507. "insert into events (kind, actor, scope_type, scope_id, payload,"
  508. " created_at) values ('comment', 'user', 'task', ?,"
  509. " ?, strftime('%s','now','-5 seconds'))",
  510. (cap_task, json.dumps({"comment_id": 0, "author": "user:admin",
  511. "body": "timeline fixture comment by a human"})))
  512. conn.execute(
  513. "insert into events (kind, actor, scope_type, scope_id, payload,"
  514. " created_at) values ('progress', 'model', 'task', ?,"
  515. " ?, strftime('%s','now','-5 seconds'))",
  516. (cap_task, json.dumps({"text": "timeline fixture progress"})))
  517. conn.commit()
  518. conn.close()
  519. # The probe RFC raised last: the newest pinned card and the newest
  520. # timeline event (its raise+answer below prove same-source ordering
  521. # both ways). The timeline sorts same-second events by source
  522. # materialisation order, so the probe must land in a LATER clock
  523. # second than the fixtures — wait for the boundary (bounded).
  524. fixtures_epoch = int(time.time())
  525. wait_for(lambda: int(time.time()) > fixtures_epoch or False,
  526. timeout=3, what="next clock second before the probe raise")
  527. raise_on_dash("Timeline probe: approve the dashboard flow",
  528. kind="rfc", body="# Probe")
  529. dash = slot_public(auth, "/", "dashboard")
  530. pinned = dash.get("pinned", [])
  531. pinned_ids = [int(b["id"]) for b in pinned]
  532. check(len(pinned_ids) >= 3 and pinned_ids == sorted(pinned_ids, reverse=True),
  533. "pinned open blocks order newest-first (%s)" % pinned_ids)
  534. check({b["block_type"] for b in pinned} >= {"question", "rfc", "review"},
  535. "pinned cards cover questions, RFCs and reviews")
  536. check(all(b["question"] and b["task_title"] and int(b["task_id"]) > 0
  537. for b in pinned)
  538. and all("answer_action" not in b for b in pinned),
  539. "pinned cards are link-through only (task title, kind badge, question)")
  540. activity = dash.get("activity", [])
  541. epochs = [int(e["at_epoch"]) for e in activity]
  542. check(0 < len(activity) <= 50 and epochs == sorted(epochs, reverse=True),
  543. "the timeline shows the last <=50 events newest-first (%d events)"
  544. % len(activity))
  545. kinds = {e["kind"] for e in activity}
  546. check({"created", "started", "dispatch", "comment", "progress", "completed"} <= kinds,
  547. "the timeline covers status changes, dispatches, comments, progress and"
  548. " completions (%s)" % sorted(kinds))
  549. check(any(e["kind"] == "comment" and e["text"].startswith("user:admin:")
  550. and "fixture comment" in e["text"] for e in activity),
  551. "comment events carry author + snippet")
  552. # The freshly raised probe pinned as the newest card and landed
  553. # atop the timeline as block-raised; answering it records
  554. # block-answered.
  555. check(dash["pinned"][0]["question"] == "Timeline probe: approve the dashboard flow"
  556. and int(dash["pinned"][0]["task_id"]) == dash_task,
  557. "the freshest open block pins as the first card")
  558. check(dash["activity"][0]["kind"] == "block-raised"
  559. and "Timeline probe" in dash["activity"][0]["text"],
  560. "the raised block lands atop the timeline")
  561. probe_block = [b for b in task_state(auth, dash_task)["blocks"]
  562. if b["question"] == "Timeline probe: approve the dashboard flow"][0]
  563. act(auth, probe_block["answer_action"], {"answer": "Ship the dashboard"})
  564. dash = slot_public(auth, "/", "dashboard")
  565. activity = dash.get("activity", [])
  566. raised_at = next((i for i, e in enumerate(activity)
  567. if e["kind"] == "block-raised" and "Timeline probe" in e["text"]), -1)
  568. answered_at = next((i for i, e in enumerate(activity)
  569. if e["kind"] == "block-answered"
  570. and "Ship the dashboard" in e["text"]), -1)
  571. check(answered_at >= 0 and raised_at >= 0 and answered_at < raised_at,
  572. "the answered event sorts above its raised event (newest-first, same source)")
  573. check(all(b["question"] != "Timeline probe: approve the dashboard flow"
  574. for b in dash["pinned"]),
  575. "the answered block leaves the pinned cards")
  576. print("== phase 3 regression: a slow ask after a wake is never dropped ==")
  577. # The daemon worker's lost-wake-guard regression: a saddle whose
  578. # ask arrives SLOWLY after the wake (busy winding down when the
  579. # wake lands) must not lose the wake — the delayed ask (inside
  580. # the guard window) claims the task, no re-wake storm fires
  581. # while it is pending, and the saddle stays healthy. The
  582. # never-asks path (guard -> offline + re-wake of the next
  583. # capable) is proven by the racer-three section above.
  584. check(wait_for(lambda: queued_count() == 0 or False,
  585. timeout=60, what="queue settles before the slow-wake run") is not None,
  586. "the queue settles before the slow-wake run")
  587. status, _ = fake2.ask_now()
  588. check(status == 204, "the slow-ask racer sleeps before its wake")
  589. saddles_state = slot_public(auth, "/saddles", "saddles")
  590. act(auth, saddles_state["create_capability"], {"name": "slowwake"})
  591. slowwake_cap = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")
  592. ["capabilities"] if c["name"] == "slowwake"][0])
  593. racer2_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  594. if s["name"] == "racer-two"][0]
  595. act(auth, racer2_card["attach_capability"], {"capability_id": str(slowwake_cap)})
  596. fake2.ask_delay = 1.2 # slow: asks well after the wake, inside the 2s guard
  597. fake2.ask_on_wake = True
  598. wakes_before_slow = len(fake2.wake_log)
  599. task_id_tmp = create_queued(auth, {
  600. "project_id": "1",
  601. "title": "Slow wake task",
  602. "priority": "2",
  603. "capability_id": [str(slowwake_cap)],
  604. })
  605. slow_task = task_id_tmp
  606. check(wait_for(lambda: len(fake2.wake_log) > wakes_before_slow or False,
  607. timeout=30, what="the slow-ask racer woken") is not None,
  608. "the only capable sleeping saddle is woken")
  609. check(wait_task_done(auth, slow_task, timeout=120) is not None,
  610. "the delayed ask claims the task — the wake was not dropped")
  611. check(any(a.get("status") == 200
  612. and int((a.get("payload") or {}).get("task", {}).get("id") or 0)
  613. == slow_task for a in fake2.asks),
  614. "the slow ask's 200 carries the task")
  615. check(len(fake2.wake_log) == wakes_before_slow + 1,
  616. "the pending-wake skip re-wakes nobody while the slow ask is in flight")
  617. racer2_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  618. if s["name"] == "racer-two"][0]
  619. check(racer2_card["status"] in ("idle", "busy"),
  620. "the slow asker never went offline (guard did not fire)")
  621. fake2.ask_delay = 0.0
  622. fake2.ask_on_wake = False # stays out of the pool below
  623. print("== goal 4 t1: plain-task force wake — offerable on its affine saddle ==")
  624. # A suspended PLAIN task stays parked until the human force-wakes
  625. # it; the resuming claim carries the explicit wake notice and
  626. # consumes the flag (one wake per press).
  627. fake2.ask_on_wake = False
  628. fake2.ask_on_idle_status = False
  629. fake2.settle_asks()
  630. conn = db()
  631. conn.execute("insert into tasks (project_id, kind, title, status, priority, mode,"
  632. " creator, created_at, suspended_at) values (1, 'task',"
  633. " 'Park me', 'suspended', 5, 'implement', 'human',"
  634. " strftime('%s','now'), strftime('%s','now'))")
  635. plain_parked = conn.execute("select max(id) from tasks").fetchone()[0]
  636. # parked_offerable keys the affine saddle off the newest SESSION
  637. # row (plan 16 D4): a parked round's row is closed 'parked' — give
  638. # the fixture one so the wake offers on the fake saddle.
  639. conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at,"
  640. " outcome) values (?, ?, strftime('%s','now'),"
  641. " strftime('%s','now'), 'parked')",
  642. (plain_parked, racer2_card_id))
  643. conn.commit()
  644. conn.close()
  645. task_page = http_get_page(auth, "/tasks/%d" % plain_parked)
  646. check('stm-action="task.force_wake"' in task_page
  647. and 'stm-if="task.force_wake != null"' in task_page,
  648. "the task page authors the Force wake button for a suspended plain task")
  649. # No press, no wake: frozen-ask racer-two gets 204.
  650. status, _ = fake2.ask_now()
  651. check(status == 204,
  652. "without a press the parked plain task stays parked (204)")
  653. detail = task_state(auth, plain_parked)
  654. check(detail.get("force_wake") is not None,
  655. "the task state exposes the offered force-wake action")
  656. # Determinism: the wake's trigger-ask (and any idle-probe ask) must
  657. # not race the MANUAL claim the section asserts on.
  658. fake2.ask_on_wake = False
  659. fake2.ask_on_idle_status = False
  660. act(auth, detail["force_wake"], {})
  661. status, resume_payload = fake2.ask_now()
  662. check(status == 200 and int(resume_payload["task"]["id"]) == plain_parked,
  663. "the force-woke plain task is claimable again")
  664. woke_notice = resume_payload.get("resume_notice") or ""
  665. check("force-woke this parked task" in woke_notice
  666. and "resume where you left off" in woke_notice,
  667. "the plain-task resume carries the explicit wake notice")
  668. conn = db()
  669. flag_row = conn.execute("select force_wake from tasks where id = ?",
  670. (plain_parked,)).fetchone()
  671. conn.close()
  672. check(flag_row is not None and flag_row[0] in (0, False),
  673. "the claim consumed the task force-wake flag")
  674. fake2.ask_on_wake = True
  675. fake2.ask_on_idle_status = True
  676. # The woken run re-parks (fake daemon finishes then suspends? no —
  677. # fake.start_payloads script: the plain task runs to done via the
  678. # daemon's own finish path in later suites; here just let it be
  679. # requeued by completing it directly to keep the fixture short).
  680. check(wait_task_done(auth, plain_parked, timeout=60) is not None,
  681. "the woken plain task completes (daemon finish path)")
  682. # Guard: a non-suspended task offers no wake action (the template
  683. # ships the gated markup statically; absence is a STATE property).
  684. check(task_state(auth, plain_parked).get("force_wake") is None,
  685. "a done task offers no force-wake action")