allocation.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  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 id from sessions where task_id = ? and ended_at is null",
  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 = ? and ended_at is null",
  220. (affinity_parent,)).fetchone()[0]
  221. conn.close()
  222. check(affinity_session == int(racer2_card["id"]),
  223. "the parked workspace sits on racer-two")
  224. # Now racer-three carries the tag too — the pinning is by
  225. # workspace, not tags. With nothing due, a non-affine ask gets 204.
  226. act(auth, racer3_card["attach_capability"], {"capability_id": str(affinity_cap)})
  227. status, _ = fake3.ask_now()
  228. check(status == 204,
  229. "a tag-capable non-affine saddle is refused the parked agent (204)")
  230. # A goal task completing makes the parked agent resume-offerable —
  231. # affine to racer-two only. The other racers wake back up (they
  232. # host the worker; their claims of the parked agent get refused as
  233. # non-affine), while racer-two's ask triggers stay frozen so the
  234. # wake measurement stays clean. A wake may have hit the frozen
  235. # racers while the goal run queued (the lost-wake guard marks them
  236. # offline): the refresh probe brings them back before the worker.
  237. fake.ask_on_wake = True
  238. fake.ask_on_idle_status = True
  239. fake3.ask_on_wake = True
  240. fake3.ask_on_idle_status = True
  241. for racer_name in ("fake-saddle", "racer-three"):
  242. act(auth, saddle_card(auth, racer_name)["refresh"], {})
  243. check(wait_for(lambda name=racer_name: [s for s in
  244. slot_public(auth, "/saddles", "saddles")["saddles"]
  245. if s["name"] == name][0]["status"] == "idle"
  246. or False, timeout=15, what="racer re-onlined") is not None,
  247. "%s re-onlined after its frozen-window wake" % racer_name)
  248. fake2.ask_on_wake = False
  249. fake2.ask_on_idle_status = False
  250. wakes_before_2 = len(fake2.wake_log)
  251. fake2.settle_asks() # converted sleep: park-time ask lands (204)
  252. status, body = saddle_api("POST", "/api/saddle/task/create", fake2.token, {
  253. "session_id": agent_session, "title": "Affinity child",
  254. "mandate": "finish the affinity fixture"})
  255. check(status == 200, "the parked agent fans its worker out via the CLI")
  256. affinity_child = int(body["task_id"])
  257. saddle_api("POST", "/api/saddle/task/enqueue", fake2.token,
  258. {"session_id": agent_session, "task_id": affinity_child})
  259. check(wait_task_done(auth, affinity_child, timeout=90) is not None,
  260. "the affinity worker completes (on a non-affine racer)")
  261. check(wait_for(lambda: len(fake2.wake_log) > wakes_before_2 or False,
  262. timeout=90, what="affine wake") is not None,
  263. "the dispatcher's wake targets the affine saddle")
  264. status, resume_payload = fake2.ask_now()
  265. fake2.ask_on_wake = True # restored whatever the claim returned
  266. fake2.ask_on_idle_status = True
  267. check(status == 200 and int(resume_payload["task"]["id"]) == affinity_parent,
  268. "the affine saddle's ask claims the parked agent")
  269. check("Affinity child" in (resume_payload.get("resume_notice") or "")
  270. and "fake daemon finished" in (resume_payload.get("resume_notice") or ""),
  271. "the resume assignment carries the goal's batched report notice")
  272. check(wait_for(lambda: task_state(auth, affinity_parent).get("status")
  273. == "suspended" or False,
  274. timeout=60, what="affinity agent re-parks") is not None,
  275. "the resumed agent re-parks (script)")
  276. act(auth, slot_public(auth, "/goals/%d" % affinity_goal, "goal")["cancel"], {})
  277. check(wait_for(lambda: slot_public(auth, "/goals/%d" % affinity_goal,
  278. "goal")["status"] == "cancelled" or False,
  279. timeout=30, what="affinity goal cancelled") is not None,
  280. "the affinity goal cancels (drain)")
  281. print("== phase 1: lost wake -> offline + re-wake of the next capable ==")
  282. # racer-three sleeps through wakes (a daemon that died after its
  283. # last 204): the guard window (SADDLE_LOST_WAKE_S=2) passes with
  284. # no ask, the saddle goes offline, and the work moves to the
  285. # next capable sleeping saddle.
  286. fake3.ask_on_wake = False
  287. fake3.ask_on_idle_status = False # sleeps through everything
  288. saddles_state = slot_public(auth, "/saddles", "saddles")
  289. act(auth, saddles_state["create_capability"], {"name": "lostwake"})
  290. lostwake_cap = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")["capabilities"]
  291. if c["name"] == "lostwake"][0])
  292. racer3_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  293. if s["name"] == "racer-three"][0]
  294. act(auth, racer3_card["attach_capability"], {"capability_id": str(lostwake_cap)})
  295. # Racer-three must be asleep to be woken: park it with a manual
  296. # 204 ask (nothing offerable for it — the task below is not
  297. # created yet).
  298. status, _ = fake3.ask_now()
  299. check(status == 204, "racer-three sleeps before the lost-wake run")
  300. task_id_tmp = create_queued(auth, {
  301. "project_id": "1",
  302. "title": "Lost wake task",
  303. "priority": "2",
  304. "capability_id": [str(lostwake_cap)],
  305. })
  306. lost_task = task_id_tmp
  307. check(wait_for(lambda: len(fake3.wake_log) > 0 or False,
  308. timeout=30, what="wake sent to racer-three") is not None,
  309. "the only capable sleeping saddle is woken")
  310. check(wait_for(lambda: [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  311. if s["name"] == "racer-three"][0]["status"] == "offline"
  312. or False, timeout=30, what="lost-wake -> offline") is not None,
  313. "no ask after the wake: the saddle goes offline (lost-wake guard)")
  314. check(task_state(auth, lost_task)["status"] == "queued",
  315. "the task is untouched while its wake was pending")
  316. # Re-wake the next capable: the healthy saddle gains the tag and
  317. # picks the work up via its own wake -> ask -> allocate roundtrip.
  318. fake_card = saddle_card(auth, "fake-saddle")
  319. act(auth, fake_card["attach_capability"], {"capability_id": str(lostwake_cap)})
  320. wakes_before = len(fake.wake_log)
  321. check(wait_for(lambda: len(fake.wake_log) > wakes_before or False,
  322. timeout=30, what="re-wake of the next capable") is not None,
  323. "the next capable sleeping saddle is woken for the task")
  324. check(wait_task_done(auth, lost_task, timeout=60) is not None,
  325. "wake -> ask -> allocate: the task completes on the healthy saddle")
  326. check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == lost_task
  327. for p in fake.start_payloads) or False,
  328. timeout=10, what="lost task assignment captured") is not None,
  329. "the re-woken saddle's ask produced the assignment")
  330. fake3.ask_on_wake = True
  331. fake3.ask_on_idle_status = True
  332. print("== phase 3: capability context, catalogue, subtask tags ==")
  333. # Assignment capability context (plan 10 phase 3): every
  334. # assignment carries {saddle_tags: the allocated saddle's tags,
  335. # fleet_tags: every tag name known across the fleet} — the
  336. # daemon renders it into its system prompt. fake-saddle carries
  337. # gpu + lostwake; the catalogue is affinity-tag, gpu, lostwake.
  338. fake2.ask_on_register = False
  339. fake2.ask_on_wake = False
  340. fake2.ask_on_terminal = False
  341. fake2.ask_on_idle_status = False
  342. fake2.settle_asks() # converted sleep: in-flight trigger asks land
  343. task_id_tmp = create_queued(auth, {
  344. "project_id": "1",
  345. "title": "Capabilities context",
  346. "priority": "2",
  347. })
  348. cap_task = task_id_tmp
  349. check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == cap_task
  350. for p in fake.start_payloads) or False,
  351. timeout=60, what="capabilities-context task dispatched") is not None,
  352. "the capabilities-context task dispatches")
  353. cap_payload = [p for p in fake.start_payloads
  354. if int(p.get("task", {}).get("id") or 0) == cap_task][-1]
  355. context = cap_payload.get("capabilities") or {}
  356. check(context.get("saddle_tags") == ["gpu", "lostwake"],
  357. "assignment saddle_tags = the allocated saddle's tags (%s)"
  358. % context.get("saddle_tags"))
  359. check(context.get("fleet_tags") == ["affinity-tag", "gpu", "lostwake"],
  360. "assignment fleet_tags = every tag name known across the fleet (%s)"
  361. % context.get("fleet_tags"))
  362. check(wait_task_done(auth, cap_task, timeout=60) is not None,
  363. "capabilities-context task completes")
  364. # GET /api/saddle/capabilities feeds the daemon's `saddle
  365. # capabilities` CLI: per-saddle-token auth, tag order by name,
  366. # each tag with the saddles carrying it (name order).
  367. status, _ = saddle_api("GET", "/api/saddle/capabilities", None)
  368. check(status == 401, "capabilities endpoint rejects a missing token")
  369. status, body = saddle_api("GET", "/api/saddle/capabilities", "saddle-bogus")
  370. check(status == 401, "capabilities endpoint rejects a wrong token")
  371. status, body = saddle_api("GET", "/api/saddle/capabilities", fake_token)
  372. catalogue = {t["name"]: t["saddles"] for t in body.get("tags", [])}
  373. check(status == 200 and body.get("ok") is True
  374. and [t["name"] for t in body.get("tags", [])]
  375. == ["affinity-tag", "gpu", "lostwake"],
  376. "catalogue lists every tag by name")
  377. check(catalogue.get("gpu") == ["fake-saddle"]
  378. and catalogue.get("affinity-tag") == ["racer-three", "racer-two"]
  379. and catalogue.get("lostwake") == ["fake-saddle", "racer-three"],
  380. "each tag names its carrying saddles in name order (%s)" % catalogue)
  381. # Task CLI capabilities (`task create --capability`): validated
  382. # against the operator tag set; unknown tags reject with the valid
  383. # list in the error (the agent reads it verbatim in the CLI
  384. # output). A goal agent's parked session hosts the calls.
  385. fake.suspend_on_title = "Tag host"
  386. tag_host = create_queued(auth, {
  387. "project_id": "1", "title": "Tag host", "priority": "3"})
  388. check(wait_for(lambda: task_state(auth, tag_host)["status"] == "suspended"
  389. or False, timeout=60, what="tag host parks") is not None,
  390. "a parked plain task hosts nothing (no goal scope)")
  391. # The goal callbacks need a real goal agent: reuse the sequencing
  392. # goal pattern — a fresh parked agent under this shard.
  393. act(auth, slot_public(auth, "/goals", "goals")["create"], {
  394. "project_id": "1", "title": "Tag goal", "brief": "b",
  395. "branch_name": "goal/tags"})
  396. conn = db()
  397. tag_goal = conn.execute("select id from goals where title = 'Tag goal'").fetchone()[0]
  398. conn.close()
  399. fake.goal_script = [("suspend",)]
  400. act(auth, slot_public(auth, "/goals/%d" % tag_goal, "goal")["start"], {})
  401. conn = db()
  402. tag_run = conn.execute(
  403. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  404. (tag_goal,)).fetchone()[0]
  405. conn.close()
  406. check(wait_for(lambda: task_state(auth, tag_run)["status"] == "suspended"
  407. or False, timeout=60, what="tag goal agent parks") is not None,
  408. "the tag goal agent parks")
  409. conn = db()
  410. tag_session = conn.execute(
  411. "select id from sessions where task_id = ? and ended_at is null",
  412. (tag_run,)).fetchone()[0]
  413. conn.close()
  414. conn = db()
  415. tag_host_saddle = conn.execute(
  416. "select saddle_id from sessions where id = ?", (tag_session,)).fetchone()[0]
  417. conn.close()
  418. racer2_card_id = int([c for c in slot_public(auth, "/saddles", "saddles")["saddles"]
  419. if c["name"] == "racer-two"][0]["id"])
  420. racer3_card_id = int([c for c in slot_public(auth, "/saddles", "saddles")["saddles"]
  421. if c["name"] == "racer-three"][0]["id"])
  422. host_daemon = {int(fake_id): fake, racer2_card_id: fake2,
  423. racer3_card_id: fake3}[int(tag_host_saddle)]
  424. status, body = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  425. "session_id": tag_session, "title": "Bad tag worker",
  426. "mandate": "m", "capabilities": ["gpu", "warp-drive"]})
  427. check(status == 400, "an unknown capability tag rejects the task call")
  428. error_text = body.get("error", "")
  429. check("warp-drive" in error_text and "valid tags" in error_text
  430. and "gpu" in error_text and "lostwake" in error_text,
  431. "the rejection names the unknown tag and the valid list (%s)" % error_text)
  432. status, _ = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  433. "session_id": tag_session, "title": "Bad shape worker",
  434. "mandate": "m", "capabilities": "gpu"})
  435. check(status == 400, "a non-array capabilities member rejects")
  436. # A valid tag creates the goal worker in draft with exactly the
  437. # explicit tag attached; the goal agent enqueues it.
  438. status, body = saddle_api("POST", "/api/saddle/task/create", host_daemon.token, {
  439. "session_id": tag_session, "title": "Explicit tag worker",
  440. "mandate": "do the tagged work", "capabilities": ["gpu"]})
  441. check(status == 200 and body.get("ok") is True,
  442. "a valid tag creates the task synchronously")
  443. tagged_worker = int(body["task_id"])
  444. detail = task_state(auth, tagged_worker)
  445. check([c["name"] for c in detail.get("capabilities", [])] == ["gpu"],
  446. "the worker carries exactly the explicit tag")
  447. conn = db()
  448. tag_rows = conn.execute(
  449. "select c.name from task_capabilities tc"
  450. " join capabilities c on c.id = tc.capability_id"
  451. " where tc.task_id = ?", (tagged_worker,)).fetchall()
  452. conn.close()
  453. check([r[0] for r in tag_rows] == ["gpu"],
  454. "the task_capabilities rows record the explicit tag")
  455. check(detail["status"] == "draft",
  456. "the callback's task lands in draft until its agent enqueues")
  457. saddle_api("POST", "/api/saddle/task/enqueue", host_daemon.token,
  458. {"session_id": tag_session, "task_id": tagged_worker})
  459. check(wait_task_done(auth, tagged_worker, timeout=90) is not None,
  460. "the tagged worker dispatches and completes")
  461. act(auth, slot_public(auth, "/goals/%d" % tag_goal, "goal")["cancel"], {})
  462. act(auth, task_state(auth, tag_host)["cancel"], {})
  463. print("== phase 3: dashboard — pinned blocks + activity timeline ==")
  464. # Pinned OPEN blocking items as link-through cards, newest
  465. # first; no inline answer forms (the card links to the task
  466. # page). Fixture: one open question + one open review raised on a
  467. # local task (stand-ins for the auto-approve grandchild's
  468. # leftovers) join the probe RFC raised below.
  469. conn = db()
  470. conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
  471. " values (1, 'Dash fixture task', 'working', 5, strftime('%s','now'))")
  472. dash_task = conn.execute("select max(id) from tasks").fetchone()[0]
  473. conn.execute("insert into sessions (task_id, saddle_id, started_at)"
  474. " values (?, ?, strftime('%s','now'))", (dash_task, fake_id))
  475. dash_session = conn.execute("select max(id) from sessions").fetchone()[0]
  476. conn.commit()
  477. conn.close()
  478. def raise_on_dash(question, kind="question", body=None, summary=None):
  479. """Push-era raises on the dashboard fixture task (the poll-era
  480. /update channel is gone): a blocked results push carries the
  481. block payload; a review verb rides a review entry."""
  482. if kind == "review" and summary is not None:
  483. return saddle_api("POST", "/api/saddle/results", fake_token, {
  484. "session_id": dash_session, "state": "working",
  485. "entries": [{"id": 9317,
  486. "command": "saddle review",
  487. "output": summary, "type": "review",
  488. "timestamp": time.strftime(
  489. "%Y-%m-%dT%H:%M:%SZ", time.gmtime())}]})[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("insert into task_progress (task_id, summary, created_at)"
  507. " values (?, 'timeline fixture progress',"
  508. " strftime('%s','now','-5 seconds'))",
  509. (cap_task,))
  510. conn.commit()
  511. conn.close()
  512. # The probe RFC raised last: the newest pinned card and the newest
  513. # timeline event (its raise+answer below prove same-source ordering
  514. # both ways). The timeline sorts same-second events by source
  515. # materialisation order, so the probe must land in a LATER clock
  516. # second than the fixtures — wait for the boundary (bounded).
  517. fixtures_epoch = int(time.time())
  518. wait_for(lambda: int(time.time()) > fixtures_epoch or False,
  519. timeout=3, what="next clock second before the probe raise")
  520. raise_on_dash("Timeline probe: approve the dashboard flow",
  521. kind="rfc", body="# Probe")
  522. dash = slot_public(auth, "/", "dashboard")
  523. pinned = dash.get("pinned", [])
  524. pinned_ids = [int(b["id"]) for b in pinned]
  525. check(len(pinned_ids) >= 3 and pinned_ids == sorted(pinned_ids, reverse=True),
  526. "pinned open blocks order newest-first (%s)" % pinned_ids)
  527. check({b["block_type"] for b in pinned} >= {"question", "rfc", "review"},
  528. "pinned cards cover questions, RFCs and reviews")
  529. check(all(b["question"] and b["task_title"] and int(b["task_id"]) > 0
  530. for b in pinned)
  531. and all("answer_action" not in b for b in pinned),
  532. "pinned cards are link-through only (task title, kind badge, question)")
  533. activity = dash.get("activity", [])
  534. epochs = [int(e["at_epoch"]) for e in activity]
  535. check(0 < len(activity) <= 50 and epochs == sorted(epochs, reverse=True),
  536. "the timeline shows the last <=50 events newest-first (%d events)"
  537. % len(activity))
  538. kinds = {e["kind"] for e in activity}
  539. check({"created", "started", "dispatch", "comment", "progress", "completed"} <= kinds,
  540. "the timeline covers status changes, dispatches, comments, progress and"
  541. " completions (%s)" % sorted(kinds))
  542. check(any(e["kind"] == "comment" and e["text"].startswith("user:admin:")
  543. and "fixture comment" in e["text"] for e in activity),
  544. "comment events carry author + snippet")
  545. # The freshly raised probe pinned as the newest card and landed
  546. # atop the timeline as block-raised; answering it records
  547. # block-answered.
  548. check(dash["pinned"][0]["question"] == "Timeline probe: approve the dashboard flow"
  549. and int(dash["pinned"][0]["task_id"]) == dash_task,
  550. "the freshest open block pins as the first card")
  551. check(dash["activity"][0]["kind"] == "block-raised"
  552. and "Timeline probe" in dash["activity"][0]["text"],
  553. "the raised block lands atop the timeline")
  554. probe_block = [b for b in task_state(auth, dash_task)["blocks"]
  555. if b["question"] == "Timeline probe: approve the dashboard flow"][0]
  556. act(auth, probe_block["answer_action"], {"answer": "Ship the dashboard"})
  557. dash = slot_public(auth, "/", "dashboard")
  558. activity = dash.get("activity", [])
  559. raised_at = next((i for i, e in enumerate(activity)
  560. if e["kind"] == "block-raised" and "Timeline probe" in e["text"]), -1)
  561. answered_at = next((i for i, e in enumerate(activity)
  562. if e["kind"] == "block-answered"
  563. and "Ship the dashboard" in e["text"]), -1)
  564. check(answered_at >= 0 and raised_at >= 0 and answered_at < raised_at,
  565. "the answered event sorts above its raised event (newest-first, same source)")
  566. check(all(b["question"] != "Timeline probe: approve the dashboard flow"
  567. for b in dash["pinned"]),
  568. "the answered block leaves the pinned cards")
  569. print("== phase 3 regression: a slow ask after a wake is never dropped ==")
  570. # The daemon worker's lost-wake-guard regression: a saddle whose
  571. # ask arrives SLOWLY after the wake (busy winding down when the
  572. # wake lands) must not lose the wake — the delayed ask (inside
  573. # the guard window) claims the task, no re-wake storm fires
  574. # while it is pending, and the saddle stays healthy. The
  575. # never-asks path (guard -> offline + re-wake of the next
  576. # capable) is proven by the racer-three section above.
  577. check(wait_for(lambda: queued_count() == 0 or False,
  578. timeout=60, what="queue settles before the slow-wake run") is not None,
  579. "the queue settles before the slow-wake run")
  580. status, _ = fake2.ask_now()
  581. check(status == 204, "the slow-ask racer sleeps before its wake")
  582. saddles_state = slot_public(auth, "/saddles", "saddles")
  583. act(auth, saddles_state["create_capability"], {"name": "slowwake"})
  584. slowwake_cap = int([c["id"] for c in slot_public(auth, "/saddles", "saddles")
  585. ["capabilities"] if c["name"] == "slowwake"][0])
  586. racer2_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  587. if s["name"] == "racer-two"][0]
  588. act(auth, racer2_card["attach_capability"], {"capability_id": str(slowwake_cap)})
  589. fake2.ask_delay = 1.2 # slow: asks well after the wake, inside the 2s guard
  590. fake2.ask_on_wake = True
  591. wakes_before_slow = len(fake2.wake_log)
  592. task_id_tmp = create_queued(auth, {
  593. "project_id": "1",
  594. "title": "Slow wake task",
  595. "priority": "2",
  596. "capability_id": [str(slowwake_cap)],
  597. })
  598. slow_task = task_id_tmp
  599. check(wait_for(lambda: len(fake2.wake_log) > wakes_before_slow or False,
  600. timeout=30, what="the slow-ask racer woken") is not None,
  601. "the only capable sleeping saddle is woken")
  602. check(wait_task_done(auth, slow_task, timeout=120) is not None,
  603. "the delayed ask claims the task — the wake was not dropped")
  604. check(any(a.get("status") == 200
  605. and int((a.get("payload") or {}).get("task", {}).get("id") or 0)
  606. == slow_task for a in fake2.asks),
  607. "the slow ask's 200 carries the task")
  608. check(len(fake2.wake_log) == wakes_before_slow + 1,
  609. "the pending-wake skip re-wakes nobody while the slow ask is in flight")
  610. racer2_card = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
  611. if s["name"] == "racer-two"][0]
  612. check(racer2_card["status"] in ("idle", "busy"),
  613. "the slow asker never went offline (guard did not fire)")
  614. fake2.ask_delay = 0.0
  615. fake2.ask_on_wake = False # stays out of the pool below