@pennyfarthing/core 7.8.1 → 7.8.4

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 (178) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/init.js +7 -6
  5. package/packages/core/dist/cli/commands/init.js.map +1 -1
  6. package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
  7. package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
  8. package/packages/core/dist/cli/utils/symlinks.js +25 -0
  9. package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
  10. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
  11. package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
  12. package/pennyfarthing-dist/scripts/core/run.sh +5 -5
  13. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
  14. package/pennyfarthing-dist/scripts/git/release.sh +2 -2
  15. package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
  16. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
  17. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
  18. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
  19. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
  20. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
  21. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
  22. package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
  23. package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
  24. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
  25. package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
  26. package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
  27. package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
  28. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
  29. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
  30. package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
  31. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
  32. package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
  33. package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
  34. package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
  35. package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
  36. package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
  37. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
  38. package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
  39. package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
  40. package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
  41. package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
  42. package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
  43. package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
  44. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
  45. package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
  46. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
  47. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
  48. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
  49. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
  50. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
  51. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
  52. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
  53. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
  54. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
  55. package/pennyfarthing_scripts/__init__.py +17 -0
  56. package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  57. package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  58. package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
  59. package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
  60. package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
  61. package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
  62. package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
  63. package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
  64. package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
  65. package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
  66. package/pennyfarthing_scripts/bellmode_hook.py +154 -0
  67. package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
  68. package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
  69. package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
  70. package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
  71. package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
  72. package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
  73. package/pennyfarthing_scripts/brownfield/cli.py +131 -0
  74. package/pennyfarthing_scripts/brownfield/discover.py +753 -0
  75. package/pennyfarthing_scripts/common/__init__.py +49 -0
  76. package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
  77. package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
  78. package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
  79. package/pennyfarthing_scripts/common/config.py +65 -0
  80. package/pennyfarthing_scripts/common/output.py +180 -0
  81. package/pennyfarthing_scripts/config.py +21 -0
  82. package/pennyfarthing_scripts/git/__init__.py +29 -0
  83. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  84. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  85. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  86. package/pennyfarthing_scripts/git/create_branches.py +439 -0
  87. package/pennyfarthing_scripts/git/status_all.py +310 -0
  88. package/pennyfarthing_scripts/hooks.py +455 -0
  89. package/pennyfarthing_scripts/jira/__init__.py +93 -0
  90. package/pennyfarthing_scripts/jira/__main__.py +10 -0
  91. package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  92. package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
  93. package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  94. package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
  95. package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
  96. package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
  97. package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
  98. package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
  99. package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
  100. package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
  101. package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
  102. package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
  103. package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
  104. package/pennyfarthing_scripts/jira/claim.py +211 -0
  105. package/pennyfarthing_scripts/jira/cli.py +150 -0
  106. package/pennyfarthing_scripts/jira/client.py +613 -0
  107. package/pennyfarthing_scripts/jira/epic.py +176 -0
  108. package/pennyfarthing_scripts/jira/story.py +219 -0
  109. package/pennyfarthing_scripts/jira/sync.py +350 -0
  110. package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
  111. package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
  112. package/pennyfarthing_scripts/jira_sync.py +36 -0
  113. package/pennyfarthing_scripts/jira_sync_story.py +30 -0
  114. package/pennyfarthing_scripts/output.py +37 -0
  115. package/pennyfarthing_scripts/preflight/__init__.py +17 -0
  116. package/pennyfarthing_scripts/preflight/__main__.py +10 -0
  117. package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
  119. package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
  120. package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
  121. package/pennyfarthing_scripts/preflight/cli.py +141 -0
  122. package/pennyfarthing_scripts/preflight/finish.py +382 -0
  123. package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
  124. package/pennyfarthing_scripts/prime/__init__.py +38 -0
  125. package/pennyfarthing_scripts/prime/__main__.py +8 -0
  126. package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  127. package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
  128. package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
  129. package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
  130. package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
  131. package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
  132. package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
  133. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  134. package/pennyfarthing_scripts/prime/cli.py +220 -0
  135. package/pennyfarthing_scripts/prime/loader.py +239 -0
  136. package/pennyfarthing_scripts/sprint/__init__.py +66 -0
  137. package/pennyfarthing_scripts/sprint/__main__.py +10 -0
  138. package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
  140. package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  141. package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  142. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  143. package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
  144. package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  145. package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
  146. package/pennyfarthing_scripts/sprint/archive.py +108 -0
  147. package/pennyfarthing_scripts/sprint/cli.py +124 -0
  148. package/pennyfarthing_scripts/sprint/loader.py +193 -0
  149. package/pennyfarthing_scripts/sprint/status.py +122 -0
  150. package/pennyfarthing_scripts/sprint/validator.py +405 -0
  151. package/pennyfarthing_scripts/sprint/work.py +192 -0
  152. package/pennyfarthing_scripts/story/__init__.py +67 -0
  153. package/pennyfarthing_scripts/story/__main__.py +10 -0
  154. package/pennyfarthing_scripts/story/cli.py +105 -0
  155. package/pennyfarthing_scripts/story/create.py +167 -0
  156. package/pennyfarthing_scripts/story/size.py +113 -0
  157. package/pennyfarthing_scripts/story/template.py +151 -0
  158. package/pennyfarthing_scripts/swebench.py +216 -0
  159. package/pennyfarthing_scripts/tests/__init__.py +1 -0
  160. package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  161. package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  162. package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
  163. package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  164. package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
  165. package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  166. package/pennyfarthing_scripts/tests/conftest.py +106 -0
  167. package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
  168. package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
  169. package/pennyfarthing_scripts/tests/test_common.py +180 -0
  170. package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
  171. package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
  172. package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
  173. package/pennyfarthing_scripts/tests/test_prime.py +397 -0
  174. package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
  175. package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
  176. package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
  177. package/pennyfarthing_scripts/welcome_hook.py +157 -0
  178. package/pennyfarthing_scripts/workflow.py +183 -0
@@ -0,0 +1,245 @@
1
+ """Tests for CLI entry point modules.
2
+
3
+ Story 63-9: Reorganize pennyfarthing_scripts into fan-out CLI pattern.
4
+
5
+ These tests verify the CLI modules work as entry points and
6
+ properly delegate to library modules.
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ from typing import Any
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ import pytest
15
+
16
+
17
+ class TestJiraCLIModule:
18
+ """Tests for jira CLI module."""
19
+
20
+ def test_jira_cli_help(self) -> None:
21
+ """jira CLI should show help with --help."""
22
+ result = subprocess.run(
23
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "--help"],
24
+ capture_output=True,
25
+ text=True,
26
+ timeout=30,
27
+ )
28
+
29
+ assert result.returncode == 0
30
+ # Should show usage info
31
+ assert "usage" in result.stdout.lower() or "Usage" in result.stdout
32
+
33
+ def test_jira_cli_view_subcommand(self) -> None:
34
+ """jira CLI should have view subcommand."""
35
+ result = subprocess.run(
36
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "view", "--help"],
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=30,
40
+ )
41
+
42
+ # Should exit 0 with help or fail gracefully without --help
43
+ assert result.returncode in (0, 1, 2)
44
+
45
+ def test_jira_cli_sync_subcommand(self) -> None:
46
+ """jira CLI should have sync subcommand."""
47
+ result = subprocess.run(
48
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "sync", "--help"],
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=30,
52
+ )
53
+
54
+ assert result.returncode in (0, 1, 2)
55
+
56
+ def test_jira_cli_claim_subcommand(self) -> None:
57
+ """jira CLI should have claim subcommand."""
58
+ result = subprocess.run(
59
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "claim", "--help"],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=30,
63
+ )
64
+
65
+ assert result.returncode in (0, 1, 2)
66
+
67
+ def test_jira_cli_create_subcommand(self) -> None:
68
+ """jira CLI should have create subcommand."""
69
+ result = subprocess.run(
70
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "create", "--help"],
71
+ capture_output=True,
72
+ text=True,
73
+ timeout=30,
74
+ )
75
+
76
+ assert result.returncode in (0, 1, 2)
77
+
78
+ def test_jira_cli_bidirectional_subcommand(self) -> None:
79
+ """jira CLI should have bidirectional subcommand."""
80
+ result = subprocess.run(
81
+ [sys.executable, "-m", "pennyfarthing_scripts.jira", "bidirectional", "--help"],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=30,
85
+ )
86
+
87
+ assert result.returncode in (0, 1, 2)
88
+
89
+
90
+ class TestSprintCLIModule:
91
+ """Tests for sprint CLI module."""
92
+
93
+ def test_sprint_cli_help(self) -> None:
94
+ """sprint CLI should show help with --help."""
95
+ result = subprocess.run(
96
+ [sys.executable, "-m", "pennyfarthing_scripts.sprint", "--help"],
97
+ capture_output=True,
98
+ text=True,
99
+ timeout=30,
100
+ )
101
+
102
+ assert result.returncode == 0
103
+ assert "usage" in result.stdout.lower() or "Usage" in result.stdout
104
+
105
+ def test_sprint_cli_status_subcommand(self) -> None:
106
+ """sprint CLI should have status subcommand."""
107
+ result = subprocess.run(
108
+ [sys.executable, "-m", "pennyfarthing_scripts.sprint", "status", "--help"],
109
+ capture_output=True,
110
+ text=True,
111
+ timeout=30,
112
+ )
113
+
114
+ assert result.returncode in (0, 1, 2)
115
+
116
+ def test_sprint_cli_backlog_subcommand(self) -> None:
117
+ """sprint CLI should have backlog subcommand."""
118
+ result = subprocess.run(
119
+ [sys.executable, "-m", "pennyfarthing_scripts.sprint", "backlog", "--help"],
120
+ capture_output=True,
121
+ text=True,
122
+ timeout=30,
123
+ )
124
+
125
+ assert result.returncode in (0, 1, 2)
126
+
127
+ def test_sprint_cli_work_subcommand(self) -> None:
128
+ """sprint CLI should have work subcommand."""
129
+ result = subprocess.run(
130
+ [sys.executable, "-m", "pennyfarthing_scripts.sprint", "work", "--help"],
131
+ capture_output=True,
132
+ text=True,
133
+ timeout=30,
134
+ )
135
+
136
+ assert result.returncode in (0, 1, 2)
137
+
138
+ def test_sprint_cli_archive_subcommand(self) -> None:
139
+ """sprint CLI should have archive subcommand."""
140
+ result = subprocess.run(
141
+ [sys.executable, "-m", "pennyfarthing_scripts.sprint", "archive", "--help"],
142
+ capture_output=True,
143
+ text=True,
144
+ timeout=30,
145
+ )
146
+
147
+ assert result.returncode in (0, 1, 2)
148
+
149
+
150
+ class TestStoryCLIModule:
151
+ """Tests for story CLI module."""
152
+
153
+ def test_story_cli_help(self) -> None:
154
+ """story CLI should show help with --help."""
155
+ result = subprocess.run(
156
+ [sys.executable, "-m", "pennyfarthing_scripts.story", "--help"],
157
+ capture_output=True,
158
+ text=True,
159
+ timeout=30,
160
+ )
161
+
162
+ assert result.returncode == 0
163
+ assert "usage" in result.stdout.lower() or "Usage" in result.stdout
164
+
165
+ def test_story_cli_size_subcommand(self) -> None:
166
+ """story CLI should have size subcommand."""
167
+ result = subprocess.run(
168
+ [sys.executable, "-m", "pennyfarthing_scripts.story", "size", "--help"],
169
+ capture_output=True,
170
+ text=True,
171
+ timeout=30,
172
+ )
173
+
174
+ assert result.returncode in (0, 1, 2)
175
+
176
+ def test_story_cli_template_subcommand(self) -> None:
177
+ """story CLI should have template subcommand."""
178
+ result = subprocess.run(
179
+ [sys.executable, "-m", "pennyfarthing_scripts.story", "template", "--help"],
180
+ capture_output=True,
181
+ text=True,
182
+ timeout=30,
183
+ )
184
+
185
+ assert result.returncode in (0, 1, 2)
186
+
187
+ def test_story_cli_create_subcommand(self) -> None:
188
+ """story CLI should have create subcommand."""
189
+ result = subprocess.run(
190
+ [sys.executable, "-m", "pennyfarthing_scripts.story", "create", "--help"],
191
+ capture_output=True,
192
+ text=True,
193
+ timeout=30,
194
+ )
195
+
196
+ assert result.returncode in (0, 1, 2)
197
+
198
+
199
+ class TestOldCLICompatibility:
200
+ """Tests for backwards compatibility of old CLI modules."""
201
+
202
+ def test_jira_sync_module_runnable(self) -> None:
203
+ """jira_sync.py should still be runnable as module."""
204
+ result = subprocess.run(
205
+ [sys.executable, "-m", "pennyfarthing_scripts.jira_sync", "--help"],
206
+ capture_output=True,
207
+ text=True,
208
+ timeout=30,
209
+ )
210
+
211
+ # Should exit with help or error (not crash)
212
+ assert result.returncode in (0, 1, 2)
213
+
214
+ def test_jira_bidirectional_sync_module_runnable(self) -> None:
215
+ """jira_bidirectional_sync.py should still be runnable."""
216
+ result = subprocess.run(
217
+ [sys.executable, "-m", "pennyfarthing_scripts.jira_bidirectional_sync", "--help"],
218
+ capture_output=True,
219
+ text=True,
220
+ timeout=30,
221
+ )
222
+
223
+ assert result.returncode in (0, 1, 2)
224
+
225
+ def test_jira_epic_creation_module_runnable(self) -> None:
226
+ """jira_epic_creation.py should still be runnable."""
227
+ result = subprocess.run(
228
+ [sys.executable, "-m", "pennyfarthing_scripts.jira_epic_creation", "--help"],
229
+ capture_output=True,
230
+ text=True,
231
+ timeout=30,
232
+ )
233
+
234
+ assert result.returncode in (0, 1, 2)
235
+
236
+ def test_jira_sync_story_module_runnable(self) -> None:
237
+ """jira_sync_story.py should still be runnable."""
238
+ result = subprocess.run(
239
+ [sys.executable, "-m", "pennyfarthing_scripts.jira_sync_story", "--help"],
240
+ capture_output=True,
241
+ text=True,
242
+ timeout=30,
243
+ )
244
+
245
+ assert result.returncode in (0, 1, 2)
@@ -0,0 +1,180 @@
1
+ """Tests for common/ shared utilities package.
2
+
3
+ Story 63-9: Reorganize pennyfarthing_scripts into fan-out CLI pattern.
4
+
5
+ These tests verify the common/ package provides shared utilities
6
+ that work correctly when imported from the new location.
7
+ """
8
+
9
+ import io
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Generator
14
+ from unittest.mock import patch
15
+
16
+ import pytest
17
+
18
+
19
+ class TestOutputModule:
20
+ """Tests for common/output.py module."""
21
+
22
+ def test_success_prints_green_prefix(self) -> None:
23
+ """success() should print with [OK] prefix."""
24
+ from pennyfarthing_scripts.common import output
25
+
26
+ buffer = io.StringIO()
27
+ # Force color support for testing
28
+ with patch.object(output, "_supports_color", return_value=True):
29
+ output.success("Test message", file=buffer)
30
+
31
+ result = buffer.getvalue()
32
+ assert "[OK]" in result or "\x1b[32m" in result # Green or prefix
33
+ assert "Test message" in result
34
+
35
+ def test_info_prints_blue_prefix(self) -> None:
36
+ """info() should print with [INFO] prefix."""
37
+ from pennyfarthing_scripts.common import output
38
+
39
+ buffer = io.StringIO()
40
+ output.info("Info message", file=buffer)
41
+
42
+ result = buffer.getvalue()
43
+ assert "INFO" in result or "Info message" in result
44
+
45
+ def test_warn_prints_yellow_prefix(self) -> None:
46
+ """warn() should print with [WARN] prefix."""
47
+ from pennyfarthing_scripts.common import output
48
+
49
+ buffer = io.StringIO()
50
+ output.warn("Warning message", file=buffer)
51
+
52
+ result = buffer.getvalue()
53
+ assert "WARN" in result or "Warning message" in result
54
+
55
+ def test_error_prints_red_prefix(self) -> None:
56
+ """error() should print with [ERROR] prefix."""
57
+ from pennyfarthing_scripts.common import output
58
+
59
+ buffer = io.StringIO()
60
+ output.error("Error message", file=buffer)
61
+
62
+ result = buffer.getvalue()
63
+ assert "ERROR" in result or "Error message" in result
64
+
65
+ def test_no_color_env_disables_colors(self) -> None:
66
+ """NO_COLOR environment variable should disable colors."""
67
+ from pennyfarthing_scripts.common import output
68
+
69
+ with patch.dict(os.environ, {"NO_COLOR": "1"}):
70
+ buffer = io.StringIO()
71
+ output.success("Test", file=buffer)
72
+ result = buffer.getvalue()
73
+ # Should not contain ANSI escape codes
74
+ assert "\x1b[" not in result
75
+
76
+ def test_force_color_env_enables_colors(self) -> None:
77
+ """FORCE_COLOR environment variable should enable colors."""
78
+ from pennyfarthing_scripts.common import output
79
+
80
+ with patch.dict(os.environ, {"FORCE_COLOR": "1"}, clear=False):
81
+ # Remove NO_COLOR if present
82
+ with patch.dict(os.environ, {"NO_COLOR": ""}, clear=False):
83
+ assert output._supports_color(sys.stderr) is True
84
+
85
+ def test_header_prints_decorated_line(self) -> None:
86
+ """header() should print decorated header."""
87
+ from pennyfarthing_scripts.common import output
88
+
89
+ buffer = io.StringIO()
90
+ output.header("Test Header", char="=", width=40, file=buffer)
91
+
92
+ result = buffer.getvalue()
93
+ assert "Test Header" in result
94
+ assert "=" * 40 in result
95
+
96
+ def test_divider_prints_line(self) -> None:
97
+ """divider() should print a line."""
98
+ from pennyfarthing_scripts.common import output
99
+
100
+ buffer = io.StringIO()
101
+ output.divider(char="-", width=20, file=buffer)
102
+
103
+ result = buffer.getvalue()
104
+ assert "-" * 20 in result
105
+
106
+
107
+ class TestConfigModule:
108
+ """Tests for common/config.py module."""
109
+
110
+ def test_get_project_root_finds_pennyfarthing_dir(self) -> None:
111
+ """get_project_root() should find .pennyfarthing directory."""
112
+ from pennyfarthing_scripts.common import config
113
+
114
+ # This test assumes we're running from within the pennyfarthing project
115
+ root = config.get_project_root()
116
+ assert root is not None
117
+ assert (root / ".pennyfarthing").is_dir()
118
+
119
+ def test_get_project_root_raises_if_not_found(self) -> None:
120
+ """get_project_root() should raise if no .pennyfarthing found."""
121
+ from pennyfarthing_scripts.common import config
122
+
123
+ # Start from root filesystem where there's no .pennyfarthing
124
+ with pytest.raises(FileNotFoundError):
125
+ config.get_project_root(start_dir=Path("/"))
126
+
127
+ def test_load_yaml_config_returns_dict(self) -> None:
128
+ """load_yaml_config() should return parsed YAML as dict."""
129
+ from pennyfarthing_scripts.common import config
130
+
131
+ # Test with existing sprint file
132
+ root = config.get_project_root()
133
+ sprint_path = root / "sprint" / "current-sprint.yaml"
134
+
135
+ if sprint_path.exists():
136
+ result = config.load_yaml_config(sprint_path)
137
+ assert isinstance(result, dict)
138
+
139
+ def test_load_yaml_config_returns_none_if_missing(self) -> None:
140
+ """load_yaml_config() should return None for missing files."""
141
+ from pennyfarthing_scripts.common import config
142
+
143
+ result = config.load_yaml_config(Path("/nonexistent/file.yaml"))
144
+ assert result is None
145
+
146
+ def test_find_project_root_alias(self) -> None:
147
+ """find_project_root should be an alias for get_project_root."""
148
+ from pennyfarthing_scripts.common import config
149
+
150
+ assert config.find_project_root == config.get_project_root
151
+
152
+ def test_load_pennyfarthing_config_returns_dict(self) -> None:
153
+ """load_pennyfarthing_config() should return config or empty dict."""
154
+ from pennyfarthing_scripts.common import config
155
+
156
+ result = config.load_pennyfarthing_config()
157
+ assert isinstance(result, dict)
158
+
159
+
160
+ class TestColorsClass:
161
+ """Tests for Colors class with ANSI codes."""
162
+
163
+ def test_colors_has_expected_constants(self) -> None:
164
+ """Colors class should have standard ANSI color codes."""
165
+ from pennyfarthing_scripts.common.output import Colors
166
+
167
+ assert hasattr(Colors, "RED")
168
+ assert hasattr(Colors, "GREEN")
169
+ assert hasattr(Colors, "YELLOW")
170
+ assert hasattr(Colors, "BLUE")
171
+ assert hasattr(Colors, "RESET")
172
+ assert hasattr(Colors, "BOLD")
173
+ assert hasattr(Colors, "DIM")
174
+
175
+ def test_colors_are_ansi_escape_codes(self) -> None:
176
+ """Color constants should be ANSI escape sequences."""
177
+ from pennyfarthing_scripts.common.output import Colors
178
+
179
+ assert Colors.RED.startswith("\x1b[")
180
+ assert Colors.RESET == "\x1b[0m"