@pennyfarthing/core 7.8.0 → 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.
- package/README.md +1 -1
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/doctor.d.ts +3 -0
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +20 -9
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -0
- package/pennyfarthing_scripts/__init__.py +17 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +154 -0
- package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
- package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +131 -0
- package/pennyfarthing_scripts/brownfield/discover.py +753 -0
- package/pennyfarthing_scripts/common/__init__.py +49 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +65 -0
- package/pennyfarthing_scripts/common/output.py +180 -0
- package/pennyfarthing_scripts/config.py +21 -0
- package/pennyfarthing_scripts/git/__init__.py +29 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +439 -0
- package/pennyfarthing_scripts/git/status_all.py +310 -0
- package/pennyfarthing_scripts/hooks.py +455 -0
- package/pennyfarthing_scripts/jira/__init__.py +93 -0
- package/pennyfarthing_scripts/jira/__main__.py +10 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
- package/pennyfarthing_scripts/jira/claim.py +211 -0
- package/pennyfarthing_scripts/jira/cli.py +150 -0
- package/pennyfarthing_scripts/jira/client.py +613 -0
- package/pennyfarthing_scripts/jira/epic.py +176 -0
- package/pennyfarthing_scripts/jira/story.py +219 -0
- package/pennyfarthing_scripts/jira/sync.py +350 -0
- package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
- package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
- package/pennyfarthing_scripts/jira_sync.py +36 -0
- package/pennyfarthing_scripts/jira_sync_story.py +30 -0
- package/pennyfarthing_scripts/output.py +37 -0
- package/pennyfarthing_scripts/preflight/__init__.py +17 -0
- package/pennyfarthing_scripts/preflight/__main__.py +10 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/cli.py +141 -0
- package/pennyfarthing_scripts/preflight/finish.py +382 -0
- package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
- package/pennyfarthing_scripts/prime/__init__.py +38 -0
- package/pennyfarthing_scripts/prime/__main__.py +8 -0
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +220 -0
- package/pennyfarthing_scripts/prime/loader.py +239 -0
- package/pennyfarthing_scripts/sprint/__init__.py +66 -0
- package/pennyfarthing_scripts/sprint/__main__.py +10 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +108 -0
- package/pennyfarthing_scripts/sprint/cli.py +124 -0
- package/pennyfarthing_scripts/sprint/loader.py +193 -0
- package/pennyfarthing_scripts/sprint/status.py +122 -0
- package/pennyfarthing_scripts/sprint/validator.py +405 -0
- package/pennyfarthing_scripts/sprint/work.py +192 -0
- package/pennyfarthing_scripts/story/__init__.py +67 -0
- package/pennyfarthing_scripts/story/__main__.py +10 -0
- package/pennyfarthing_scripts/story/cli.py +105 -0
- package/pennyfarthing_scripts/story/create.py +167 -0
- package/pennyfarthing_scripts/story/size.py +113 -0
- package/pennyfarthing_scripts/story/template.py +151 -0
- package/pennyfarthing_scripts/swebench.py +216 -0
- package/pennyfarthing_scripts/tests/__init__.py +1 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +106 -0
- package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
- package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
- package/pennyfarthing_scripts/tests/test_common.py +180 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
- package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
- package/pennyfarthing_scripts/tests/test_prime.py +397 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
- package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
- package/pennyfarthing_scripts/welcome_hook.py +157 -0
- package/pennyfarthing_scripts/workflow.py +183 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Tests for pennyfarthing_scripts.prime module.
|
|
2
|
+
|
|
3
|
+
Tests context loading for prime command.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch, MagicMock
|
|
9
|
+
|
|
10
|
+
from pennyfarthing_scripts.prime.loader import (
|
|
11
|
+
load_agent_definition,
|
|
12
|
+
load_behavior_guide,
|
|
13
|
+
load_sprint_context,
|
|
14
|
+
load_session_context,
|
|
15
|
+
load_sidecars,
|
|
16
|
+
load_domain_docs,
|
|
17
|
+
_extract_session_parts,
|
|
18
|
+
_find_session_file,
|
|
19
|
+
)
|
|
20
|
+
from pennyfarthing_scripts.prime.cli import prime, main
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestLoadAgentDefinition:
|
|
24
|
+
"""Tests for load_agent_definition function."""
|
|
25
|
+
|
|
26
|
+
def test_load_existing_agent(self, tmp_path: Path) -> None:
|
|
27
|
+
"""Test loading an existing agent definition."""
|
|
28
|
+
# Setup
|
|
29
|
+
agents_dir = tmp_path / ".pennyfarthing" / "agents"
|
|
30
|
+
agents_dir.mkdir(parents=True)
|
|
31
|
+
agent_file = agents_dir / "dev.md"
|
|
32
|
+
agent_file.write_text("# Developer Agent\n\nTest content")
|
|
33
|
+
|
|
34
|
+
# Test
|
|
35
|
+
result = load_agent_definition("dev", tmp_path)
|
|
36
|
+
|
|
37
|
+
# Verify
|
|
38
|
+
assert result is not None
|
|
39
|
+
assert "# Developer Agent" in result
|
|
40
|
+
assert "Test content" in result
|
|
41
|
+
|
|
42
|
+
def test_load_nonexistent_agent(self, tmp_path: Path) -> None:
|
|
43
|
+
"""Test loading a non-existent agent returns None."""
|
|
44
|
+
# Setup
|
|
45
|
+
agents_dir = tmp_path / ".pennyfarthing" / "agents"
|
|
46
|
+
agents_dir.mkdir(parents=True)
|
|
47
|
+
|
|
48
|
+
# Test
|
|
49
|
+
result = load_agent_definition("nonexistent", tmp_path)
|
|
50
|
+
|
|
51
|
+
# Verify
|
|
52
|
+
assert result is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestLoadBehaviorGuide:
|
|
56
|
+
"""Tests for load_behavior_guide function."""
|
|
57
|
+
|
|
58
|
+
def test_load_existing_guide(self, tmp_path: Path) -> None:
|
|
59
|
+
"""Test loading an existing behavior guide."""
|
|
60
|
+
# Setup
|
|
61
|
+
guides_dir = tmp_path / ".pennyfarthing" / "guides"
|
|
62
|
+
guides_dir.mkdir(parents=True)
|
|
63
|
+
guide_file = guides_dir / "agent-behavior.md"
|
|
64
|
+
guide_file.write_text("# Agent Behavior Guide\n\nShared protocols")
|
|
65
|
+
|
|
66
|
+
# Test
|
|
67
|
+
result = load_behavior_guide(tmp_path)
|
|
68
|
+
|
|
69
|
+
# Verify
|
|
70
|
+
assert result is not None
|
|
71
|
+
assert "# Agent Behavior Guide" in result
|
|
72
|
+
|
|
73
|
+
def test_load_nonexistent_guide(self, tmp_path: Path) -> None:
|
|
74
|
+
"""Test loading a non-existent guide returns None."""
|
|
75
|
+
# Setup
|
|
76
|
+
guides_dir = tmp_path / ".pennyfarthing" / "guides"
|
|
77
|
+
guides_dir.mkdir(parents=True)
|
|
78
|
+
|
|
79
|
+
# Test
|
|
80
|
+
result = load_behavior_guide(tmp_path)
|
|
81
|
+
|
|
82
|
+
# Verify
|
|
83
|
+
assert result is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestLoadSprintContext:
|
|
87
|
+
"""Tests for load_sprint_context function."""
|
|
88
|
+
|
|
89
|
+
def test_load_sprint_context(self, tmp_path: Path, sample_sprint_data: dict) -> None:
|
|
90
|
+
"""Test loading sprint context."""
|
|
91
|
+
import yaml
|
|
92
|
+
|
|
93
|
+
# Setup
|
|
94
|
+
sprint_dir = tmp_path / "sprint"
|
|
95
|
+
sprint_dir.mkdir()
|
|
96
|
+
sprint_file = sprint_dir / "current-sprint.yaml"
|
|
97
|
+
# Add sprint number for the test
|
|
98
|
+
sample_sprint_data["sprint"]["number"] = 2604
|
|
99
|
+
sample_sprint_data["sprint"]["goal"] = "Test Goal"
|
|
100
|
+
sprint_file.write_text(yaml.dump(sample_sprint_data))
|
|
101
|
+
|
|
102
|
+
# Also need .pennyfarthing dir for project root detection
|
|
103
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
104
|
+
pf_dir.mkdir()
|
|
105
|
+
|
|
106
|
+
# Test
|
|
107
|
+
with patch("pennyfarthing_scripts.prime.loader.get_project_root", return_value=tmp_path):
|
|
108
|
+
with patch("pennyfarthing_scripts.sprint.loader.get_project_root", return_value=tmp_path):
|
|
109
|
+
result = load_sprint_context(tmp_path)
|
|
110
|
+
|
|
111
|
+
# Verify
|
|
112
|
+
assert result is not None
|
|
113
|
+
assert "Sprint 2604" in result
|
|
114
|
+
|
|
115
|
+
def test_no_sprint_file(self, tmp_path: Path) -> None:
|
|
116
|
+
"""Test when no sprint file exists."""
|
|
117
|
+
# Setup - no sprint file
|
|
118
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
119
|
+
pf_dir.mkdir()
|
|
120
|
+
|
|
121
|
+
# Test
|
|
122
|
+
result = load_sprint_context(tmp_path)
|
|
123
|
+
|
|
124
|
+
# Verify
|
|
125
|
+
assert result is None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestExtractSessionParts:
|
|
129
|
+
"""Tests for _extract_session_parts function."""
|
|
130
|
+
|
|
131
|
+
def test_extract_header_and_assessment(self) -> None:
|
|
132
|
+
"""Test extracting header and assessment from session file."""
|
|
133
|
+
content = """# Session: Test Story
|
|
134
|
+
|
|
135
|
+
Story: 63-1
|
|
136
|
+
Status: in_progress
|
|
137
|
+
Agent: dev
|
|
138
|
+
|
|
139
|
+
## TEA Assessment
|
|
140
|
+
|
|
141
|
+
Tests written successfully.
|
|
142
|
+
Coverage: 85%
|
|
143
|
+
|
|
144
|
+
## Dev Assessment
|
|
145
|
+
|
|
146
|
+
Implementation complete.
|
|
147
|
+
All tests passing.
|
|
148
|
+
"""
|
|
149
|
+
header, assessment = _extract_session_parts(content)
|
|
150
|
+
|
|
151
|
+
# Header should be everything before first ##
|
|
152
|
+
assert "# Session: Test Story" in header
|
|
153
|
+
assert "Story: 63-1" in header
|
|
154
|
+
assert "Agent: dev" in header
|
|
155
|
+
|
|
156
|
+
# Assessment should be the LAST assessment section
|
|
157
|
+
assert "## Dev Assessment" in assessment
|
|
158
|
+
assert "Implementation complete" in assessment
|
|
159
|
+
# Should NOT include TEA assessment
|
|
160
|
+
assert "TEA Assessment" not in assessment
|
|
161
|
+
|
|
162
|
+
def test_extract_no_assessment(self) -> None:
|
|
163
|
+
"""Test when no assessment section exists."""
|
|
164
|
+
content = """# Session: Test Story
|
|
165
|
+
|
|
166
|
+
Story: 63-1
|
|
167
|
+
Status: in_progress
|
|
168
|
+
"""
|
|
169
|
+
header, assessment = _extract_session_parts(content)
|
|
170
|
+
|
|
171
|
+
assert "# Session: Test Story" in header
|
|
172
|
+
assert assessment == ""
|
|
173
|
+
|
|
174
|
+
def test_extract_header_only(self) -> None:
|
|
175
|
+
"""Test file with only header content."""
|
|
176
|
+
content = "# Session Header\n\nSome metadata"
|
|
177
|
+
|
|
178
|
+
header, assessment = _extract_session_parts(content)
|
|
179
|
+
|
|
180
|
+
assert "# Session Header" in header
|
|
181
|
+
assert assessment == ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestFindSessionFile:
|
|
185
|
+
"""Tests for _find_session_file function."""
|
|
186
|
+
|
|
187
|
+
def test_find_session_file(self, tmp_path: Path) -> None:
|
|
188
|
+
"""Test finding session file."""
|
|
189
|
+
# Setup
|
|
190
|
+
session_dir = tmp_path / ".session"
|
|
191
|
+
session_dir.mkdir()
|
|
192
|
+
session_file = session_dir / "63-1-session.md"
|
|
193
|
+
session_file.write_text("# Session")
|
|
194
|
+
|
|
195
|
+
# Test
|
|
196
|
+
result = _find_session_file(tmp_path)
|
|
197
|
+
|
|
198
|
+
# Verify
|
|
199
|
+
assert result is not None
|
|
200
|
+
assert result.name == "63-1-session.md"
|
|
201
|
+
|
|
202
|
+
def test_no_session_directory(self, tmp_path: Path) -> None:
|
|
203
|
+
"""Test when no session directory exists."""
|
|
204
|
+
result = _find_session_file(tmp_path)
|
|
205
|
+
assert result is None
|
|
206
|
+
|
|
207
|
+
def test_empty_session_directory(self, tmp_path: Path) -> None:
|
|
208
|
+
"""Test when session directory is empty."""
|
|
209
|
+
session_dir = tmp_path / ".session"
|
|
210
|
+
session_dir.mkdir()
|
|
211
|
+
|
|
212
|
+
result = _find_session_file(tmp_path)
|
|
213
|
+
assert result is None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class TestLoadSessionContext:
|
|
217
|
+
"""Tests for load_session_context function."""
|
|
218
|
+
|
|
219
|
+
def test_load_session_context(self, tmp_path: Path) -> None:
|
|
220
|
+
"""Test loading full session context."""
|
|
221
|
+
# Setup
|
|
222
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
223
|
+
pf_dir.mkdir()
|
|
224
|
+
session_dir = tmp_path / ".session"
|
|
225
|
+
session_dir.mkdir()
|
|
226
|
+
session_file = session_dir / "63-1-session.md"
|
|
227
|
+
session_file.write_text("""# Session: Story 63-1
|
|
228
|
+
|
|
229
|
+
Story: 63-1
|
|
230
|
+
Status: in_progress
|
|
231
|
+
|
|
232
|
+
## Dev Assessment
|
|
233
|
+
|
|
234
|
+
Work complete.
|
|
235
|
+
""")
|
|
236
|
+
|
|
237
|
+
# Test
|
|
238
|
+
with patch("pennyfarthing_scripts.prime.loader.get_project_root", return_value=tmp_path):
|
|
239
|
+
result = load_session_context(tmp_path)
|
|
240
|
+
|
|
241
|
+
# Verify
|
|
242
|
+
assert result is not None
|
|
243
|
+
filename, header, assessment = result
|
|
244
|
+
assert filename == "63-1-session.md"
|
|
245
|
+
assert "# Session: Story 63-1" in header
|
|
246
|
+
assert "## Dev Assessment" in assessment
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestLoadSidecars:
|
|
250
|
+
"""Tests for load_sidecars function."""
|
|
251
|
+
|
|
252
|
+
def test_load_all_sidecars(self, tmp_path: Path) -> None:
|
|
253
|
+
"""Test loading all sidecar files."""
|
|
254
|
+
# Setup
|
|
255
|
+
sidecar_dir = tmp_path / ".pennyfarthing" / "sidecars" / "dev"
|
|
256
|
+
sidecar_dir.mkdir(parents=True)
|
|
257
|
+
(sidecar_dir / "patterns.md").write_text("# Patterns")
|
|
258
|
+
(sidecar_dir / "gotchas.md").write_text("# Gotchas")
|
|
259
|
+
(sidecar_dir / "decisions.md").write_text("# Decisions")
|
|
260
|
+
|
|
261
|
+
# Test
|
|
262
|
+
result = load_sidecars("dev", tmp_path)
|
|
263
|
+
|
|
264
|
+
# Verify
|
|
265
|
+
assert len(result) == 3
|
|
266
|
+
assert "# Patterns" in result["patterns.md"]
|
|
267
|
+
assert "# Gotchas" in result["gotchas.md"]
|
|
268
|
+
assert "# Decisions" in result["decisions.md"]
|
|
269
|
+
|
|
270
|
+
def test_load_partial_sidecars(self, tmp_path: Path) -> None:
|
|
271
|
+
"""Test loading when only some sidecars exist."""
|
|
272
|
+
# Setup
|
|
273
|
+
sidecar_dir = tmp_path / ".pennyfarthing" / "sidecars" / "dev"
|
|
274
|
+
sidecar_dir.mkdir(parents=True)
|
|
275
|
+
(sidecar_dir / "patterns.md").write_text("# Patterns only")
|
|
276
|
+
|
|
277
|
+
# Test
|
|
278
|
+
result = load_sidecars("dev", tmp_path)
|
|
279
|
+
|
|
280
|
+
# Verify
|
|
281
|
+
assert len(result) == 1
|
|
282
|
+
assert "patterns.md" in result
|
|
283
|
+
|
|
284
|
+
def test_no_sidecar_directory(self, tmp_path: Path) -> None:
|
|
285
|
+
"""Test when no sidecar directory exists."""
|
|
286
|
+
result = load_sidecars("dev", tmp_path)
|
|
287
|
+
assert result == {}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class TestLoadDomainDocs:
|
|
291
|
+
"""Tests for load_domain_docs function."""
|
|
292
|
+
|
|
293
|
+
def test_load_domain_docs(self, tmp_path: Path) -> None:
|
|
294
|
+
"""Test loading domain documentation."""
|
|
295
|
+
# Setup
|
|
296
|
+
project_dir = tmp_path / ".claude" / "project"
|
|
297
|
+
project_dir.mkdir(parents=True)
|
|
298
|
+
(project_dir / "CLAUDE-api.md").write_text("# API Docs")
|
|
299
|
+
(project_dir / "CLAUDE-testing.md").write_text("# Testing Docs")
|
|
300
|
+
(project_dir / "other-file.md").write_text("# Not included")
|
|
301
|
+
|
|
302
|
+
# Test
|
|
303
|
+
result = load_domain_docs(tmp_path)
|
|
304
|
+
|
|
305
|
+
# Verify
|
|
306
|
+
assert len(result) == 2
|
|
307
|
+
filenames = [f for f, _ in result]
|
|
308
|
+
assert "CLAUDE-api.md" in filenames
|
|
309
|
+
assert "CLAUDE-testing.md" in filenames
|
|
310
|
+
assert "other-file.md" not in filenames
|
|
311
|
+
|
|
312
|
+
def test_no_domain_docs_directory(self, tmp_path: Path) -> None:
|
|
313
|
+
"""Test when no .claude/project directory exists."""
|
|
314
|
+
result = load_domain_docs(tmp_path)
|
|
315
|
+
assert result == []
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class TestPrimeFunction:
|
|
319
|
+
"""Tests for the prime() function."""
|
|
320
|
+
|
|
321
|
+
def test_minimal_mode(self, tmp_path: Path, capsys) -> None:
|
|
322
|
+
"""Test minimal mode returns immediately."""
|
|
323
|
+
result = prime(minimal=True, project_root=tmp_path)
|
|
324
|
+
|
|
325
|
+
assert result == 0
|
|
326
|
+
captured = capsys.readouterr()
|
|
327
|
+
assert captured.out == ""
|
|
328
|
+
|
|
329
|
+
def test_quiet_suppresses_headers(self, tmp_path: Path, capsys) -> None:
|
|
330
|
+
"""Test quiet mode suppresses headers but shows content."""
|
|
331
|
+
# Setup
|
|
332
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
333
|
+
pf_dir.mkdir()
|
|
334
|
+
agents_dir = pf_dir / "agents"
|
|
335
|
+
agents_dir.mkdir()
|
|
336
|
+
(agents_dir / "dev.md").write_text("Agent content here")
|
|
337
|
+
|
|
338
|
+
# Test
|
|
339
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
340
|
+
result = prime(agent_name="dev", quiet=True, project_root=tmp_path)
|
|
341
|
+
|
|
342
|
+
# Verify
|
|
343
|
+
assert result == 0
|
|
344
|
+
captured = capsys.readouterr()
|
|
345
|
+
assert "Agent content here" in captured.out
|
|
346
|
+
assert "# Agent Definition" not in captured.out
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class TestMainCLI:
|
|
350
|
+
"""Tests for CLI main() function."""
|
|
351
|
+
|
|
352
|
+
def test_help_flag(self) -> None:
|
|
353
|
+
"""Test --help flag."""
|
|
354
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
355
|
+
main(["--help"])
|
|
356
|
+
assert exc_info.value.code == 0
|
|
357
|
+
|
|
358
|
+
def test_minimal_flag(self, capsys) -> None:
|
|
359
|
+
"""Test --minimal flag."""
|
|
360
|
+
# With minimal, should just return 0 without any output
|
|
361
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root") as mock_root:
|
|
362
|
+
mock_root.return_value = Path("/tmp/test")
|
|
363
|
+
result = main(["--minimal"])
|
|
364
|
+
|
|
365
|
+
assert result == 0
|
|
366
|
+
captured = capsys.readouterr()
|
|
367
|
+
assert captured.out == ""
|
|
368
|
+
|
|
369
|
+
def test_agent_flag(self, tmp_path: Path, capsys) -> None:
|
|
370
|
+
"""Test --agent flag."""
|
|
371
|
+
# Setup
|
|
372
|
+
pf_dir = tmp_path / ".pennyfarthing"
|
|
373
|
+
pf_dir.mkdir()
|
|
374
|
+
agents_dir = pf_dir / "agents"
|
|
375
|
+
agents_dir.mkdir()
|
|
376
|
+
(agents_dir / "tea.md").write_text("# TEA Agent\nTest content")
|
|
377
|
+
|
|
378
|
+
# Test
|
|
379
|
+
with patch("pennyfarthing_scripts.prime.cli.get_project_root", return_value=tmp_path):
|
|
380
|
+
result = main(["--agent", "tea"])
|
|
381
|
+
|
|
382
|
+
# Verify
|
|
383
|
+
assert result == 0
|
|
384
|
+
captured = capsys.readouterr()
|
|
385
|
+
assert "# TEA Agent" in captured.out
|
|
386
|
+
|
|
387
|
+
def test_project_not_found_error(self, capsys) -> None:
|
|
388
|
+
"""Test error handling when project root not found."""
|
|
389
|
+
with patch(
|
|
390
|
+
"pennyfarthing_scripts.prime.cli.get_project_root",
|
|
391
|
+
side_effect=FileNotFoundError("No project found"),
|
|
392
|
+
):
|
|
393
|
+
result = main([])
|
|
394
|
+
|
|
395
|
+
assert result == 1
|
|
396
|
+
captured = capsys.readouterr()
|
|
397
|
+
assert "Error" in captured.err
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Tests for sprint/ library package.
|
|
2
|
+
|
|
3
|
+
Story 63-9: Reorganize pennyfarthing_scripts into fan-out CLI pattern.
|
|
4
|
+
|
|
5
|
+
These tests verify the sprint/ package modules work correctly
|
|
6
|
+
after reorganization from flat modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestSprintLoader:
|
|
17
|
+
"""Tests for sprint/loader.py module."""
|
|
18
|
+
|
|
19
|
+
def test_load_sprint_returns_dict(self) -> None:
|
|
20
|
+
"""load_sprint should return sprint data as dict."""
|
|
21
|
+
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
22
|
+
|
|
23
|
+
result = load_sprint()
|
|
24
|
+
|
|
25
|
+
# In a project with sprint data, should return dict
|
|
26
|
+
# May return None if no sprint file exists
|
|
27
|
+
assert result is None or isinstance(result, dict)
|
|
28
|
+
|
|
29
|
+
def test_load_sprint_with_custom_root(self) -> None:
|
|
30
|
+
"""load_sprint should accept custom project root."""
|
|
31
|
+
from pennyfarthing_scripts.sprint.loader import load_sprint
|
|
32
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
33
|
+
|
|
34
|
+
root = get_project_root()
|
|
35
|
+
result = load_sprint(project_root=root)
|
|
36
|
+
|
|
37
|
+
assert result is None or isinstance(result, dict)
|
|
38
|
+
|
|
39
|
+
def test_find_epic_by_number(self) -> None:
|
|
40
|
+
"""find_epic should find epic by number."""
|
|
41
|
+
from pennyfarthing_scripts.sprint.loader import find_epic
|
|
42
|
+
|
|
43
|
+
sprint_data = {
|
|
44
|
+
"epics": [
|
|
45
|
+
{"id": "epic-63", "title": "Test Epic"},
|
|
46
|
+
{"id": "epic-64", "title": "Another Epic"},
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Find by full ID
|
|
51
|
+
epic = find_epic(sprint_data, "epic-63")
|
|
52
|
+
assert epic is not None
|
|
53
|
+
assert epic["title"] == "Test Epic"
|
|
54
|
+
|
|
55
|
+
# Find by number only
|
|
56
|
+
epic = find_epic(sprint_data, "63")
|
|
57
|
+
assert epic is not None
|
|
58
|
+
assert epic["title"] == "Test Epic"
|
|
59
|
+
|
|
60
|
+
def test_find_epic_returns_none_if_not_found(self) -> None:
|
|
61
|
+
"""find_epic should return None if epic not found."""
|
|
62
|
+
from pennyfarthing_scripts.sprint.loader import find_epic
|
|
63
|
+
|
|
64
|
+
sprint_data = {"epics": [{"id": "epic-63", "title": "Test Epic"}]}
|
|
65
|
+
|
|
66
|
+
result = find_epic(sprint_data, "epic-99")
|
|
67
|
+
assert result is None
|
|
68
|
+
|
|
69
|
+
def test_find_epic_handles_empty_data(self) -> None:
|
|
70
|
+
"""find_epic should handle empty or None data."""
|
|
71
|
+
from pennyfarthing_scripts.sprint.loader import find_epic
|
|
72
|
+
|
|
73
|
+
assert find_epic(None, "63") is None
|
|
74
|
+
assert find_epic({}, "63") is None
|
|
75
|
+
assert find_epic({"epics": []}, "63") is None
|
|
76
|
+
|
|
77
|
+
def test_find_story_in_epic(self) -> None:
|
|
78
|
+
"""find_story should find story within an epic."""
|
|
79
|
+
from pennyfarthing_scripts.sprint.loader import find_story
|
|
80
|
+
|
|
81
|
+
epic = {
|
|
82
|
+
"id": "epic-63",
|
|
83
|
+
"stories": [
|
|
84
|
+
{"id": "63-1", "title": "First Story"},
|
|
85
|
+
{"id": "63-2", "title": "Second Story"},
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
story = find_story(epic, "63-1")
|
|
90
|
+
assert story is not None
|
|
91
|
+
assert story["title"] == "First Story"
|
|
92
|
+
|
|
93
|
+
def test_find_story_returns_none_if_not_found(self) -> None:
|
|
94
|
+
"""find_story should return None if story not found."""
|
|
95
|
+
from pennyfarthing_scripts.sprint.loader import find_story
|
|
96
|
+
|
|
97
|
+
epic = {"id": "epic-63", "stories": [{"id": "63-1", "title": "Story"}]}
|
|
98
|
+
|
|
99
|
+
result = find_story(epic, "63-99")
|
|
100
|
+
assert result is None
|
|
101
|
+
|
|
102
|
+
def test_get_all_stories_returns_flat_list(self) -> None:
|
|
103
|
+
"""get_all_stories should return flat list from all epics."""
|
|
104
|
+
from pennyfarthing_scripts.sprint.loader import get_all_stories
|
|
105
|
+
|
|
106
|
+
# With actual sprint data loaded
|
|
107
|
+
stories = get_all_stories()
|
|
108
|
+
|
|
109
|
+
# Should be a list (may be empty if no sprint data)
|
|
110
|
+
assert isinstance(stories, list)
|
|
111
|
+
|
|
112
|
+
def test_get_story_by_id(self) -> None:
|
|
113
|
+
"""get_story_by_id should find story across all epics."""
|
|
114
|
+
from pennyfarthing_scripts.sprint.loader import get_story_by_id
|
|
115
|
+
|
|
116
|
+
# This relies on actual sprint data
|
|
117
|
+
result = get_story_by_id("nonexistent-99")
|
|
118
|
+
assert result is None # Should not find nonexistent story
|
|
119
|
+
|
|
120
|
+
def test_get_stories_by_status(self) -> None:
|
|
121
|
+
"""get_stories_by_status should filter by status."""
|
|
122
|
+
from pennyfarthing_scripts.sprint.loader import get_stories_by_status
|
|
123
|
+
|
|
124
|
+
result = get_stories_by_status("backlog")
|
|
125
|
+
assert isinstance(result, list)
|
|
126
|
+
|
|
127
|
+
def test_get_story_field(self) -> None:
|
|
128
|
+
"""get_story_field should extract field from story."""
|
|
129
|
+
from pennyfarthing_scripts.sprint.loader import get_story_field
|
|
130
|
+
|
|
131
|
+
sprint_data = {
|
|
132
|
+
"epics": [
|
|
133
|
+
{
|
|
134
|
+
"id": "epic-63",
|
|
135
|
+
"stories": [
|
|
136
|
+
{"id": "63-1", "status": "in_progress", "points": 3}
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
assert get_story_field(sprint_data, "63-1", "status") == "in_progress"
|
|
143
|
+
assert get_story_field(sprint_data, "63-1", "points") == 3
|
|
144
|
+
assert get_story_field(sprint_data, "63-1", "nonexistent") is None
|
|
145
|
+
|
|
146
|
+
def test_load_current_sprint_alias(self) -> None:
|
|
147
|
+
"""load_current_sprint should be alias for load_sprint."""
|
|
148
|
+
from pennyfarthing_scripts.sprint.loader import (
|
|
149
|
+
load_current_sprint,
|
|
150
|
+
load_sprint,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Should be the same function
|
|
154
|
+
assert load_current_sprint == load_sprint
|
|
155
|
+
|
|
156
|
+
def test_get_sprint_info(self) -> None:
|
|
157
|
+
"""get_sprint_info should return sprint metadata."""
|
|
158
|
+
from pennyfarthing_scripts.sprint.loader import get_sprint_info
|
|
159
|
+
|
|
160
|
+
result = get_sprint_info()
|
|
161
|
+
assert isinstance(result, dict)
|
|
162
|
+
|
|
163
|
+
def test_get_epic_by_id(self) -> None:
|
|
164
|
+
"""get_epic_by_id should find epic by ID."""
|
|
165
|
+
from pennyfarthing_scripts.sprint.loader import get_epic_by_id
|
|
166
|
+
|
|
167
|
+
# Test with nonexistent ID
|
|
168
|
+
result = get_epic_by_id("nonexistent-epic")
|
|
169
|
+
assert result is None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestSprintStatus:
|
|
173
|
+
"""Tests for sprint/status.py module."""
|
|
174
|
+
|
|
175
|
+
def test_get_sprint_status_returns_dict(self) -> None:
|
|
176
|
+
"""get_sprint_status should return status information."""
|
|
177
|
+
from pennyfarthing_scripts.sprint.status import get_sprint_status
|
|
178
|
+
|
|
179
|
+
result = get_sprint_status()
|
|
180
|
+
|
|
181
|
+
assert isinstance(result, dict)
|
|
182
|
+
# Should have expected keys
|
|
183
|
+
assert "total_stories" in result or result == {}
|
|
184
|
+
assert "completed" in result or result == {}
|
|
185
|
+
|
|
186
|
+
def test_format_status(self) -> None:
|
|
187
|
+
"""format_status should return formatted string."""
|
|
188
|
+
from pennyfarthing_scripts.sprint.status import format_status
|
|
189
|
+
|
|
190
|
+
status = {
|
|
191
|
+
"total_stories": 10,
|
|
192
|
+
"completed": 3,
|
|
193
|
+
"in_progress": 2,
|
|
194
|
+
"backlog": 5,
|
|
195
|
+
}
|
|
196
|
+
result = format_status(status)
|
|
197
|
+
|
|
198
|
+
assert isinstance(result, str)
|
|
199
|
+
# Should contain numbers
|
|
200
|
+
assert "10" in result or "3" in result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestSprintWork:
|
|
204
|
+
"""Tests for sprint/work.py module."""
|
|
205
|
+
|
|
206
|
+
def test_check_story_returns_availability(self) -> None:
|
|
207
|
+
"""check_story should return story availability info."""
|
|
208
|
+
from pennyfarthing_scripts.sprint.work import check_story
|
|
209
|
+
|
|
210
|
+
result = check_story("nonexistent-99")
|
|
211
|
+
|
|
212
|
+
assert isinstance(result, dict)
|
|
213
|
+
assert "available" in result or "error" in result
|
|
214
|
+
|
|
215
|
+
def test_start_work_validates_story(self) -> None:
|
|
216
|
+
"""start_work should validate story exists."""
|
|
217
|
+
from pennyfarthing_scripts.sprint.work import start_work
|
|
218
|
+
|
|
219
|
+
result = start_work("nonexistent-99", dry_run=True)
|
|
220
|
+
|
|
221
|
+
assert isinstance(result, dict)
|
|
222
|
+
assert result.get("success") is False or "error" in result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestSprintArchive:
|
|
226
|
+
"""Tests for sprint/archive.py module."""
|
|
227
|
+
|
|
228
|
+
def test_archive_story_validates_story(self) -> None:
|
|
229
|
+
"""archive_story should validate story exists."""
|
|
230
|
+
from pennyfarthing_scripts.sprint.archive import archive_story
|
|
231
|
+
|
|
232
|
+
result = archive_story("nonexistent-99", dry_run=True)
|
|
233
|
+
|
|
234
|
+
assert isinstance(result, dict)
|
|
235
|
+
# Should fail for nonexistent story
|
|
236
|
+
assert result.get("success") is False or "error" in result
|