lineage.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. """The lineage 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: lineage — the plan-12 goal lineage: goal context on worker
  6. # dispatches (read-only reference + ephemeral branch), the daemon's
  7. # task-reading API (goal members, no parent/plan), and the goal
  8. # agent's workspace affinity while parked.
  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 exists only as a hold (no saddle carries it): the held goal
  15. # worker stays queued until the release loop lifts it.
  16. saddles_state = slot_public(auth, "/saddles", "saddles")
  17. act(auth, saddles_state["create_capability"], {"name": "gpu"})
  18. def create_goal_row(fields):
  19. act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
  20. rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
  21. if g["title"] == fields["title"]]
  22. return int(rows[0]["id"])
  23. goal = create_goal_row({"project_id": "1", "title": "Lineage goal",
  24. "brief": "Carry the goal lineage.",
  25. "branch_name": "goal/lineage"})
  26. fake.goal_script = [("plan", "# Lineage plan\n\n1. Fan out\n2. Verify")]
  27. act(auth, goal_state(goal)["start"], {})
  28. check(wait_for(lambda: goal_state(goal)["status"] == "plan_review" or False,
  29. timeout=60, what="lineage goal plans") is not None,
  30. "the lineage goal reaches plan review")
  31. fake.goal_script = [("suspend",)]
  32. act(auth, goal_state(goal)["plan_pending_block"]["answer_action"],
  33. {"answer": "Plan approved. Proceed with implementation."})
  34. conn = db()
  35. auto_parent = conn.execute(
  36. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  37. (goal,)).fetchone()[0]
  38. conn.close()
  39. check(wait_for(lambda: task_state(auth, auto_parent)["status"] == "suspended"
  40. or False, timeout=60, what="lineage goal agent parks") is not None,
  41. "the lineage goal agent parks")
  42. conn = db()
  43. agent_session = conn.execute(
  44. "select max(id) from sessions where task_id = ?",
  45. (auto_parent,)).fetchone()[0]
  46. conn.close()
  47. # Two workers under the goal: one free, one held by the gpu tag.
  48. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  49. "session_id": agent_session, "title": "Auto child",
  50. "mandate": "build the widget", "mode": "implement"})
  51. auto_child = int(body["task_id"])
  52. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  53. "session_id": agent_session, "title": "Auto grandchild",
  54. "mandate": "verify the widget", "mode": "test",
  55. "capabilities": ["gpu"]})
  56. auto_grand = int(body["task_id"])
  57. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  58. "session_id": agent_session, "title": "Wire the flange",
  59. "description": "torque to spec"})
  60. check(status == 400 and "mandate is required" in (body.get("error") or ""),
  61. "task create without a mandate errors verbatim (%s)" % body.get("error"))
  62. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  63. "session_id": agent_session, "title": "Wire the flange",
  64. "mandate": "torque to spec"})
  65. flange_task = int(body["task_id"])
  66. for held in (auto_child, auto_grand, flange_task):
  67. saddle_api("POST", "/api/saddle/task/enqueue", r4_token,
  68. {"session_id": agent_session, "task_id": held})
  69. check(wait_for(lambda: task_state(auth, auto_child)["status"] in
  70. ("queued", "assigned", "working", "done") or False,
  71. timeout=30, what="free goal worker dispatches") is not None,
  72. "the free goal worker dispatches")
  73. check(quiet_window(3, lambda: task_state(auth, auto_grand)["status"] == "queued",
  74. "held goal worker stays queued"),
  75. "the gpu-held goal worker stays queued")
  76. # A goal worker's dispatch carries the read-only goal context.
  77. lineage_push = [p for p in fake.start_payloads
  78. if int(p.get("task", {}).get("id") or 0) == auto_child][-1]
  79. goal_ctx = lineage_push.get("goal") or {}
  80. check(int(goal_ctx.get("id") or 0) == goal
  81. and goal_ctx.get("branch") == "goal/lineage"
  82. and "Carry the goal lineage." in goal_ctx.get("brief", "")
  83. and "# Lineage plan" in goal_ctx.get("plan", ""),
  84. "the worker dispatch carries the goal context (brief + approved plan)")
  85. check(lineage_push.get("ephemeral_branch") == "task/%d-%d" % (goal, auto_child)
  86. and lineage_push.get("kind") == "task"
  87. and lineage_push.get("mode") == "implement"
  88. and lineage_push.get("creator") == "goal",
  89. "the worker dispatch carries the ephemeral branch + identity triplet")
  90. check("parent" not in lineage_push and "subtasks" not in lineage_push,
  91. "the plan-12 payload drops the parent/subtasks members")
  92. print("== R5 B2: task-reading API ==")
  93. status, _ = saddle_api("GET", "/api/saddle/tasks", None)
  94. check(status == 401, "tasks endpoint rejects a missing token")
  95. status, _ = saddle_api("GET", "/api/saddle/task/%d" % auto_child, None)
  96. check(status == 401, "task detail endpoint rejects a missing token")
  97. # Fixture: a second project whose tasks all carry the "Diff " title
  98. # prefix for the project_id filter checks — created first so the
  99. # unfiltered list has the population the count check expects.
  100. state = slot_public(auth, "/projects", "projects")
  101. act(auth, state["create"], {"name": "diff-fabrica",
  102. "git_url": "ssh://fabrica.unitatem.net:222/clanker/clanker-saddle.git"})
  103. conn = db()
  104. other_project = int([p["id"] for p in slot_public(auth, "/projects", "projects")["projects"]
  105. if p["name"] == "diff-fabrica"][0])
  106. for suffix in ("fabrica", "scp"):
  107. conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
  108. " values (?, ?, 'done', 5, strftime('%s','now'))",
  109. (other_project, "Diff %s" % suffix))
  110. conn.commit()
  111. conn.close()
  112. status, body = saddle_api("GET", "/api/saddle/tasks", r4_token)
  113. check(status == 200 and len(body.get("tasks", [])) > 5,
  114. "tasks endpoint lists tasks without filters")
  115. row = [t for t in body["tasks"] if int(t["id"]) == auto_child][0]
  116. check(row["title"] == "Auto child" and int(row["goal_id"] or 0) == goal
  117. and row["status"] in ("done", "queued", "working"),
  118. "compact rows carry id/title/status/goal_id")
  119. status, body = saddle_api("GET", "/api/saddle/tasks?project_id=1", r4_token)
  120. check(all(True for _ in [1]) # project 1 dominates the data set
  121. and not any(int(t["id"]) == 0 for t in body["tasks"]),
  122. "project_id filter accepted")
  123. status, body = saddle_api("GET",
  124. "/api/saddle/tasks?project_id=%d" % other_project, r4_token)
  125. check(all(t["title"].startswith("Diff") for t in body["tasks"]),
  126. "other project lists only its own tasks")
  127. status, body = saddle_api("GET", "/api/saddle/tasks?q=FLANGE", r4_token)
  128. titles = [t["title"] for t in body["tasks"]]
  129. check("Wire the flange" in titles,
  130. "q matches titles case-insensitively")
  131. status, body = saddle_api("GET", "/api/saddle/tasks?q=torque%20to%20spec", r4_token)
  132. check(any(t["title"] == "Wire the flange" for t in body["tasks"]),
  133. "q matches mandate text")
  134. status, body = saddle_api("GET", "/api/saddle/task/%d" % auto_child, r4_token)
  135. task_view = body.get("task", {})
  136. check(status == 200 and int(task_view.get("id") or 0) == auto_child
  137. and task_view.get("goal_title") == "Lineage goal"
  138. and int(task_view.get("goal_id") or 0) == goal,
  139. "single-task view joins the goal title")
  140. check("status" in task_view and "description" in task_view
  141. and "summary" in task_view,
  142. "single-task view carries the full detail fields")
  143. check("plan" not in task_view and "parent_task_id" not in task_view,
  144. "the single-task view drops the deleted parent/plan members")
  145. status, body = saddle_api("GET", "/api/saddle/task/99999", r4_token)
  146. check(status == 404, "unknown task id returns 404")
  147. print("== goal 9: the read-only goals API (list_goals wire) ==")
  148. # The daemon's list_goals tool reads this endpoint: bearer-only,
  149. # project-scoped (primary OR linked), newest first, with a
  150. # goal_id detail carrying the plan/report presence flags.
  151. status, _ = saddle_api("GET", "/api/saddle/goals", None)
  152. check(status == 401, "goals endpoint rejects a missing token")
  153. status, _ = saddle_api("GET", "/api/saddle/goals?project_id=1", r4_token)
  154. check(status == 200, "goals endpoint ignores stray params (project is by name)")
  155. # A linked-project goal: the primary's rows plus a goal that only
  156. # references the project through goal_links.
  157. conn = db()
  158. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  159. " values ('goal-fabrica',"
  160. " 'ssh://fabrica.unitatem.net:222/clanker/clanker-saddle.git',"
  161. " 'main', strftime('%s','now'))")
  162. conn.commit()
  163. conn.close()
  164. state = slot_public(auth, "/projects", "projects")
  165. goal_fabrica = int([p["id"] for p in state["projects"]
  166. if p["name"] == "goal-fabrica"][0])
  167. goal_via_link = create_goal_row({
  168. "project_id": "1", "title": "Linked-context goal",
  169. "brief": "Touches goal-fabrica only through its link.",
  170. "branch_name": "goal/linked-context"})
  171. act(auth, goal_state(goal_via_link)["add_link"],
  172. {"linked_project_id": str(goal_fabrica),
  173. "relationship": "co-developed"})
  174. status, body = saddle_api("GET", "/api/saddle/goals?project=demo",
  175. r4_token)
  176. check(status == 200 and any(int(g["id"]) == goal for g in body.get("goals", [])),
  177. "the primary project's goals list (name lookup)")
  178. row = [g for g in body["goals"] if int(g["id"]) == goal_via_link][0]
  179. check(row["title"] == "Linked-context goal"
  180. and row["status"] in ("draft", "planning", "plan_review", "active", "cancelled", "done")
  181. and row["branch"] == "goal/linked-context"
  182. and len(row.get("updated_at") or "") > 0,
  183. "compact goal rows carry id/title/status/branch/updated_at")
  184. ids = [int(g["id"]) for g in body["goals"]]
  185. check(ids == sorted(ids, reverse=True),
  186. "the goal list reads newest first")
  187. check(not any(int(g["id"]) == goal_via_link
  188. for g in body["goals"] if int(g["id"]) != goal_via_link),
  189. "no duplicate rows for a goal listed once")
  190. status, body = saddle_api("GET", "/api/saddle/goals?project=goal-fabrica",
  191. r4_token)
  192. check(status == 200
  193. and [int(g["id"]) for g in body.get("goals", [])] == [goal_via_link],
  194. "a linked-only project lists its linked goals (not the primary's)")
  195. # No project param: the recent-goals context answers with every
  196. # goal newest-first (the daemon's list_goals defaults to the task's
  197. # primary project, but a standalone task sends no filter — an empty
  198. # answer there would read as "no goals ever", goal 9 task 9).
  199. status, body = saddle_api("GET", "/api/saddle/goals", r4_token)
  200. unfiltered = [int(g["id"]) for g in body.get("goals", [])]
  201. check(status == 200 and unfiltered
  202. and unfiltered == sorted(unfiltered, reverse=True)
  203. and goal_via_link in unfiltered,
  204. "no project param lists every goal newest-first (the whole-repo view)")
  205. status, body = saddle_api("GET",
  206. "/api/saddle/goals?project=no-such-project", r4_token)
  207. check(status == 200 and body.get("goals") == [],
  208. "an unknown project name yields an empty list, not an error")
  209. status, body = saddle_api("GET",
  210. "/api/saddle/goals?goal_id=%d" % goal, r4_token)
  211. detail = body.get("goal") or {}
  212. check(status == 200 and int(detail.get("id") or 0) == goal
  213. and detail.get("title") == "Lineage goal"
  214. and detail.get("branch") == "goal/lineage"
  215. and "Carry the goal lineage." in detail.get("brief", ""),
  216. "the goal_id detail carries id/title/branch/brief")
  217. check(detail.get("has_plan") is True and detail.get("has_report") is False,
  218. "the detail's has_plan/has_report reflect the goal's documents")
  219. detail_link = saddle_api("GET", "/api/saddle/goals?goal_id=%d" % goal_via_link,
  220. r4_token)[1].get("goal") or {}
  221. check(detail_link.get("has_plan") is False,
  222. "a goal without an approved plan reports has_plan false")
  223. status, body = saddle_api("GET", "/api/saddle/goals?goal_id=99999", r4_token)
  224. check(status == 404, "an unknown goal_id returns 404")
  225. print("== settle: released workers resume the parked agent (coalesced) ==")
  226. # Release the held workers: their completions wake the parked goal
  227. # agent (affinity — the same fake hosts it) with the batched
  228. # notice; the free worker already completed.
  229. check(wait_task_done(auth, auto_child, timeout=90) is not None,
  230. "the free worker settles first")
  231. def agent_push_count():
  232. return len([p for p in fake.goal_assignments
  233. if int(p.get("task", {}).get("id") or 0) == auto_parent])
  234. before_pushes = agent_push_count()
  235. for held_task in (auto_grand, flange_task):
  236. caps = task_state(auth, held_task).get("capabilities") or []
  237. if caps:
  238. act(auth, caps[0]["detach"], {})
  239. check(wait_task_done(auth, held_task, timeout=90) is not None,
  240. "released worker settles (%d)" % held_task)
  241. check(wait_for(lambda: agent_push_count() > before_pushes or False,
  242. timeout=90, what="goal agent resumes") is not None,
  243. "the workers' completions resume the parked goal agent")
  244. notice = ""
  245. for p in fake.goal_assignments:
  246. if int(p.get("task", {}).get("id") or 0) == auto_parent:
  247. notice = p.get("resume_notice") or ""
  248. check("Auto child" in notice and "Auto grandchild" in notice
  249. and "Wire the flange" in notice,
  250. "the coalesced resume notice covers every plan task")
  251. check("New completion reports" in notice and "fake daemon finished" in notice,
  252. "the notice carries the new completion reports")
  253. act(auth, goal_state(goal)["cancel"], {})
  254. check(wait_for(lambda: goal_state(goal)["status"] == "cancelled" or False,
  255. timeout=30, what="lineage goal cancelled") is not None,
  256. "the lineage goal cancels (drain)")