|
|
@@ -0,0 +1,316 @@
|
|
|
+"""The stacks bundle (goal 8 task 2): the goal link pre-fill from the
|
|
|
+primary project's project_links, and the stacks feature — a named
|
|
|
+collection of reference projects applied one-click to a project's or a
|
|
|
+goal's link set (stacks/stack_projects tables, /stacks pages, the apply
|
|
|
+actions with exact-duplicate skipping)."""
|
|
|
+from harness import * # noqa: F401,F403 — the shared wire helpers
|
|
|
+
|
|
|
+
|
|
|
+def run(ctx):
|
|
|
+ auth, fake, fake_id, fake_token = ctx.auth, ctx.fake, ctx.fake_id, ctx.fake_token
|
|
|
+
|
|
|
+ def create_goal(fields):
|
|
|
+ act(auth, slot_public(auth, "/goals", "goals")["create"], fields)
|
|
|
+ rows = [g for g in slot_public(auth, "/goals", "goals")["goals"]
|
|
|
+ if g["title"] == fields["title"]]
|
|
|
+ return int(rows[0]["id"])
|
|
|
+
|
|
|
+ def stack_state(stack_id):
|
|
|
+ return slot_public(auth, "/stacks/%d" % stack_id, "stack")
|
|
|
+
|
|
|
+ def link_rows(goal_id):
|
|
|
+ conn = db()
|
|
|
+ try:
|
|
|
+ return conn.execute(
|
|
|
+ "select gl.project_id, gl.relationship from goal_links gl"
|
|
|
+ " where gl.goal_id = ? order by gl.id", (goal_id,)).fetchall()
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ def project_link_rows(parent_id):
|
|
|
+ conn = db()
|
|
|
+ try:
|
|
|
+ return conn.execute(
|
|
|
+ "select linked_project_id, relationship from project_links"
|
|
|
+ " where parent_project_id = ? order by id", (parent_id,)).fetchall()
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ print("== goal 8 task 2 A: goal links pre-fill from the project ==")
|
|
|
+ # Two satellites around the demo project: one co-developed, one
|
|
|
+ # reference. The goal created on demo must inherit both.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-codev', 'https://example.com/codev.git', 'main',"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-ref', 'https://example.com/ref.git', 'main',"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ conn.commit()
|
|
|
+ codev_id = int(conn.execute(
|
|
|
+ "select id from projects where name = 'stacks-codev'").fetchone()[0])
|
|
|
+ ref_id = int(conn.execute(
|
|
|
+ "select id from projects where name = 'stacks-ref'").fetchone()[0])
|
|
|
+ # A third project linked to the project but AFTER goal creation —
|
|
|
+ # it must NOT propagate (the pre-fill is a one-time snapshot).
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-late', 'https://example.com/late.git', 'main',"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ conn.commit()
|
|
|
+ late_id = int(conn.execute(
|
|
|
+ "select id from projects where name = 'stacks-late'").fetchone()[0])
|
|
|
+ conn.execute("insert into project_links (parent_project_id, linked_project_id,"
|
|
|
+ " relationship) values (1, ?, 'co-developed')", (codev_id,))
|
|
|
+ conn.execute("insert into project_links (parent_project_id, linked_project_id,"
|
|
|
+ " relationship) values (1, ?, 'reference')", (ref_id,))
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ goal = create_goal({"project_id": "1", "title": "Prefill probe",
|
|
|
+ "brief": "Check the link pre-fill.",
|
|
|
+ "branch_name": "goal/prefill", "priority": "3"})
|
|
|
+ check(set(link_rows(goal)) == {(codev_id, "co-developed"), (ref_id, "reference")},
|
|
|
+ "a goal created on a linked project pre-fills goal_links from"
|
|
|
+ " project_links (same targets, same relationships)")
|
|
|
+ state = goal_state(goal)
|
|
|
+ check(sorted(link["name"] for link in state["links"]) == ["stacks-codev", "stacks-ref"],
|
|
|
+ "the goal page lists the pre-filled links")
|
|
|
+ check(state["links"][0].get("remove") is not None,
|
|
|
+ "the pre-filled links are prunable like manual ones")
|
|
|
+
|
|
|
+ # The late link must not propagate into the existing goal.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into project_links (parent_project_id, linked_project_id,"
|
|
|
+ " relationship) values (1, ?, 'reference')", (late_id,))
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ check(all(row[0] != late_id for row in link_rows(goal)),
|
|
|
+ "a project link added AFTER creation never propagates (snapshot)")
|
|
|
+
|
|
|
+ # A NEW goal created after the late link lands DOES carry it.
|
|
|
+ goal2 = create_goal({"project_id": "1", "title": "Prefill probe 2",
|
|
|
+ "brief": "Snapshot at creation time.",
|
|
|
+ "branch_name": "goal/prefill2", "priority": "3"})
|
|
|
+ check(set(link_rows(goal2)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference"),
|
|
|
+ (late_id, "reference")},
|
|
|
+ "a goal created after a new project link snapshots that link too")
|
|
|
+
|
|
|
+ # Self-links in the project graph are never copied (a project link
|
|
|
+ # to the goal's own primary is redundant — it is implicit).
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-self', 'https://example.com/self.git', 'main',"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ goal3 = create_goal({"project_id": "1", "title": "No self links",
|
|
|
+ "brief": "The primary is implicit.",
|
|
|
+ "branch_name": "goal/prefill3", "priority": "3"})
|
|
|
+ check(all(row[0] != 1 for row in link_rows(goal3)),
|
|
|
+ "the primary project is never duplicated as an explicit link")
|
|
|
+
|
|
|
+ print("== the pre-filled links ride the assignment payload ==")
|
|
|
+ # Removing one pre-filled link must remove it from the payload —
|
|
|
+ # goal_links stays the single source of truth after the snapshot.
|
|
|
+ state = goal_state(goal)
|
|
|
+ codev_link = [link for link in state["links"] if link["name"] == "stacks-codev"][0]
|
|
|
+ act(auth, codev_link["remove"], {"link_id": str(codev_link["id"])})
|
|
|
+ check(set(link_rows(goal)) == {(ref_id, "reference")},
|
|
|
+ "removing a pre-filled goal link removes the row")
|
|
|
+ fake.goal_script = [("suspend",)]
|
|
|
+ act(auth, goal_state(goal)["start"], {})
|
|
|
+ check(wait_for(lambda: next(
|
|
|
+ (True for row in db().execute(
|
|
|
+ "select id from tasks where goal_id = ? and kind = 'goal_run'",
|
|
|
+ (goal,)).fetchall()), False) or False,
|
|
|
+ timeout=30, what="goal run task creation") is not None,
|
|
|
+ "start creates the goal-run task")
|
|
|
+ conn = db()
|
|
|
+ run_task = int(conn.execute(
|
|
|
+ "select id from tasks where goal_id = ? and kind = 'goal_run'",
|
|
|
+ (goal,)).fetchone()[0])
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ def assigned_payload():
|
|
|
+ for payload in reversed(fake.goal_assignments):
|
|
|
+ if (payload.get("task") or {}).get("id") == run_task:
|
|
|
+ return payload
|
|
|
+ return None
|
|
|
+
|
|
|
+ check(wait_for(lambda: assigned_payload() is not None or False,
|
|
|
+ timeout=60, what="goal run assignment") is not None,
|
|
|
+ "the fake saddle captures the goal-run assignment")
|
|
|
+ payload = assigned_payload()
|
|
|
+ codev_names = [p["name"] for p in payload["task"]["co_developed"]]
|
|
|
+ ref_names = [p["name"] for p in payload["task"]["reference"]]
|
|
|
+ check("demo" in codev_names and "stacks-codev" not in codev_names,
|
|
|
+ "the assignment payload's co-developed carries the primary and the"
|
|
|
+ " surviving pre-filled link (the removed one is gone)")
|
|
|
+ check(ref_names == ["stacks-ref"],
|
|
|
+ "the assignment payload's reference set comes from the goal's links")
|
|
|
+
|
|
|
+ print("== stacks: CRUD ==")
|
|
|
+ stacks_state = slot_public(auth, "/stacks", "stacks")
|
|
|
+ check(stacks_state.get("create") is not None and stacks_state["stacks"] == [],
|
|
|
+ "the stacks page starts empty with a create action")
|
|
|
+ act(auth, stacks_state["create"],
|
|
|
+ {"name": "web-stack-refs", "summary": "The usual reference collection"})
|
|
|
+ stacks = slot_public(auth, "/stacks", "stacks")["stacks"]
|
|
|
+ check(len(stacks) == 1 and stacks[0]["name"] == "web-stack-refs"
|
|
|
+ and int(stacks[0]["member_count"]) == 0,
|
|
|
+ "a stack lands in the list with its (empty) member count")
|
|
|
+ stack_id = int(stacks[0]["id"])
|
|
|
+
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ check(detail["exists"] and detail["name"] == "web-stack-refs"
|
|
|
+ and detail["members"] == [] and detail.get("update") is not None
|
|
|
+ and detail.get("add_member") is not None and detail.get("delete") is not None,
|
|
|
+ "the stack page exposes edit, add-member and delete actions")
|
|
|
+ check(any(option["name"] == "demo" for option in detail["options"]),
|
|
|
+ "every project is offerable as a stack member")
|
|
|
+
|
|
|
+ # Duplicate name refused.
|
|
|
+ dup = act(auth, slot_public(auth, "/stacks", "stacks")["create"],
|
|
|
+ {"name": "web-stack-refs"})
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error"
|
|
|
+ for d in dup),
|
|
|
+ "a duplicate stack name refuses")
|
|
|
+
|
|
|
+ # Rename + summary clear.
|
|
|
+ act(auth, detail["update"], {"name": "web-stack-refs-2", "summary": ""})
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ check(detail["name"] == "web-stack-refs-2" and detail["summary"] == "",
|
|
|
+ "a stack renames and its summary clears")
|
|
|
+
|
|
|
+ print("== stacks: members ==")
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ act(auth, detail["add_member"],
|
|
|
+ {"project_id": str(codev_id), "relationship": "co-developed"})
|
|
|
+ act(auth, stack_state(stack_id)["add_member"],
|
|
|
+ {"project_id": str(ref_id), "relationship": "reference"})
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ member_names = [(m["name"], m["relationship"]) for m in detail["members"]]
|
|
|
+ check(member_names == [("stacks-codev", "co-developed"),
|
|
|
+ ("stacks-ref", "reference")],
|
|
|
+ "members land with their relationships")
|
|
|
+ dup = act(auth, stack_state(stack_id)["add_member"],
|
|
|
+ {"project_id": str(ref_id), "relationship": "reference"})
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error"
|
|
|
+ and "already" in (d.get("message") or "") for d in dup),
|
|
|
+ "adding the same project twice refuses")
|
|
|
+ bad = act(auth, stack_state(stack_id)["add_member"],
|
|
|
+ {"project_id": str(late_id), "relationship": "sponsor"})
|
|
|
+ check(any(d.get("type") == "notify" and d.get("kind") == "error"
|
|
|
+ for d in bad),
|
|
|
+ "an unknown relationship refuses")
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ check(len(detail["members"]) == 2,
|
|
|
+ "the refused additions left the stack untouched")
|
|
|
+
|
|
|
+ print("== stacks: apply to a project ==")
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-target', 'https://example.com/target.git', 'main',"
|
|
|
+ " strftime('%s','now'))")
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ target_id = int(db().execute(
|
|
|
+ "select id from projects where name = 'stacks-target'"
|
|
|
+ ).fetchone()[0])
|
|
|
+
|
|
|
+ project_state = slot_public(auth, "/projects/%d" % target_id, "project")
|
|
|
+ check(project_state.get("apply_stack") is not None
|
|
|
+ and any(opt["name"] == "web-stack-refs-2" for opt in project_state["stack_options"]),
|
|
|
+ "the project page offers Apply stack with the stacks as options")
|
|
|
+ result = act(auth, project_state["apply_stack"], {"stack_id": str(stack_id)})
|
|
|
+ check("2 link" in (action_notify(result) or ""),
|
|
|
+ "applying reports how many links landed")
|
|
|
+ check(set(project_link_rows(target_id)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "the stack's members became project_links rows")
|
|
|
+
|
|
|
+ # Re-apply: exact duplicates skipped, nothing duplicated.
|
|
|
+ result = act(auth, slot_public(auth, "/projects/%d" % target_id,
|
|
|
+ "project")["apply_stack"],
|
|
|
+ {"stack_id": str(stack_id)})
|
|
|
+ check("nothing new" in (action_notify(result) or ""),
|
|
|
+ "re-applying skips exact duplicates")
|
|
|
+ check(set(project_link_rows(target_id)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "no duplicate project_links rows appear")
|
|
|
+
|
|
|
+ # A pre-existing DIFFERENT relationship on the same target is not a
|
|
|
+ # duplicate: applying adds the stack's own relationship alongside.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into project_links (parent_project_id, linked_project_id,"
|
|
|
+ " relationship) values (?, ?, 'co-developed')",
|
|
|
+ (target_id, ref_id))
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ act(auth, slot_public(auth, "/projects/%d" % target_id, "project")["apply_stack"],
|
|
|
+ {"stack_id": str(stack_id)})
|
|
|
+ check((ref_id, "reference") in project_link_rows(target_id)
|
|
|
+ and project_link_rows(target_id).count((ref_id, "co-developed")) == 1,
|
|
|
+ "a different relationship on the same target coexists (exact pairs skip)")
|
|
|
+
|
|
|
+ print("== stacks: apply to a goal ==")
|
|
|
+ # A goal on a LINK-FREE host project — the pre-fill seeds nothing,
|
|
|
+ # so the stack's members are exactly what applying must land.
|
|
|
+ conn = db()
|
|
|
+ conn.execute("insert into projects (name, git_url, default_branch, created_at)"
|
|
|
+ " values ('stacks-goalhost', 'https://example.com/goalhost.git',"
|
|
|
+ " 'main', strftime('%s','now'))")
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ host_id = int(db().execute(
|
|
|
+ "select id from projects where name = 'stacks-goalhost'").fetchone()[0])
|
|
|
+ goal4 = create_goal({"project_id": str(host_id), "title": "Stack target",
|
|
|
+ "brief": "Apply a stack here.",
|
|
|
+ "branch_name": "goal/stacktarget", "priority": "3"})
|
|
|
+ check(link_rows(goal4) == [],
|
|
|
+ "a goal on a link-free project pre-fills nothing")
|
|
|
+ gstate = goal_state(goal4)
|
|
|
+ check(gstate.get("apply_stack") is not None
|
|
|
+ and any(opt["name"] == "web-stack-refs-2" for opt in gstate["stack_options"]),
|
|
|
+ "the goal page offers Apply stack with the stacks as options")
|
|
|
+ result = act(auth, gstate["apply_stack"], {"stack_id": str(stack_id)})
|
|
|
+ check("2 link" in (action_notify(result) or ""),
|
|
|
+ "applying to the goal reports the links landed")
|
|
|
+ check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "the stack's members became goal_links rows")
|
|
|
+
|
|
|
+ # Re-apply: idempotent.
|
|
|
+ result = act(auth, goal_state(goal4)["apply_stack"], {"stack_id": str(stack_id)})
|
|
|
+ check("nothing new" in (action_notify(result) or ""),
|
|
|
+ "re-applying to the goal skips exact duplicates")
|
|
|
+ check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "no duplicate goal_links rows appear")
|
|
|
+
|
|
|
+ # Removing a stack member then re-applying adds only the newcomer.
|
|
|
+ detail = stack_state(stack_id)
|
|
|
+ ref_view = [m for m in detail["members"]
|
|
|
+ if int(m["project_id"]) == ref_id][0]
|
|
|
+ act(auth, ref_view["remove"], {})
|
|
|
+ check(len(stack_state(stack_id)["members"]) == 1,
|
|
|
+ "removing a member shrinks the stack")
|
|
|
+ act(auth, goal_state(goal4)["apply_stack"], {"stack_id": str(stack_id)})
|
|
|
+ check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "removing a stack member never unlinks what was already applied")
|
|
|
+
|
|
|
+ print("== stacks: delete ==")
|
|
|
+ conn = db()
|
|
|
+ act(auth, slot_public(auth, "/stacks/%d" % stack_id, "stack")["delete"], {})
|
|
|
+ conn.close()
|
|
|
+ check(slot_public(auth, "/stacks", "stacks")["stacks"] == [],
|
|
|
+ "deleting a stack empties the list")
|
|
|
+ conn = db()
|
|
|
+ leftover = conn.execute("select count(*) from stack_projects").fetchone()[0]
|
|
|
+ conn.close()
|
|
|
+ check(leftover == 0, "the stack's member rows die with it")
|
|
|
+ check(set(link_rows(goal4)) == {(codev_id, "co-developed"),
|
|
|
+ (ref_id, "reference")},
|
|
|
+ "deleting a stack keeps every link it already applied")
|