diff --git a/skills/fix-plan/scripts/plane_sync.py b/skills/fix-plan/scripts/plane_sync.py index 95012dc8..3d52a437 100644 --- a/skills/fix-plan/scripts/plane_sync.py +++ b/skills/fix-plan/scripts/plane_sync.py @@ -35,8 +35,11 @@ # plane_create_comment.py, the in-repo precedent that already clears the WAF. UA = "Mozilla/5.0 (plane-backlog)" +# The identifier prefix is a Plane project identifier, which may contain digits +# (e.g. ES6KR-117). A letters-only prefix class silently matched nothing for such +# workspaces, so every sync reported "0 index lines" instead of failing loudly. INDEX_LINE_RE = re.compile( - r'^(?P\s*)-\s+\[(?P[^\]]*)\]\s+\[(?P[A-Z]+-\d+)\]\s+(?P.+?)\s+' + r'^(?P<indent>\s*)-\s+\[(?P<marker>[^\]]*)\]\s+\[(?P<ident>[A-Z][A-Z0-9]*-\d+)\]\s+(?P<title>.+?)\s+' r'(?:→|->)\s+Plane\s+\((?P<url>https://[^\s)]+)\)(?P<rest>.*)$' ) diff --git a/tests/test_plane_profile.py b/tests/test_plane_profile.py index 14bbeca4..86c857e5 100644 --- a/tests/test_plane_profile.py +++ b/tests/test_plane_profile.py @@ -202,6 +202,12 @@ def test_k3s_fallback_script_survives_quote_in_workspace_slug(monkeypatch): plane_create_issue = importlib.util.module_from_spec(spec) spec.loader.exec_module(plane_create_issue) + # create_via_k3s_fallback() returns before subprocess.run() when kubectl is + # absent from PATH. Without this stub the test asserts on ambient tooling + # rather than on the escaping behaviour it is about, and fails on any host + # (or git hook with a trimmed PATH) that has no kubectl installed. + monkeypatch.setattr(plane_create_issue.shutil, "which", lambda _cmd: "/usr/bin/kubectl") + captured_cmd = {} class _FakeCompletedProcess: diff --git a/tests/test_plane_script_defects.py b/tests/test_plane_script_defects.py index 7607ae2b..31934ae5 100644 --- a/tests/test_plane_script_defects.py +++ b/tests/test_plane_script_defects.py @@ -194,3 +194,29 @@ def test_k3s_fallback_skips_gracefully_without_kubectl(script_path, monkeypatch) assert res["success"] is False assert "kubectl" in res["reason"] assert not ran, "fallback must not shell out when kubectl is absent" + + +def test_index_line_re_matches_identifier_prefix_containing_digits(): + """A Plane project identifier may contain digits (e.g. ES6KR-117). + + INDEX_LINE_RE previously matched the prefix with `[A-Z]+`, so no index line + from such a workspace ever matched. plane_sync then reported "0 index lines" + and every marker stayed unsynced — a silent no-op rather than a loud error. + """ + mod = load_module(FIX_PLAN_SCRIPTS / "plane_sync.py", "plane_sync_ident") + url = ( + "https://plane.example.invalid/acme/projects/" + "4b4d8bfc-5e5d-495b-bd4c-301fe89e5bb0/issues/" + "016a702b-11aa-424d-834c-53a818e9de0c" + ) + + for ident in ("ES6KR-117", "INFRA-12", "A1-3"): + line = f"- [ ] [{ident}] some tracked item → Plane ({url})" + match = mod.INDEX_LINE_RE.match(line) + assert match, f"index line with identifier {ident} must match" + assert match.group("ident") == ident + + # The prefix must still start with a letter — a purely numeric prefix is not + # a Plane identifier and must not be absorbed. + numeric = f"- [ ] [123-4] not an identifier → Plane ({url})" + assert mod.INDEX_LINE_RE.match(numeric) is None