lineage.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. agent_session = conn.execute(
  39. "select id from sessions where task_id = ? and ended_at is null",
  40. (auto_parent,)).fetchone()[0]
  41. conn.close()
  42. check(wait_for(lambda: task_state(auth, auto_parent)["status"] == "suspended"
  43. or False, timeout=60, what="lineage goal agent parks") is not None,
  44. "the lineage goal agent parks")
  45. # Two workers under the goal: one free, one held by the gpu tag.
  46. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  47. "session_id": agent_session, "title": "Auto child",
  48. "mandate": "build the widget", "mode": "implement"})
  49. auto_child = int(body["task_id"])
  50. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  51. "session_id": agent_session, "title": "Auto grandchild",
  52. "mandate": "verify the widget", "mode": "test",
  53. "capabilities": ["gpu"]})
  54. auto_grand = int(body["task_id"])
  55. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  56. "session_id": agent_session, "title": "Wire the flange",
  57. "description": "torque to spec"})
  58. check(status == 400 and "mandate is required" in (body.get("error") or ""),
  59. "task create without a mandate errors verbatim (%s)" % body.get("error"))
  60. status, body = saddle_api("POST", "/api/saddle/task/create", r4_token, {
  61. "session_id": agent_session, "title": "Wire the flange",
  62. "mandate": "torque to spec"})
  63. flange_task = int(body["task_id"])
  64. for held in (auto_child, auto_grand, flange_task):
  65. saddle_api("POST", "/api/saddle/task/enqueue", r4_token,
  66. {"session_id": agent_session, "task_id": held})
  67. check(wait_for(lambda: task_state(auth, auto_child)["status"] in
  68. ("queued", "assigned", "working", "done") or False,
  69. timeout=30, what="free goal worker dispatches") is not None,
  70. "the free goal worker dispatches")
  71. check(quiet_window(3, lambda: task_state(auth, auto_grand)["status"] == "queued",
  72. "held goal worker stays queued"),
  73. "the gpu-held goal worker stays queued")
  74. # A goal worker's dispatch carries the read-only goal context.
  75. lineage_push = [p for p in fake.start_payloads
  76. if int(p.get("task", {}).get("id") or 0) == auto_child][-1]
  77. goal_ctx = lineage_push.get("goal") or {}
  78. check(int(goal_ctx.get("id") or 0) == goal
  79. and goal_ctx.get("branch") == "goal/lineage"
  80. and "Carry the goal lineage." in goal_ctx.get("brief", "")
  81. and "# Lineage plan" in goal_ctx.get("plan", ""),
  82. "the worker dispatch carries the goal context (brief + approved plan)")
  83. check(lineage_push.get("ephemeral_branch") == "task/%d-%d" % (goal, auto_child)
  84. and lineage_push.get("kind") == "task"
  85. and lineage_push.get("mode") == "implement"
  86. and lineage_push.get("creator") == "goal",
  87. "the worker dispatch carries the ephemeral branch + identity triplet")
  88. check("parent" not in lineage_push and "subtasks" not in lineage_push,
  89. "the plan-12 payload drops the parent/subtasks members")
  90. print("== R5 B2: task-reading API ==")
  91. status, _ = saddle_api("GET", "/api/saddle/tasks", None)
  92. check(status == 401, "tasks endpoint rejects a missing token")
  93. status, _ = saddle_api("GET", "/api/saddle/task/%d" % auto_child, None)
  94. check(status == 401, "task detail endpoint rejects a missing token")
  95. # Fixture: a second project whose tasks all carry the "Diff " title
  96. # prefix for the project_id filter checks — created first so the
  97. # unfiltered list has the population the count check expects.
  98. state = slot_public(auth, "/projects", "projects")
  99. act(auth, state["create"], {"name": "diff-fabrica",
  100. "git_url": "ssh://fabrica.unitatem.net:222/clanker/clanker-saddle.git"})
  101. conn = db()
  102. other_project = int([p["id"] for p in slot_public(auth, "/projects", "projects")["projects"]
  103. if p["name"] == "diff-fabrica"][0])
  104. for suffix in ("fabrica", "scp"):
  105. conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
  106. " values (?, ?, 'done', 5, strftime('%s','now'))",
  107. (other_project, "Diff %s" % suffix))
  108. conn.commit()
  109. conn.close()
  110. status, body = saddle_api("GET", "/api/saddle/tasks", r4_token)
  111. check(status == 200 and len(body.get("tasks", [])) > 5,
  112. "tasks endpoint lists tasks without filters")
  113. row = [t for t in body["tasks"] if int(t["id"]) == auto_child][0]
  114. check(row["title"] == "Auto child" and int(row["goal_id"] or 0) == goal
  115. and row["status"] in ("done", "queued", "working"),
  116. "compact rows carry id/title/status/goal_id")
  117. status, body = saddle_api("GET", "/api/saddle/tasks?project_id=1", r4_token)
  118. check(all(True for _ in [1]) # project 1 dominates the data set
  119. and not any(int(t["id"]) == 0 for t in body["tasks"]),
  120. "project_id filter accepted")
  121. status, body = saddle_api("GET",
  122. "/api/saddle/tasks?project_id=%d" % other_project, r4_token)
  123. check(all(t["title"].startswith("Diff") for t in body["tasks"]),
  124. "other project lists only its own tasks")
  125. status, body = saddle_api("GET", "/api/saddle/tasks?q=FLANGE", r4_token)
  126. titles = [t["title"] for t in body["tasks"]]
  127. check("Wire the flange" in titles,
  128. "q matches titles case-insensitively")
  129. status, body = saddle_api("GET", "/api/saddle/tasks?q=torque%20to%20spec", r4_token)
  130. check(any(t["title"] == "Wire the flange" for t in body["tasks"]),
  131. "q matches mandate text")
  132. status, body = saddle_api("GET", "/api/saddle/task/%d" % auto_child, r4_token)
  133. task_view = body.get("task", {})
  134. check(status == 200 and int(task_view.get("id") or 0) == auto_child
  135. and task_view.get("goal_title") == "Lineage goal"
  136. and int(task_view.get("goal_id") or 0) == goal,
  137. "single-task view joins the goal title")
  138. check("status" in task_view and "description" in task_view
  139. and "summary" in task_view,
  140. "single-task view carries the full detail fields")
  141. check("plan" not in task_view and "parent_task_id" not in task_view,
  142. "the single-task view drops the deleted parent/plan members")
  143. status, body = saddle_api("GET", "/api/saddle/task/99999", r4_token)
  144. check(status == 404, "unknown task id returns 404")
  145. print("== settle: released workers resume the parked agent (coalesced) ==")
  146. # Release the held workers: their completions wake the parked goal
  147. # agent (affinity — the same fake hosts it) with the batched
  148. # notice; the free worker already completed.
  149. check(wait_task_done(auth, auto_child, timeout=90) is not None,
  150. "the free worker settles first")
  151. def agent_push_count():
  152. return len([p for p in fake.goal_assignments
  153. if int(p.get("task", {}).get("id") or 0) == auto_parent])
  154. before_pushes = agent_push_count()
  155. for held_task in (auto_grand, flange_task):
  156. caps = task_state(auth, held_task).get("capabilities") or []
  157. if caps:
  158. act(auth, caps[0]["detach"], {})
  159. check(wait_task_done(auth, held_task, timeout=90) is not None,
  160. "released worker settles (%d)" % held_task)
  161. check(wait_for(lambda: agent_push_count() > before_pushes or False,
  162. timeout=90, what="goal agent resumes") is not None,
  163. "the workers' completions resume the parked goal agent")
  164. notice = ""
  165. for p in fake.goal_assignments:
  166. if int(p.get("task", {}).get("id") or 0) == auto_parent:
  167. notice = p.get("resume_notice") or ""
  168. check("Auto child" in notice and "Auto grandchild" in notice
  169. and "Wire the flange" in notice,
  170. "the coalesced resume notice covers every plan task")
  171. check("New completion reports" in notice and "fake daemon finished" in notice,
  172. "the notice carries the new completion reports")
  173. act(auth, goal_state(goal)["cancel"], {})
  174. check(wait_for(lambda: goal_state(goal)["status"] == "cancelled" or False,
  175. timeout=30, what="lineage goal cancelled") is not None,
  176. "the lineage goal cancels (drain)")