aboutsummaryrefslogtreecommitdiff
path: root/scripts/tests/test_seed_pearl_workspace.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/tests/test_seed_pearl_workspace.py')
-rw-r--r--scripts/tests/test_seed_pearl_workspace.py217
1 files changed, 217 insertions, 0 deletions
diff --git a/scripts/tests/test_seed_pearl_workspace.py b/scripts/tests/test_seed_pearl_workspace.py
new file mode 100644
index 0000000..2e7cb4a
--- /dev/null
+++ b/scripts/tests/test_seed_pearl_workspace.py
@@ -0,0 +1,217 @@
+"""Tests for seed_pearl_workspace.
+
+The seeding logic splits into a pure planner (given a workspace snapshot, decide
+what to create) and a thin Linear GraphQL client (the only network boundary).
+The planner carries the behavior, so it gets exhaustive pure tests; the client
+and the seed() orchestration get fake-transport tests that assert the right
+mutations go out and that a second run is a no-op (idempotence).
+"""
+
+import os
+import sys
+
+import pytest
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import seed_pearl_workspace as seed
+
+
+# --- pure: target definitions -------------------------------------------------
+
+def test_target_states_are_the_six_agile_columns_in_order():
+ names = [s["name"] for s in seed.TARGET_STATES]
+ assert names == ["icebox", "triage queue", "backlog",
+ "in-progress", "done", "cancelled"]
+
+
+def test_early_columns_share_backlog_type_with_ascending_positions():
+ # icebox / triage queue / backlog all sort by position within one type, so
+ # pearl's (type-rank, position) grouping reproduces the board column order.
+ early = [s for s in seed.TARGET_STATES
+ if s["name"] in ("icebox", "triage queue", "backlog")]
+ assert all(s["type"] == "backlog" for s in early)
+ positions = [s["position"] for s in early]
+ assert positions == sorted(positions)
+ assert len({s["position"] for s in seed.TARGET_STATES}) == 6 # all distinct
+
+
+def test_later_columns_map_to_distinct_linear_types():
+ by_name = {s["name"]: s["type"] for s in seed.TARGET_STATES}
+ assert by_name["in-progress"] == "started"
+ assert by_name["done"] == "completed"
+ assert by_name["cancelled"] == "canceled"
+
+
+def test_every_state_has_a_color():
+ assert all(s.get("color") for s in seed.TARGET_STATES)
+
+
+def test_view_specs_names_and_project_scope():
+ specs = seed.desired_views("proj-1")
+ assert [v["name"] for v in specs] == [
+ "Pearl Open Issues", "Pearl Icebox", "Pearl Inbox"]
+ for v in specs:
+ assert v["filterData"]["project"]["id"]["eq"] == "proj-1"
+
+
+def test_open_view_excludes_completed_and_cancelled():
+ open_view = next(v for v in seed.desired_views("p")
+ if v["name"] == "Pearl Open Issues")
+ nin = open_view["filterData"]["state"]["type"]["nin"]
+ assert "completed" in nin and "canceled" in nin
+
+
+def test_icebox_and_inbox_views_filter_to_their_state():
+ views = {v["name"]: v for v in seed.desired_views("p")}
+ assert views["Pearl Icebox"]["filterData"]["state"]["name"]["eq"] == "icebox"
+ assert views["Pearl Inbox"]["filterData"]["state"]["name"]["eq"] == "triage queue"
+
+
+# --- pure: the planner --------------------------------------------------------
+
+EMPTY = {"team": None, "states": [], "project": None, "views": []}
+
+
+def test_plan_on_empty_workspace_creates_everything():
+ plan = seed.plan_seed(EMPTY)
+ assert plan.create_team is True
+ assert [s["name"] for s in plan.create_states] == [s["name"] for s in seed.TARGET_STATES]
+ assert plan.create_project is True
+ assert [v["name"] for v in plan.create_views] == [
+ "Pearl Open Issues", "Pearl Icebox", "Pearl Inbox"]
+
+
+def test_plan_is_idempotent_when_everything_exists():
+ current = {
+ "team": {"id": "t1", "name": "Pearl"},
+ "states": [{"name": n} for n in
+ ["icebox", "triage queue", "backlog", "in-progress", "done", "cancelled"]],
+ "project": {"id": "p1", "name": "Pearl"},
+ "views": [{"name": n} for n in
+ ["Pearl Open Issues", "Pearl Icebox", "Pearl Inbox"]],
+ }
+ plan = seed.plan_seed(current)
+ assert plan.create_team is False
+ assert plan.create_states == []
+ assert plan.create_project is False
+ assert plan.create_views == []
+
+
+def test_plan_creates_only_missing_states_and_views():
+ current = {
+ "team": {"id": "t1", "name": "Pearl"},
+ "states": [{"name": "backlog"}, {"name": "done"}],
+ "project": {"id": "p1", "name": "Pearl"},
+ "views": [{"name": "Pearl Icebox"}],
+ }
+ plan = seed.plan_seed(current)
+ assert plan.create_team is False
+ assert plan.create_project is False
+ assert [s["name"] for s in plan.create_states] == [
+ "icebox", "triage queue", "in-progress", "cancelled"]
+ assert [v["name"] for v in plan.create_views] == [
+ "Pearl Open Issues", "Pearl Inbox"]
+
+
+# --- boundary: a fake GraphQL transport ---------------------------------------
+
+class FakeTransport:
+ """Records GraphQL calls and replies from a scripted op->response map."""
+
+ def __init__(self, responses):
+ self.responses = responses
+ self.calls = []
+
+ def __call__(self, query, variables):
+ op = next(name for name in self.responses if name in query)
+ self.calls.append((op, variables))
+ resp = self.responses[op]
+ return resp(variables) if callable(resp) else resp
+
+ def ops(self):
+ return [op for op, _ in self.calls]
+
+
+def test_seed_on_empty_workspace_issues_creates_in_order():
+ state_n = {"i": 0}
+
+ def make_state(_v):
+ state_n["i"] += 1
+ return {"data": {"workflowStateCreate":
+ {"success": True, "workflowState": {"id": f"s{state_n['i']}"}}}}
+
+ fake = FakeTransport({
+ # snapshot queries: empty workspace
+ "teams": {"data": {"teams": {"nodes": []}}},
+ "teamCreate": {"data": {"teamCreate": {"success": True,
+ "team": {"id": "t1", "key": "PEARL", "name": "Pearl",
+ "states": {"nodes": []}}}}},
+ "workflowStateCreate": make_state,
+ "projects": {"data": {"projects": {"nodes": []}}},
+ "projectCreate": {"data": {"projectCreate":
+ {"success": True, "project": {"id": "p1", "name": "Pearl"}}}},
+ "customViews": {"data": {"customViews": {"nodes": []}}},
+ "customViewCreate": {"data": {"customViewCreate":
+ {"success": True, "customView": {"id": "v1"}}}},
+ })
+ client = seed.LinearClient("key", transport=fake)
+ summary = seed.seed(client)
+
+ assert fake.ops().count("teamCreate") == 1
+ assert fake.ops().count("workflowStateCreate") == 6
+ assert fake.ops().count("projectCreate") == 1
+ assert fake.ops().count("customViewCreate") == 3
+ assert summary["team"] == "t1"
+ assert summary["project"] == "p1"
+
+
+def test_seed_is_idempotent_against_a_populated_workspace():
+ populated = {
+ "teams": {"data": {"teams": {"nodes": [
+ {"id": "t1", "key": "PEARL", "name": "Pearl",
+ "states": {"nodes": [{"id": f"s{i}", "name": n}
+ for i, n in enumerate(
+ ["icebox", "triage queue", "backlog",
+ "in-progress", "done", "cancelled"])]}}]}}},
+ "projects": {"data": {"projects": {"nodes": [{"id": "p1", "name": "Pearl"}]}}},
+ "customViews": {"data": {"customViews": {"nodes": [
+ {"id": "v1", "name": "Pearl Open Issues"},
+ {"id": "v2", "name": "Pearl Icebox"},
+ {"id": "v3", "name": "Pearl Inbox"}]}}},
+ }
+ fake = FakeTransport(populated)
+ client = seed.LinearClient("key", transport=fake)
+ seed.seed(client)
+ for create_op in ("teamCreate", "workflowStateCreate",
+ "projectCreate", "customViewCreate"):
+ assert create_op not in fake.ops()
+
+
+def test_seed_reports_leftover_default_states_without_archiving():
+ # An existing team carrying the six columns plus Linear's leftover defaults:
+ # seed creates nothing and surfaces the defaults for manual tidy-up, and
+ # never issues an archive.
+ populated = {
+ "teams": {"data": {"teams": {"nodes": [
+ {"id": "t1", "key": "PEARL", "name": "Pearl",
+ "states": {"nodes": [{"id": f"s{i}", "name": n} for i, n in enumerate(
+ ["icebox", "triage queue", "backlog", "in-progress", "done",
+ "cancelled", "Todo", "In Review"])]}}]}}},
+ "projects": {"data": {"projects": {"nodes": [{"id": "p1", "name": "Pearl"}]}}},
+ "customViews": {"data": {"customViews": {"nodes": [
+ {"id": "v1", "name": "Pearl Open Issues"},
+ {"id": "v2", "name": "Pearl Icebox"},
+ {"id": "v3", "name": "Pearl Inbox"}]}}},
+ }
+ fake = FakeTransport(populated)
+ summary = seed.seed(seed.LinearClient("key", transport=fake))
+ assert summary["leftover_states"] == ["Todo", "In Review"]
+ assert "workflowStateArchive" not in fake.ops()
+
+
+def test_client_raises_on_graphql_errors():
+ fake = FakeTransport({"teams": {"errors": [{"message": "bad key"}]}})
+ client = seed.LinearClient("key", transport=fake)
+ with pytest.raises(seed.LinearError, match="bad key"):
+ client.execute("query { teams { nodes { id } } }", {})