@pennyfarthing/core 7.8.1 → 7.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/package.json +2 -1
  2. package/pennyfarthing-dist/scripts/core/prime.sh +8 -0
  3. package/pennyfarthing_scripts/__init__.py +17 -0
  4. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  7. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  8. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  9. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  10. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  11. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  12. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  13. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  14. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  15. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  16. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  17. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  18. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  19. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  20. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  21. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  22. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  23. package/pennyfarthing_scripts/common/__init__.py +49 -0
  24. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  26. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  27. package/pennyfarthing_scripts/common/config.py +65 -0
  28. package/pennyfarthing_scripts/common/output.py +180 -0
  29. package/pennyfarthing_scripts/config.py +21 -0
  30. package/pennyfarthing_scripts/git/__init__.py +29 -0
  31. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  32. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  33. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  34. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  35. package/pennyfarthing_scripts/git/status_all.py +310 -0
  36. package/pennyfarthing_scripts/hooks.py +455 -0
  37. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  38. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  39. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  40. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  41. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  42. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  43. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  44. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  45. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  46. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  47. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  48. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  49. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  50. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  51. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  52. package/pennyfarthing_scripts/jira/claim.py +211 -0
  53. package/pennyfarthing_scripts/jira/cli.py +150 -0
  54. package/pennyfarthing_scripts/jira/client.py +613 -0
  55. package/pennyfarthing_scripts/jira/epic.py +176 -0
  56. package/pennyfarthing_scripts/jira/story.py +219 -0
  57. package/pennyfarthing_scripts/jira/sync.py +350 -0
  58. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  59. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  60. package/pennyfarthing_scripts/jira_sync.py +36 -0
  61. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  62. package/pennyfarthing_scripts/output.py +37 -0
  63. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  64. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  65. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  67. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  68. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  69. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  70. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  71. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  72. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  73. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  74. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  75. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  76. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  80. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  81. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  82. package/pennyfarthing_scripts/prime/cli.py +220 -0
  83. package/pennyfarthing_scripts/prime/loader.py +239 -0
  84. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  85. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  86. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  87. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  88. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  89. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  90. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  91. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  95. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  96. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  97. package/pennyfarthing_scripts/sprint/status.py +122 -0
  98. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  99. package/pennyfarthing_scripts/sprint/work.py +192 -0
  100. package/pennyfarthing_scripts/story/__init__.py +67 -0
  101. package/pennyfarthing_scripts/story/__main__.py +10 -0
  102. package/pennyfarthing_scripts/story/cli.py +105 -0
  103. package/pennyfarthing_scripts/story/create.py +167 -0
  104. package/pennyfarthing_scripts/story/size.py +113 -0
  105. package/pennyfarthing_scripts/story/template.py +151 -0
  106. package/pennyfarthing_scripts/swebench.py +216 -0
  107. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  108. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  109. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  110. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  111. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  112. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  113. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  114. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  115. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  116. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  117. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  118. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  119. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  120. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  121. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  122. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  123. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  124. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  125. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  126. package/pennyfarthing_scripts/workflow.py +183 -0
@@ -0,0 +1,334 @@
1
+ """Tests for jira/ library package.
2
+
3
+ Story 63-9: Reorganize pennyfarthing_scripts into fan-out CLI pattern.
4
+
5
+ These tests verify the jira/ package modules work correctly
6
+ after reorganization from flat modules.
7
+ """
8
+
9
+ from typing import Any
10
+ from unittest.mock import MagicMock, patch
11
+
12
+ import pytest
13
+
14
+
15
+ class TestJiraClient:
16
+ """Tests for jira/client.py JiraClient class."""
17
+
18
+ def test_jira_client_initialization(self) -> None:
19
+ """JiraClient should initialize with default or custom values."""
20
+ from pennyfarthing_scripts.jira.client import JiraClient
21
+
22
+ # Default initialization
23
+ client = JiraClient()
24
+ assert client.base_url is not None
25
+ assert client.user is not None
26
+
27
+ # Custom initialization
28
+ client = JiraClient(
29
+ base_url="https://custom.atlassian.net",
30
+ user="test@example.com",
31
+ token="test-token",
32
+ )
33
+ assert client.base_url == "https://custom.atlassian.net"
34
+ assert client.user == "test@example.com"
35
+ assert client.token == "test-token"
36
+
37
+ def test_get_auth_header_returns_basic_auth(self) -> None:
38
+ """_get_auth_header should return Basic auth header."""
39
+ from pennyfarthing_scripts.jira.client import JiraClient
40
+
41
+ client = JiraClient(
42
+ user="user@example.com",
43
+ token="my-token",
44
+ )
45
+ headers = client._get_auth_header()
46
+
47
+ assert "Authorization" in headers
48
+ assert headers["Authorization"].startswith("Basic ")
49
+
50
+ def test_get_auth_header_empty_without_token(self) -> None:
51
+ """_get_auth_header should return empty dict without token."""
52
+ from pennyfarthing_scripts.jira.client import JiraClient
53
+
54
+ client = JiraClient(token="")
55
+ headers = client._get_auth_header()
56
+
57
+ assert headers == {}
58
+
59
+ def test_get_headers_includes_accept(self) -> None:
60
+ """_get_headers should include Accept header."""
61
+ from pennyfarthing_scripts.jira.client import JiraClient
62
+
63
+ client = JiraClient(token="test")
64
+ headers = client._get_headers()
65
+
66
+ assert "Accept" in headers
67
+ assert headers["Accept"] == "application/json"
68
+
69
+ def test_get_headers_includes_content_type_when_requested(self) -> None:
70
+ """_get_headers should include Content-Type when content_type=True."""
71
+ from pennyfarthing_scripts.jira.client import JiraClient
72
+
73
+ client = JiraClient(token="test")
74
+ headers = client._get_headers(content_type=True)
75
+
76
+ assert "Content-Type" in headers
77
+ assert headers["Content-Type"] == "application/json"
78
+
79
+
80
+ class TestStatusMappings:
81
+ """Tests for status mapping functions in jira/client.py."""
82
+
83
+ def test_map_status_to_jira_known_status(self) -> None:
84
+ """map_status_to_jira should map known statuses correctly."""
85
+ from pennyfarthing_scripts.jira.client import map_status_to_jira
86
+
87
+ assert map_status_to_jira("backlog") == "To Do"
88
+ assert map_status_to_jira("in-progress") == "In Progress"
89
+ assert map_status_to_jira("in_progress") == "In Progress"
90
+ assert map_status_to_jira("done") == "Done"
91
+ assert map_status_to_jira("review") == "In Review"
92
+
93
+ def test_map_status_to_jira_unknown_defaults_to_todo(self) -> None:
94
+ """map_status_to_jira should default to 'To Do' for unknown."""
95
+ from pennyfarthing_scripts.jira.client import map_status_to_jira
96
+
97
+ assert map_status_to_jira("unknown_status") == "To Do"
98
+ assert map_status_to_jira(None) == "To Do"
99
+
100
+ def test_map_jira_to_status_known_status(self) -> None:
101
+ """map_jira_to_status should map known Jira statuses correctly."""
102
+ from pennyfarthing_scripts.jira.client import map_jira_to_status
103
+
104
+ assert map_jira_to_status("To Do") == "backlog"
105
+ assert map_jira_to_status("In Progress") == "in_progress"
106
+ assert map_jira_to_status("Done") == "done"
107
+ assert map_jira_to_status("In Review") == "review"
108
+
109
+ def test_map_jira_to_status_unknown_defaults_to_backlog(self) -> None:
110
+ """map_jira_to_status should default to 'backlog' for unknown."""
111
+ from pennyfarthing_scripts.jira.client import map_jira_to_status
112
+
113
+ assert map_jira_to_status("Unknown Status") == "backlog"
114
+ assert map_jira_to_status(None) == "backlog"
115
+
116
+
117
+ class TestExtractJiraKey:
118
+ """Tests for extract_jira_key function."""
119
+
120
+ def test_extract_jira_key_from_key(self) -> None:
121
+ """extract_jira_key should return key as-is if already a key."""
122
+ from pennyfarthing_scripts.jira.client import extract_jira_key
123
+
124
+ assert extract_jira_key("MSSCI-12345") == "MSSCI-12345"
125
+
126
+ def test_extract_jira_key_from_url(self) -> None:
127
+ """extract_jira_key should extract key from URL."""
128
+ from pennyfarthing_scripts.jira.client import extract_jira_key
129
+
130
+ url = "https://1898andco.atlassian.net/browse/MSSCI-12345"
131
+ assert extract_jira_key(url) == "MSSCI-12345"
132
+
133
+ def test_extract_jira_key_returns_none_for_none(self) -> None:
134
+ """extract_jira_key should return None for None input."""
135
+ from pennyfarthing_scripts.jira.client import extract_jira_key
136
+
137
+ assert extract_jira_key(None) is None
138
+
139
+
140
+ class TestGetJiraField:
141
+ """Tests for get_jira_field helper function."""
142
+
143
+ def test_get_jira_field_simple_path(self) -> None:
144
+ """get_jira_field should extract simple field paths."""
145
+ from pennyfarthing_scripts.jira.client import get_jira_field
146
+
147
+ issue = {"key": "MSSCI-123", "id": "10001"}
148
+ assert get_jira_field(issue, "key") == "MSSCI-123"
149
+
150
+ def test_get_jira_field_nested_path(self) -> None:
151
+ """get_jira_field should extract nested field paths."""
152
+ from pennyfarthing_scripts.jira.client import get_jira_field
153
+
154
+ issue = {
155
+ "fields": {
156
+ "status": {"name": "In Progress"},
157
+ "customfield_10031": 5,
158
+ }
159
+ }
160
+ assert get_jira_field(issue, "fields.status.name") == "In Progress"
161
+ assert get_jira_field(issue, "fields.customfield_10031") == 5
162
+
163
+ def test_get_jira_field_returns_default_if_missing(self) -> None:
164
+ """get_jira_field should return default for missing paths."""
165
+ from pennyfarthing_scripts.jira.client import get_jira_field
166
+
167
+ issue = {"key": "MSSCI-123"}
168
+ assert get_jira_field(issue, "fields.missing", "default") == "default"
169
+ assert get_jira_field(issue, "nonexistent") is None
170
+
171
+ def test_get_jira_field_handles_none_input(self) -> None:
172
+ """get_jira_field should handle None issue input."""
173
+ from pennyfarthing_scripts.jira.client import get_jira_field
174
+
175
+ assert get_jira_field(None, "key", "default") == "default"
176
+
177
+
178
+ class TestJiraSyncModule:
179
+ """Tests for jira/sync.py module."""
180
+
181
+ def test_sync_result_dataclass(self) -> None:
182
+ """SyncResult should be a valid dataclass."""
183
+ from pennyfarthing_scripts.jira.sync import SyncResult
184
+
185
+ result = SyncResult(
186
+ story_id="63-1",
187
+ success=True,
188
+ skipped=False,
189
+ error=None,
190
+ actions=["transitioned"],
191
+ dry_run=False,
192
+ )
193
+ assert result.story_id == "63-1"
194
+ assert result.success is True
195
+ assert "transitioned" in result.actions
196
+
197
+ def test_format_story_line(self) -> None:
198
+ """format_story_line should format story for display."""
199
+ from pennyfarthing_scripts.jira.sync import format_story_line
200
+
201
+ story = {"id": "63-1", "title": "Test Story", "status": "in_progress"}
202
+ result = format_story_line(story)
203
+
204
+ assert "63-1" in result
205
+ assert "Test Story" in result
206
+ assert "in_progress" in result
207
+
208
+ def test_format_summary(self) -> None:
209
+ """format_summary should format sync summary."""
210
+ from pennyfarthing_scripts.jira.sync import format_summary
211
+
212
+ result = format_summary(synced=5, skipped=2, errors=1)
213
+
214
+ assert "5" in result
215
+ assert "2" in result
216
+ assert "1" in result
217
+
218
+
219
+ class TestJiraBidirectionalModule:
220
+ """Tests for jira/bidirectional.py module."""
221
+
222
+ def test_sync_change_dataclass(self) -> None:
223
+ """SyncChange should be a valid dataclass."""
224
+ from pennyfarthing_scripts.jira.bidirectional import SyncChange
225
+
226
+ change = SyncChange(
227
+ key="MSSCI-12345",
228
+ field="status",
229
+ action="update-jira",
230
+ yaml_value="in_progress",
231
+ jira_value="To Do",
232
+ target_value="In Progress",
233
+ )
234
+ assert change.key == "MSSCI-12345"
235
+ assert change.action == "update-jira"
236
+
237
+ def test_sync_plan_dataclass(self) -> None:
238
+ """SyncPlan should be a valid dataclass."""
239
+ from pennyfarthing_scripts.jira.bidirectional import SyncPlan
240
+
241
+ plan = SyncPlan()
242
+ assert plan.changes == []
243
+ assert plan.yaml_only == []
244
+ assert plan.jira_only == []
245
+ assert plan.both == []
246
+
247
+ def test_generate_sync_plan_empty_inputs(self) -> None:
248
+ """generate_sync_plan should handle empty inputs."""
249
+ from pennyfarthing_scripts.jira.bidirectional import generate_sync_plan
250
+
251
+ plan = generate_sync_plan([], [], sync_status=True)
252
+
253
+ assert plan.changes == []
254
+ assert plan.yaml_only == []
255
+ assert plan.jira_only == []
256
+
257
+ def test_generate_sync_plan_identifies_yaml_only(self) -> None:
258
+ """generate_sync_plan should identify stories only in YAML."""
259
+ from pennyfarthing_scripts.jira.bidirectional import generate_sync_plan
260
+
261
+ yaml_stories = [{"id": "63-1", "jira": "MSSCI-12345", "status": "in_progress"}]
262
+ jira_stories: list[dict[str, Any]] = []
263
+
264
+ plan = generate_sync_plan(yaml_stories, jira_stories, sync_status=True)
265
+
266
+ assert "MSSCI-12345" in plan.yaml_only
267
+
268
+ def test_generate_sync_plan_identifies_jira_only(self) -> None:
269
+ """generate_sync_plan should identify stories only in Jira."""
270
+ from pennyfarthing_scripts.jira.bidirectional import generate_sync_plan
271
+
272
+ yaml_stories: list[dict[str, Any]] = []
273
+ jira_stories = [{"key": "MSSCI-12345", "fields": {"status": {"name": "To Do"}}}]
274
+
275
+ plan = generate_sync_plan(yaml_stories, jira_stories, sync_status=True)
276
+
277
+ assert "MSSCI-12345" in plan.jira_only
278
+
279
+ def test_format_sync_plan(self) -> None:
280
+ """format_sync_plan should return formatted string."""
281
+ from pennyfarthing_scripts.jira.bidirectional import (
282
+ SyncPlan,
283
+ format_sync_plan,
284
+ )
285
+
286
+ plan = SyncPlan(
287
+ yaml_only=["MSSCI-111"],
288
+ jira_only=["MSSCI-222"],
289
+ both=["MSSCI-333"],
290
+ )
291
+ result = format_sync_plan(plan)
292
+
293
+ assert "MSSCI-111" in result
294
+ assert "MSSCI-222" in result
295
+ assert "Sync Plan" in result
296
+
297
+
298
+ class TestJiraEpicModule:
299
+ """Tests for jira/epic.py module."""
300
+
301
+ def test_build_epic_payload(self) -> None:
302
+ """build_epic_payload should create valid Jira API payload."""
303
+ from pennyfarthing_scripts.jira.epic import build_epic_payload
304
+
305
+ epic_data = {"title": "Test Epic", "description": "Epic description"}
306
+ payload = build_epic_payload(epic_data)
307
+
308
+ assert "fields" in payload
309
+ assert payload["fields"]["summary"] == "Test Epic"
310
+ assert payload["fields"]["issuetype"]["name"] == "Epic"
311
+ assert "description" in payload["fields"]
312
+
313
+ def test_create_epic_dry_run(self) -> None:
314
+ """create_epic with dry_run should not call API."""
315
+ from pennyfarthing_scripts.jira.epic import create_epic
316
+
317
+ result = create_epic("Test Epic", "Description", dry_run=True)
318
+
319
+ assert result["success"] is True
320
+ assert result["dry_run"] is True
321
+ assert "payload" in result
322
+
323
+
324
+ class TestJiraStoryModule:
325
+ """Tests for jira/story.py module."""
326
+
327
+ def test_sync_story_missing_story(self) -> None:
328
+ """sync_story should return error for missing story."""
329
+ from pennyfarthing_scripts.jira.story import sync_story
330
+
331
+ result = sync_story("nonexistent-99", do_transition=False, dry_run=True)
332
+
333
+ assert result["success"] is False
334
+ assert "not found" in result.get("error", "").lower()