gabriel / musehub public
test_publish_issue.py python
149 lines 6.5 KB
Raw
sha256:411992ae51c0ea420190eb8cfc86e6f04de4d0af5f11238a7542e04cf80afc18 fix(publish_issue): hard-block publishing a body that leaks… Sonnet 5 patch 21 hours ago
1 """Tests for scripts/dev/publish_issue.py — one-way staging→production ticket publish (#186).
2
3 Uses an in-memory fake HubClient so these tests never shell out to a real
4 `muse hub` CLI or hit a network. Real wiring (MuseHubCliClient) is exercised
5 manually against staging/production, not here.
6 """
7 import sys
8 from pathlib import Path
9
10 import pytest
11
12 sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts" / "dev"))
13 from publish_issue import StagingUrlLeakError, build_mirror_marker, publish_issue # noqa: E402
14
15 STAGING_HUB = "https://staging.musehub.ai"
16 PRODUCTION_HUB = "https://musehub.ai"
17
18
19 class FakeHubClient:
20 """In-memory stand-in for the real `muse hub issue` CLI calls."""
21
22 def __init__(self) -> None:
23 self._issues: dict[tuple[str, int], dict] = {}
24 self._next_number: dict[str, int] = {}
25
26 def seed(self, hub: str, number: int, *, title: str, body: str, labels: list[str]) -> None:
27 self._issues[(hub, number)] = {
28 "number": number, "title": title, "body": body, "labels": labels,
29 "url": f"{hub}/gabriel/musehub/issues/{number}",
30 }
31 self._next_number[hub] = max(self._next_number.get(hub, 0), number) + 1
32
33 def read_issue(self, number: int, hub: str) -> dict:
34 return dict(self._issues[(hub, number)])
35
36 def list_issues(self, hub: str, state: str = "all") -> list[dict]:
37 return [dict(v) for (h, _), v in self._issues.items() if h == hub]
38
39 def create_issue(self, hub: str, *, title: str, body: str, labels: list[str]) -> dict:
40 number = self._next_number.get(hub, 1)
41 self._next_number[hub] = number + 1
42 issue = {
43 "number": number, "title": title, "body": body, "labels": labels,
44 "url": f"{hub}/gabriel/musehub/issues/{number}",
45 }
46 self._issues[(hub, number)] = issue
47 return dict(issue)
48
49 def update_issue(self, hub: str, number: int, *, title: str, body: str) -> dict:
50 issue = self._issues[(hub, number)]
51 issue["title"] = title
52 issue["body"] = body
53 return dict(issue)
54
55 def set_labels(self, hub: str, number: int, labels: list[str]) -> None:
56 self._issues[(hub, number)]["labels"] = labels
57
58
59 @pytest.fixture
60 def client() -> FakeHubClient:
61 return FakeHubClient()
62
63
64 class TestPublishIssue:
65 def test_fresh_publish_creates_exactly_one_production_issue(self, client: FakeHubClient) -> None:
66 client.seed(STAGING_HUB, 42, title="feat: thing", body="Do the thing.", labels=["enhancement"])
67
68 result = publish_issue(client, 42, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
69
70 assert result["action"] == "created"
71 prod_issues = client.list_issues(PRODUCTION_HUB)
72 assert len(prod_issues) == 1
73 assert prod_issues[0]["title"] == "feat: thing"
74 assert "Do the thing." in prod_issues[0]["body"]
75 assert build_mirror_marker(42) in prod_issues[0]["body"]
76 assert prod_issues[0]["labels"] == ["enhancement"]
77
78 def test_republishing_same_staging_issue_updates_rather_than_duplicates(self, client: FakeHubClient) -> None:
79 client.seed(STAGING_HUB, 42, title="feat: thing", body="Do the thing.", labels=["enhancement"])
80 publish_issue(client, 42, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
81
82 # Staging ticket gets edited after the first publish.
83 client.seed(STAGING_HUB, 42, title="feat: thing (updated)", body="Do the thing, better.", labels=["enhancement", "bug"])
84 result = publish_issue(client, 42, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
85
86 assert result["action"] == "updated"
87 prod_issues = client.list_issues(PRODUCTION_HUB)
88 assert len(prod_issues) == 1, "must not create a second production issue for the same staging ticket"
89 assert prod_issues[0]["title"] == "feat: thing (updated)"
90 assert "better" in prod_issues[0]["body"]
91 assert prod_issues[0]["labels"] == ["enhancement", "bug"]
92
93 def test_different_staging_issues_produce_separate_production_issues(self, client: FakeHubClient) -> None:
94 client.seed(STAGING_HUB, 1, title="A", body="body A", labels=[])
95 client.seed(STAGING_HUB, 2, title="B", body="body B", labels=[])
96
97 publish_issue(client, 1, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
98 publish_issue(client, 2, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
99
100 assert len(client.list_issues(PRODUCTION_HUB)) == 2
101
102 def test_staging_url_in_body_raises_and_blocks_publish(self, client: FakeHubClient) -> None:
103 """A hard block, not a warning — a warning is exactly the thing that
104 gets skimmed past at the moment someone's about to post publicly."""
105 client.seed(
106 STAGING_HUB, 7, title="bug: X",
107 body="See https://staging.musehub.ai/gabriel/musehub/issues/6 for context.",
108 labels=[],
109 )
110
111 with pytest.raises(StagingUrlLeakError, match="staging.musehub.ai"):
112 publish_issue(client, 7, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
113
114 assert client.list_issues(PRODUCTION_HUB) == [], (
115 "must not publish anything when the body leaks a staging URL"
116 )
117
118 def test_clean_body_has_no_warnings(self, client: FakeHubClient) -> None:
119 client.seed(STAGING_HUB, 8, title="bug: Y", body="No staging links here.", labels=[])
120
121 result = publish_issue(client, 8, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
122
123 assert result["warnings"] == []
124
125 def test_bare_issue_reference_in_body_triggers_warning(self, client: FakeHubClient) -> None:
126 client.seed(
127 STAGING_HUB, 9, title="bug: Z",
128 body="Related to #182 and #183, see also the discussion there.",
129 labels=[],
130 )
131
132 result = publish_issue(client, 9, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
133
134 assert any("#182" in w and "#183" in w for w in result["warnings"])
135
136 def test_markdown_heading_does_not_trigger_bare_reference_warning(self, client: FakeHubClient) -> None:
137 client.seed(
138 STAGING_HUB, 10, title="bug: W",
139 body="## Summary\n\nNo real issue references here, just headings.",
140 labels=[],
141 )
142
143 result = publish_issue(client, 10, staging_hub=STAGING_HUB, production_hub=PRODUCTION_HUB)
144
145 assert result["warnings"] == []
146
147 def test_mirror_marker_is_stable_per_staging_number(self) -> None:
148 assert build_mirror_marker(42) == build_mirror_marker(42)
149 assert build_mirror_marker(42) != build_mirror_marker(43)
File History 1 commit
sha256:411992ae51c0ea420190eb8cfc86e6f04de4d0af5f11238a7542e04cf80afc18 fix(publish_issue): hard-block publishing a body that leaks… Sonnet 5 patch 21 hours ago