stacks.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. """The stacks bundle (goal 8 task 2): the goal link pre-fill from the
  2. primary project's project_links, and the stacks feature — a named
  3. collection of reference projects applied one-click to a project's or a
  4. goal's link set (stacks/stack_projects tables, /stacks pages, the apply
  5. actions with exact-duplicate skipping)."""
  6. from harness import * # noqa: F401,F403 — the shared wire helpers
  7. def run(ctx):
  8. auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
  9. def create_goal(fields):
  10. act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
  11. rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
  12. if g["title"] == fields["title"]]
  13. return int(rows[0]["id"])
  14. def stack_state(stack_id):
  15. return slot_public(auth, "/stacks/%d" % stack_id, "stack")
  16. def link_rows(goal_id):
  17. conn = db()
  18. try:
  19. return conn.execute(
  20. "select gl.project_id, gl.relationship from goal_links gl"
  21. " where gl.goal_id = ? order by gl.id", (goal_id,)).fetchall()
  22. finally:
  23. conn.close()
  24. def project_link_rows(parent_id):
  25. conn = db()
  26. try:
  27. return conn.execute(
  28. "select linked_project_id, relationship from project_links"
  29. " where parent_project_id = ? order by id", (parent_id,)).fetchall()
  30. finally:
  31. conn.close()
  32. print("== goal 8 task 2 A: goal links pre-fill from the project ==")
  33. # Two satellites around the demo project: one co-developed, one
  34. # reference. The goal created on demo must inherit both.
  35. conn = db()
  36. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  37. " values ('stacks-codev', 'https://example.com/codev.git', 'main',"
  38. " strftime('%s','now'))")
  39. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  40. " values ('stacks-ref', 'https://example.com/ref.git', 'main',"
  41. " strftime('%s','now'))")
  42. conn.commit()
  43. codev_id = int(conn.execute(
  44. "select id from projects where name = 'stacks-codev'").fetchone()[0])
  45. ref_id = int(conn.execute(
  46. "select id from projects where name = 'stacks-ref'").fetchone()[0])
  47. # A third project linked to the project but AFTER goal creation —
  48. # it must NOT propagate (the pre-fill is a one-time snapshot).
  49. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  50. " values ('stacks-late', 'https://example.com/late.git', 'main',"
  51. " strftime('%s','now'))")
  52. conn.commit()
  53. late_id = int(conn.execute(
  54. "select id from projects where name = 'stacks-late'").fetchone()[0])
  55. conn.execute("insert into project_links (parent_project_id, linked_project_id,"
  56. " relationship) values (1, ?, 'co-developed')", (codev_id,))
  57. conn.execute("insert into project_links (parent_project_id, linked_project_id,"
  58. " relationship) values (1, ?, 'reference')", (ref_id,))
  59. conn.commit()
  60. conn.close()
  61. goal = create_goal({"project_id": "1", "title": "Prefill probe",
  62. "brief": "Check the link pre-fill.",
  63. "branch_name": "goal/prefill", "priority": "3"})
  64. check(set(link_rows(goal)) == {(codev_id, "co-developed"), (ref_id, "reference")},
  65. "a goal created on a linked project pre-fills goal_links from"
  66. " project_links (same targets, same relationships)")
  67. state = goal_state(goal)
  68. check(sorted(link["name"] for link in state["links"]) == ["stacks-codev", "stacks-ref"],
  69. "the goal page lists the pre-filled links")
  70. check(state["links"][0].get("remove") is not None,
  71. "the pre-filled links are prunable like manual ones")
  72. # The late link must not propagate into the existing goal.
  73. conn = db()
  74. conn.execute("insert into project_links (parent_project_id, linked_project_id,"
  75. " relationship) values (1, ?, 'reference')", (late_id,))
  76. conn.commit()
  77. conn.close()
  78. check(all(row[0] != late_id for row in link_rows(goal)),
  79. "a project link added AFTER creation never propagates (snapshot)")
  80. # A NEW goal created after the late link lands DOES carry it.
  81. goal2 = create_goal({"project_id": "1", "title": "Prefill probe 2",
  82. "brief": "Snapshot at creation time.",
  83. "branch_name": "goal/prefill2", "priority": "3"})
  84. check(set(link_rows(goal2)) == {(codev_id, "co-developed"),
  85. (ref_id, "reference"),
  86. (late_id, "reference")},
  87. "a goal created after a new project link snapshots that link too")
  88. # Self-links in the project graph are never copied (a project link
  89. # to the goal's own primary is redundant — it is implicit).
  90. conn = db()
  91. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  92. " values ('stacks-self', 'https://example.com/self.git', 'main',"
  93. " strftime('%s','now'))")
  94. conn.commit()
  95. conn.close()
  96. goal3 = create_goal({"project_id": "1", "title": "No self links",
  97. "brief": "The primary is implicit.",
  98. "branch_name": "goal/prefill3", "priority": "3"})
  99. check(all(row[0] != 1 for row in link_rows(goal3)),
  100. "the primary project is never duplicated as an explicit link")
  101. print("== the pre-filled links ride the assignment payload ==")
  102. # Removing one pre-filled link must remove it from the payload —
  103. # goal_links stays the single source of truth after the snapshot.
  104. state = goal_state(goal)
  105. codev_link = [link for link in state["links"] if link["name"] == "stacks-codev"][0]
  106. act(auth, codev_link["remove"], {"link_id": str(codev_link["id"])})
  107. check(set(link_rows(goal)) == {(ref_id, "reference")},
  108. "removing a pre-filled goal link removes the row")
  109. fake.goal_script = [("suspend",)]
  110. act(auth, goal_state(goal)["start"], {})
  111. check(wait_for(lambda: next(
  112. (True for row in db().execute(
  113. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  114. (goal,)).fetchall()), False) or False,
  115. timeout=30, what="goal run task creation") is not None,
  116. "start creates the goal-run task")
  117. conn = db()
  118. run_task = int(conn.execute(
  119. "select id from tasks where goal_id = ? and kind = 'goal_run'",
  120. (goal,)).fetchone()[0])
  121. conn.close()
  122. def assigned_payload():
  123. for payload in reversed(fake.goal_assignments):
  124. if (payload.get("task") or {}).get("id") == run_task:
  125. return payload
  126. return None
  127. check(wait_for(lambda: assigned_payload() is not None or False,
  128. timeout=60, what="goal run assignment") is not None,
  129. "the fake saddle captures the goal-run assignment")
  130. payload = assigned_payload()
  131. codev_names = [p["name"] for p in payload["task"]["co_developed"]]
  132. ref_names = [p["name"] for p in payload["task"]["reference"]]
  133. check("demo" in codev_names and "stacks-codev" not in codev_names,
  134. "the assignment payload's co-developed carries the primary and the"
  135. " surviving pre-filled link (the removed one is gone)")
  136. check(ref_names == ["stacks-ref"],
  137. "the assignment payload's reference set comes from the goal's links")
  138. print("== stacks: CRUD ==")
  139. stacks_state = slot_public(auth, "/stacks", "stacks")
  140. check(stacks_state.get("create") is not None and stacks_state["stacks"] == [],
  141. "the stacks page starts empty with a create action")
  142. act(auth, stacks_state["create"],
  143. {"name": "web-stack-refs", "summary": "The usual reference collection"})
  144. stacks = slot_public(auth, "/stacks", "stacks")["stacks"]
  145. check(len(stacks) == 1 and stacks[0]["name"] == "web-stack-refs"
  146. and int(stacks[0]["member_count"]) == 0,
  147. "a stack lands in the list with its (empty) member count")
  148. stack_id = int(stacks[0]["id"])
  149. detail = stack_state(stack_id)
  150. check(detail["exists"] and detail["name"] == "web-stack-refs"
  151. and detail["members"] == [] and detail.get("update") is not None
  152. and detail.get("add_member") is not None and detail.get("delete") is not None,
  153. "the stack page exposes edit, add-member and delete actions")
  154. check(any(option["name"] == "demo" for option in detail["options"]),
  155. "every project is offerable as a stack member")
  156. # Duplicate name refused.
  157. dup = act(auth, slot_public(auth, "/stacks", "stacks")["create"],
  158. {"name": "web-stack-refs"})
  159. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  160. for d in dup),
  161. "a duplicate stack name refuses")
  162. # Rename + summary clear.
  163. act(auth, detail["update"], {"name": "web-stack-refs-2", "summary": ""})
  164. detail = stack_state(stack_id)
  165. check(detail["name"] == "web-stack-refs-2" and detail["summary"] == "",
  166. "a stack renames and its summary clears")
  167. print("== stacks: members ==")
  168. detail = stack_state(stack_id)
  169. act(auth, detail["add_member"],
  170. {"project_id": str(codev_id), "relationship": "co-developed"})
  171. act(auth, stack_state(stack_id)["add_member"],
  172. {"project_id": str(ref_id), "relationship": "reference"})
  173. detail = stack_state(stack_id)
  174. member_names = [(m["name"], m["relationship"]) for m in detail["members"]]
  175. check(member_names == [("stacks-codev", "co-developed"),
  176. ("stacks-ref", "reference")],
  177. "members land with their relationships")
  178. dup = act(auth, stack_state(stack_id)["add_member"],
  179. {"project_id": str(ref_id), "relationship": "reference"})
  180. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  181. and "already" in (d.get("message") or "") for d in dup),
  182. "adding the same project twice refuses")
  183. bad = act(auth, stack_state(stack_id)["add_member"],
  184. {"project_id": str(late_id), "relationship": "sponsor"})
  185. check(any(d.get("type") == "notify" and d.get("kind") == "error"
  186. for d in bad),
  187. "an unknown relationship refuses")
  188. detail = stack_state(stack_id)
  189. check(len(detail["members"]) == 2,
  190. "the refused additions left the stack untouched")
  191. print("== stacks: apply to a project ==")
  192. conn = db()
  193. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  194. " values ('stacks-target', 'https://example.com/target.git', 'main',"
  195. " strftime('%s','now'))")
  196. conn.commit()
  197. conn.close()
  198. target_id = int(db().execute(
  199. "select id from projects where name = 'stacks-target'"
  200. ).fetchone()[0])
  201. project_state = slot_public(auth, "/projects/%d" % target_id, "project")
  202. check(project_state.get("apply_stack") is not None
  203. and any(opt["name"] == "web-stack-refs-2" for opt in project_state["stack_options"]),
  204. "the project page offers Apply stack with the stacks as options")
  205. result = act(auth, project_state["apply_stack"], {"stack_id": str(stack_id)})
  206. check("2 link" in (action_notify(result) or ""),
  207. "applying reports how many links landed")
  208. check(set(project_link_rows(target_id)) == {(codev_id, "co-developed"),
  209. (ref_id, "reference")},
  210. "the stack's members became project_links rows")
  211. # Re-apply: exact duplicates skipped, nothing duplicated.
  212. result = act(auth, slot_public(auth, "/projects/%d" % target_id,
  213. "project")["apply_stack"],
  214. {"stack_id": str(stack_id)})
  215. check("nothing new" in (action_notify(result) or ""),
  216. "re-applying skips exact duplicates")
  217. check(set(project_link_rows(target_id)) == {(codev_id, "co-developed"),
  218. (ref_id, "reference")},
  219. "no duplicate project_links rows appear")
  220. # A pre-existing DIFFERENT relationship on the same target is not a
  221. # duplicate: applying adds the stack's own relationship alongside.
  222. conn = db()
  223. conn.execute("insert into project_links (parent_project_id, linked_project_id,"
  224. " relationship) values (?, ?, 'co-developed')",
  225. (target_id, ref_id))
  226. conn.commit()
  227. conn.close()
  228. act(auth, slot_public(auth, "/projects/%d" % target_id, "project")["apply_stack"],
  229. {"stack_id": str(stack_id)})
  230. check((ref_id, "reference") in project_link_rows(target_id)
  231. and project_link_rows(target_id).count((ref_id, "co-developed")) == 1,
  232. "a different relationship on the same target coexists (exact pairs skip)")
  233. print("== stacks: apply to a goal ==")
  234. # A goal on a LINK-FREE host project — the pre-fill seeds nothing,
  235. # so the stack's members are exactly what applying must land.
  236. conn = db()
  237. conn.execute("insert into projects (name, git_url, default_branch, created_at)"
  238. " values ('stacks-goalhost', 'https://example.com/goalhost.git',"
  239. " 'main', strftime('%s','now'))")
  240. conn.commit()
  241. conn.close()
  242. host_id = int(db().execute(
  243. "select id from projects where name = 'stacks-goalhost'").fetchone()[0])
  244. goal4 = create_goal({"project_id": str(host_id), "title": "Stack target",
  245. "brief": "Apply a stack here.",
  246. "branch_name": "goal/stacktarget", "priority": "3"})
  247. check(link_rows(goal4) == [],
  248. "a goal on a link-free project pre-fills nothing")
  249. gstate = goal_state(goal4)
  250. check(gstate.get("apply_stack") is not None
  251. and any(opt["name"] == "web-stack-refs-2" for opt in gstate["stack_options"]),
  252. "the goal page offers Apply stack with the stacks as options")
  253. result = act(auth, gstate["apply_stack"], {"stack_id": str(stack_id)})
  254. check("2 link" in (action_notify(result) or ""),
  255. "applying to the goal reports the links landed")
  256. check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
  257. (ref_id, "reference")},
  258. "the stack's members became goal_links rows")
  259. # Re-apply: idempotent.
  260. result = act(auth, goal_state(goal4)["apply_stack"], {"stack_id": str(stack_id)})
  261. check("nothing new" in (action_notify(result) or ""),
  262. "re-applying to the goal skips exact duplicates")
  263. check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
  264. (ref_id, "reference")},
  265. "no duplicate goal_links rows appear")
  266. # Removing a stack member then re-applying adds only the newcomer.
  267. detail = stack_state(stack_id)
  268. ref_view = [m for m in detail["members"]
  269. if int(m["project_id"]) == ref_id][0]
  270. act(auth, ref_view["remove"], {})
  271. check(len(stack_state(stack_id)["members"]) == 1,
  272. "removing a member shrinks the stack")
  273. act(auth, goal_state(goal4)["apply_stack"], {"stack_id": str(stack_id)})
  274. check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
  275. (ref_id, "reference")},
  276. "removing a stack member never unlinks what was already applied")
  277. print("== stacks: delete ==")
  278. conn = db()
  279. act(auth, slot_public(auth, "/stacks/%d" % stack_id, "stack")["delete"], {})
  280. conn.close()
  281. check(slot_public(auth, "/stacks", "stacks")["stacks"] == [],
  282. "deleting a stack empties the list")
  283. conn = db()
  284. leftover = conn.execute("select count(*) from stack_projects").fetchone()[0]
  285. conn.close()
  286. check(leftover == 0, "the stack's member rows die with it")
  287. check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
  288. (ref_id, "reference")},
  289. "deleting a stack keeps every link it already applied")