"""Tests for import_org_backlog. The importer splits into a pure org parser + mapper (no I/O) and a thin Linear GraphQL client. The parser pulls top-level TODO/DOING tasks out of the "Pearl Open Work" section; the mapper turns each into an issueCreate input (priority, state, labels); the client creates missing labels and the issues, skipping a task whose title already exists so a re-run is safe. """ import os import sys import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import import_org_backlog as imp SAMPLE = """\ * Pearl Priority Scheme - =[#A]= urgent (not a task) * Pearl Open Work ** DOING [#A] Personal Account Dogfooding Meta task, skipped. *** TODO a child child body ** TODO [#B] Fix the thing :bug:discuss: :PROPERTIES: :LAST_REVIEWED: 2026-06-06 :END: The body explaining the bug. More body. *** 2026-06-06 dated log log entry, excluded from the description ** TODO [#C] A docs task :docs:quick: Body here. ** TODO [#B] Manual testing and validation :test: checklist umbrella, skipped *** TODO a verify verify body ** TODO [#D] Someday feature :feature: Someday body. * Pearl Resolved ** DONE [#A] old finished thing done body, excluded (resolved section) """ # --- pure: parser ------------------------------------------------------------ def test_parse_pulls_open_work_tasks_skipping_meta_and_resolved(): tasks = imp.parse_open_work_tasks(SAMPLE) assert [t["title"] for t in tasks] == [ "Fix the thing", "A docs task", "Someday feature"] def test_parse_captures_priority_and_tags(): tasks = {t["title"]: t for t in imp.parse_open_work_tasks(SAMPLE)} assert tasks["Fix the thing"]["priority"] == "B" assert tasks["Fix the thing"]["tags"] == ["bug", "discuss"] assert tasks["Someday feature"]["priority"] == "D" assert tasks["A docs task"]["tags"] == ["docs", "quick"] def test_parse_body_excludes_drawer_and_subtrees(): tasks = {t["title"]: t for t in imp.parse_open_work_tasks(SAMPLE)} body = tasks["Fix the thing"]["body"] assert body == "The body explaining the bug.\nMore body." assert "LAST_REVIEWED" not in body # drawer dropped assert "dated log" not in body # sub-entry dropped def test_parse_handles_a_colon_laden_title(): text = ("* Pearl Open Work\n\n" "** TODO [#C] Reconsider how =#+title:= displays :bug:quick:\nBody.\n") t = imp.parse_open_work_tasks(text)[0] assert t["title"] == "Reconsider how =#+title:= displays" assert t["tags"] == ["bug", "quick"] # --- pure: label set + mapping ----------------------------------------------- def test_label_names_needed_drops_the_four_tags(): tasks = imp.parse_open_work_tasks(SAMPLE) # bug/docs/quick/feature survive (capitalized); discuss is dropped assert imp.label_names_needed(tasks) == {"Bug", "Docs", "Quick", "Feature"} def test_task_to_issue_maps_priority_state_and_labels(): label_ids = {"bug": "L-bug", "feature": "L-feat", "docs": "L-docs", "quick": "L-quick"} states = {"Backlog": "S-back", "Icebox": "S-ice"} by = {t["title"]: t for t in imp.parse_open_work_tasks(SAMPLE)} bug = imp.task_to_issue(by["Fix the thing"], states, label_ids) assert bug["priority"] == 2 # [#B] assert bug["stateId"] == "S-back" assert bug["labelIds"] == ["L-bug"] # discuss dropped assert "explaining the bug" in bug["description"] someday = imp.task_to_issue(by["Someday feature"], states, label_ids) assert someday["priority"] == 4 # [#D] assert someday["stateId"] == "S-ice" # [#D] -> Icebox # --- boundary: a fake GraphQL transport -------------------------------------- class FakeTransport: 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_import_creates_missing_labels_and_new_issues_only(): created_labels = {"i": 0} def make_label(_v): created_labels["i"] += 1 return {"data": {"issueLabelCreate": {"success": True, "issueLabel": {"id": f"NL{created_labels['i']}"}}}} fake = FakeTransport({ "Teams": {"data": {"teams": {"nodes": [ {"id": "t1", "labels": {"nodes": [{"id": "L-bug", "name": "Bug"}, {"id": "L-feat", "name": "Feature"}]}, "states": {"nodes": [{"id": "S-back", "name": "Backlog"}, {"id": "S-ice", "name": "Icebox"}]}}]}}}, "Projects": {"data": {"projects": {"nodes": [{"id": "p1", "name": "Pearl"}]}}}, "ProjectIssues": {"data": {"project": {"issues": {"nodes": [{"title": "A docs task"}]}}}}, "LabelCreate": make_label, "IssueCreate": {"data": {"issueCreate": {"success": True, "issue": {"id": "i1"}}}}, }) summary = imp.import_backlog(imp.LinearClient("key", transport=fake), SAMPLE) ops = fake.ops() # Bug + Feature already exist; only Docs + Quick get created (discuss dropped) assert ops.count("LabelCreate") == 2 # "A docs task" already exists -> skipped; the other two created assert ops.count("IssueCreate") == 2 assert set(summary["created"]) == {"Fix the thing", "Someday feature"} assert summary["skipped"] == ["A docs task"] def test_client_raises_on_graphql_errors(): fake = FakeTransport({"Teams": {"errors": [{"message": "nope"}]}}) with pytest.raises(imp.LinearError, match="nope"): imp.LinearClient("key", transport=fake).execute("query Teams { teams { nodes { id } } }", {})