@theihtisham/dev-pulse 1.0.0
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/LICENSE +21 -0
- package/README.md +73 -0
- package/devpulse/__init__.py +4 -0
- package/devpulse/api/__init__.py +1 -0
- package/devpulse/api/app.py +371 -0
- package/devpulse/cli/__init__.py +1 -0
- package/devpulse/cli/dashboard.py +131 -0
- package/devpulse/cli/main.py +678 -0
- package/devpulse/cli/render.py +175 -0
- package/devpulse/core/__init__.py +34 -0
- package/devpulse/core/analytics.py +487 -0
- package/devpulse/core/config.py +77 -0
- package/devpulse/core/database.py +612 -0
- package/devpulse/core/github_client.py +281 -0
- package/devpulse/core/models.py +142 -0
- package/devpulse/core/report_generator.py +454 -0
- package/devpulse/static/.gitkeep +1 -0
- package/devpulse/templates/report.html +64 -0
- package/jest.config.js +7 -0
- package/package.json +35 -0
- package/pyproject.toml +80 -0
- package/requirements.txt +14 -0
- package/tests/__init__.py +1 -0
- package/tests/conftest.py +208 -0
- package/tests/test_analytics.py +284 -0
- package/tests/test_api.py +313 -0
- package/tests/test_cli.py +204 -0
- package/tests/test_config.py +47 -0
- package/tests/test_database.py +255 -0
- package/tests/test_models.py +107 -0
- package/tests/test_report_generator.py +173 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Tests for the FastAPI web API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from httpx import AsyncClient, ASGITransport
|
|
7
|
+
|
|
8
|
+
from devpulse.api.app import app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client(populated_db):
|
|
13
|
+
"""Provide a test client with populated database."""
|
|
14
|
+
# Override DB in the app to use test DB
|
|
15
|
+
from devpulse.api import app as app_module
|
|
16
|
+
|
|
17
|
+
original_get_db = app_module._get_db
|
|
18
|
+
original_get_engine = app_module._get_engine
|
|
19
|
+
original_get_gen = app_module._get_report_gen
|
|
20
|
+
|
|
21
|
+
app_module._get_db = lambda: populated_db
|
|
22
|
+
app_module._get_engine = lambda: __import__("devpulse.core.analytics", fromlist=["AnalyticsEngine"]).AnalyticsEngine(db=populated_db)
|
|
23
|
+
app_module._get_report_gen = lambda: __import__("devpulse.core.report_generator", fromlist=["ReportGenerator"]).ReportGenerator(db=populated_db)
|
|
24
|
+
|
|
25
|
+
transport = ASGITransport(app=app)
|
|
26
|
+
yield AsyncClient(transport=transport, base_url="http://test")
|
|
27
|
+
|
|
28
|
+
app_module._get_db = original_get_db
|
|
29
|
+
app_module._get_engine = original_get_engine
|
|
30
|
+
app_module._get_gen = original_get_gen
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestHealthCheck:
|
|
34
|
+
"""Test API health check endpoints."""
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_root(self, client):
|
|
38
|
+
async with client as c:
|
|
39
|
+
response = await c.get("/")
|
|
40
|
+
assert response.status_code == 200
|
|
41
|
+
data = response.json()
|
|
42
|
+
assert data["name"] == "DevPulse API"
|
|
43
|
+
assert data["status"] == "healthy"
|
|
44
|
+
assert "version" in data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestCommitEndpoints:
|
|
48
|
+
"""Test commit API endpoints."""
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_list_commits(self, client):
|
|
52
|
+
async with client as c:
|
|
53
|
+
response = await c.get("/api/commits")
|
|
54
|
+
assert response.status_code == 200
|
|
55
|
+
data = response.json()
|
|
56
|
+
assert len(data) == 4
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_filter_commits_by_author(self, client):
|
|
60
|
+
async with client as c:
|
|
61
|
+
response = await c.get("/api/commits", params={"author": "Alice"})
|
|
62
|
+
assert response.status_code == 200
|
|
63
|
+
data = response.json()
|
|
64
|
+
assert len(data) == 3
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_commit_heatmap(self, client):
|
|
68
|
+
async with client as c:
|
|
69
|
+
response = await c.get("/api/commits/heatmap", params={"days": 30})
|
|
70
|
+
assert response.status_code == 200
|
|
71
|
+
data = response.json()
|
|
72
|
+
assert len(data) >= 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestPullRequestEndpoints:
|
|
76
|
+
"""Test pull request API endpoints."""
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_list_prs(self, client):
|
|
80
|
+
async with client as c:
|
|
81
|
+
response = await c.get("/api/pull-requests")
|
|
82
|
+
assert response.status_code == 200
|
|
83
|
+
data = response.json()
|
|
84
|
+
assert len(data) == 3
|
|
85
|
+
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_filter_prs_by_state(self, client):
|
|
88
|
+
async with client as c:
|
|
89
|
+
response = await c.get("/api/pull-requests", params={"state": "merged"})
|
|
90
|
+
assert response.status_code == 200
|
|
91
|
+
data = response.json()
|
|
92
|
+
assert len(data) == 2
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestIssueEndpoints:
|
|
96
|
+
"""Test issue API endpoints."""
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_list_issues(self, client):
|
|
100
|
+
async with client as c:
|
|
101
|
+
response = await c.get("/api/issues")
|
|
102
|
+
assert response.status_code == 200
|
|
103
|
+
data = response.json()
|
|
104
|
+
assert len(data) == 3
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_filter_issues_by_state(self, client):
|
|
108
|
+
async with client as c:
|
|
109
|
+
response = await c.get("/api/issues", params={"state": "open"})
|
|
110
|
+
assert response.status_code == 200
|
|
111
|
+
data = response.json()
|
|
112
|
+
assert len(data) == 2
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestMetricsEndpoints:
|
|
116
|
+
"""Test metrics API endpoints."""
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_developer_metrics(self, client):
|
|
120
|
+
async with client as c:
|
|
121
|
+
response = await c.get("/api/metrics/Alice", params={"days": 30})
|
|
122
|
+
assert response.status_code == 200
|
|
123
|
+
data = response.json()
|
|
124
|
+
assert data["author"] == "Alice"
|
|
125
|
+
assert data["commits_count"] == 3
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_team_metrics(self, client):
|
|
129
|
+
async with client as c:
|
|
130
|
+
response = await c.get("/api/metrics", params={"days": 30})
|
|
131
|
+
assert response.status_code == 200
|
|
132
|
+
data = response.json()
|
|
133
|
+
assert len(data) >= 2
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestHealthEndpoint:
|
|
137
|
+
"""Test team health API endpoint."""
|
|
138
|
+
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_team_health(self, client):
|
|
141
|
+
async with client as c:
|
|
142
|
+
response = await c.get("/api/health", params={"days": 30})
|
|
143
|
+
assert response.status_code == 200
|
|
144
|
+
data = response.json()
|
|
145
|
+
assert "overall_score" in data
|
|
146
|
+
assert "burnout_risk" in data
|
|
147
|
+
assert "recommendations" in data
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestInsightsEndpoint:
|
|
151
|
+
"""Test AI insights API endpoint."""
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
async def test_get_insights(self, client):
|
|
155
|
+
async with client as c:
|
|
156
|
+
response = await c.get("/api/insights", params={"days": 30})
|
|
157
|
+
assert response.status_code == 200
|
|
158
|
+
data = response.json()
|
|
159
|
+
assert isinstance(data, list)
|
|
160
|
+
assert len(data) > 0
|
|
161
|
+
for insight in data:
|
|
162
|
+
assert "message" in insight
|
|
163
|
+
assert "recommendation" in insight
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestGoalEndpoints:
|
|
167
|
+
"""Test goals API endpoints."""
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_create_goal(self, client):
|
|
171
|
+
async with client as c:
|
|
172
|
+
response = await c.post("/api/goals", json={
|
|
173
|
+
"title": "Test goal",
|
|
174
|
+
"target_value": 10,
|
|
175
|
+
"metric": "commits_per_day",
|
|
176
|
+
})
|
|
177
|
+
assert response.status_code == 201
|
|
178
|
+
data = response.json()
|
|
179
|
+
assert data["title"] == "Test goal"
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_list_goals(self, client):
|
|
183
|
+
async with client as c:
|
|
184
|
+
# Create first
|
|
185
|
+
await c.post("/api/goals", json={
|
|
186
|
+
"title": "Goal 1",
|
|
187
|
+
"target_value": 5,
|
|
188
|
+
"metric": "test",
|
|
189
|
+
})
|
|
190
|
+
# Then list
|
|
191
|
+
response = await c.get("/api/goals")
|
|
192
|
+
assert response.status_code == 200
|
|
193
|
+
data = response.json()
|
|
194
|
+
assert len(data) >= 1
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
async def test_delete_goal(self, client):
|
|
198
|
+
async with client as c:
|
|
199
|
+
create = await c.post("/api/goals", json={
|
|
200
|
+
"title": "Delete me",
|
|
201
|
+
"target_value": 1,
|
|
202
|
+
"metric": "test",
|
|
203
|
+
})
|
|
204
|
+
goal_id = create.json()["id"]
|
|
205
|
+
response = await c.delete(f"/api/goals/{goal_id}")
|
|
206
|
+
assert response.status_code == 200
|
|
207
|
+
|
|
208
|
+
@pytest.mark.asyncio
|
|
209
|
+
async def test_delete_nonexistent_goal(self, client):
|
|
210
|
+
async with client as c:
|
|
211
|
+
response = await c.delete("/api/goals/99999")
|
|
212
|
+
assert response.status_code == 404
|
|
213
|
+
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_goal_coaching(self, client):
|
|
216
|
+
async with client as c:
|
|
217
|
+
response = await c.get("/api/goals/coach")
|
|
218
|
+
assert response.status_code == 200
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestSprintEndpoints:
|
|
222
|
+
"""Test sprint API endpoints."""
|
|
223
|
+
|
|
224
|
+
@pytest.mark.asyncio
|
|
225
|
+
async def test_create_sprint_snapshot(self, client):
|
|
226
|
+
async with client as c:
|
|
227
|
+
response = await c.post("/api/sprints/snapshot", json={
|
|
228
|
+
"sprint_name": "Sprint 1",
|
|
229
|
+
"total_points": 30,
|
|
230
|
+
"completed_points": 10,
|
|
231
|
+
"added_points": 0,
|
|
232
|
+
})
|
|
233
|
+
assert response.status_code == 201
|
|
234
|
+
|
|
235
|
+
@pytest.mark.asyncio
|
|
236
|
+
async def test_sprint_burndown(self, client):
|
|
237
|
+
async with client as c:
|
|
238
|
+
# Create snapshot first
|
|
239
|
+
await c.post("/api/sprints/snapshot", json={
|
|
240
|
+
"sprint_name": "Sprint B",
|
|
241
|
+
"total_points": 20,
|
|
242
|
+
"completed_points": 5,
|
|
243
|
+
})
|
|
244
|
+
response = await c.get("/api/sprints/Sprint B/burndown")
|
|
245
|
+
assert response.status_code == 200
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestQualityEndpoints:
|
|
249
|
+
"""Test code quality API endpoints."""
|
|
250
|
+
|
|
251
|
+
@pytest.mark.asyncio
|
|
252
|
+
async def test_create_quality_snapshot(self, client):
|
|
253
|
+
async with client as c:
|
|
254
|
+
response = await c.post("/api/quality/snapshot", json={
|
|
255
|
+
"repo": "testorg/testrepo",
|
|
256
|
+
"test_coverage": 0.85,
|
|
257
|
+
"open_bugs": 3,
|
|
258
|
+
"tech_debt_score": 4.2,
|
|
259
|
+
})
|
|
260
|
+
assert response.status_code == 201
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_quality_trends(self, client):
|
|
264
|
+
async with client as c:
|
|
265
|
+
response = await c.get("/api/quality", params={"days": 90})
|
|
266
|
+
assert response.status_code == 200
|
|
267
|
+
|
|
268
|
+
@pytest.mark.asyncio
|
|
269
|
+
async def test_quality_score(self, client):
|
|
270
|
+
async with client as c:
|
|
271
|
+
response = await c.get("/api/quality/score", params={"repo": "testorg/testrepo", "days": 30})
|
|
272
|
+
assert response.status_code == 200
|
|
273
|
+
data = response.json()
|
|
274
|
+
assert data["repo"] == "testorg/testrepo"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestReportEndpoints:
|
|
278
|
+
"""Test report generation API endpoints."""
|
|
279
|
+
|
|
280
|
+
@pytest.mark.asyncio
|
|
281
|
+
async def test_daily_report_json(self, client):
|
|
282
|
+
async with client as c:
|
|
283
|
+
response = await c.get("/api/reports/daily", params={"date": "2026-04-01"})
|
|
284
|
+
assert response.status_code == 200
|
|
285
|
+
data = response.json()
|
|
286
|
+
assert data["date"] == "2026-04-01"
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_daily_report_markdown(self, client):
|
|
290
|
+
async with client as c:
|
|
291
|
+
response = await c.get("/api/reports/daily", params={"date": "2026-04-01", "format": "markdown"})
|
|
292
|
+
assert response.status_code == 200
|
|
293
|
+
data = response.json()
|
|
294
|
+
assert "markdown" in data
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_daily_report_html(self, client):
|
|
298
|
+
async with client as c:
|
|
299
|
+
response = await c.get("/api/reports/daily", params={"date": "2026-04-01", "format": "html"})
|
|
300
|
+
assert response.status_code == 200
|
|
301
|
+
assert "<!DOCTYPE html>" in response.text
|
|
302
|
+
|
|
303
|
+
@pytest.mark.asyncio
|
|
304
|
+
async def test_weekly_report(self, client):
|
|
305
|
+
async with client as c:
|
|
306
|
+
response = await c.get("/api/reports/weekly", params={"week_start": "2026-03-30"})
|
|
307
|
+
assert response.status_code == 200
|
|
308
|
+
|
|
309
|
+
@pytest.mark.asyncio
|
|
310
|
+
async def test_monthly_report(self, client):
|
|
311
|
+
async with client as c:
|
|
312
|
+
response = await c.get("/api/reports/monthly", params={"year": 2026, "month": 4})
|
|
313
|
+
assert response.status_code == 200
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Tests for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from click.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from devpulse.cli.main import cli
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def runner():
|
|
14
|
+
return CliRunner()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestCLIBase:
|
|
18
|
+
"""Test CLI base commands."""
|
|
19
|
+
|
|
20
|
+
def test_version(self, runner):
|
|
21
|
+
result = runner.invoke(cli, ["--version"])
|
|
22
|
+
assert result.exit_code == 0
|
|
23
|
+
assert "1.0.0" in result.output
|
|
24
|
+
|
|
25
|
+
def test_help(self, runner):
|
|
26
|
+
result = runner.invoke(cli, ["--help"])
|
|
27
|
+
assert result.exit_code == 0
|
|
28
|
+
assert "DevPulse" in result.output
|
|
29
|
+
assert "sync" in result.output
|
|
30
|
+
assert "daily" in result.output
|
|
31
|
+
assert "metrics" in result.output
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestSyncCommand:
|
|
35
|
+
"""Test sync command (without actual GitHub)."""
|
|
36
|
+
|
|
37
|
+
def test_sync_no_token(self, runner, monkeypatch):
|
|
38
|
+
monkeypatch.delenv("DEVPULSE_GITHUB_TOKEN", raising=False)
|
|
39
|
+
monkeypatch.setenv("DEVPULSE_GITHUB_TOKEN", "")
|
|
40
|
+
from devpulse.core.config import reset_settings
|
|
41
|
+
reset_settings()
|
|
42
|
+
|
|
43
|
+
result = runner.invoke(cli, ["sync"])
|
|
44
|
+
assert result.exit_code != 0 or "DEVPULSE_GITHUB_TOKEN" in result.output
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestReportCommands:
|
|
48
|
+
"""Test report generation CLI commands."""
|
|
49
|
+
|
|
50
|
+
def test_daily_report_terminal(self, runner, populated_db):
|
|
51
|
+
with patch("devpulse.core.report_generator.ReportGenerator", return_value=MagicMock(
|
|
52
|
+
daily_report=MagicMock(return_value={
|
|
53
|
+
"date": "2026-04-01",
|
|
54
|
+
"author": "all",
|
|
55
|
+
"commits": 2,
|
|
56
|
+
"prs_opened": 0,
|
|
57
|
+
"prs_merged": 0,
|
|
58
|
+
"issues_opened": 0,
|
|
59
|
+
"issues_closed": 0,
|
|
60
|
+
"reviews_given": 0,
|
|
61
|
+
"lines_changed": 80,
|
|
62
|
+
"summary": "2 commit(s)",
|
|
63
|
+
"commit_details": [],
|
|
64
|
+
}),
|
|
65
|
+
)):
|
|
66
|
+
result = runner.invoke(cli, ["daily", "--date", "2026-04-01"])
|
|
67
|
+
# Should not crash (may print to console)
|
|
68
|
+
|
|
69
|
+
def test_daily_report_json(self, runner, populated_db):
|
|
70
|
+
with patch("devpulse.core.report_generator.ReportGenerator", return_value=MagicMock(
|
|
71
|
+
daily_report=MagicMock(return_value={
|
|
72
|
+
"date": "2026-04-01",
|
|
73
|
+
"author": "all",
|
|
74
|
+
"commits": 2,
|
|
75
|
+
"prs_opened": 0,
|
|
76
|
+
"prs_merged": 0,
|
|
77
|
+
"issues_opened": 0,
|
|
78
|
+
"issues_closed": 0,
|
|
79
|
+
"reviews_given": 0,
|
|
80
|
+
"lines_changed": 80,
|
|
81
|
+
"summary": "2 commits",
|
|
82
|
+
"commit_details": [],
|
|
83
|
+
}),
|
|
84
|
+
to_json=MagicMock(return_value='{"date": "2026-04-01"}'),
|
|
85
|
+
)):
|
|
86
|
+
result = runner.invoke(cli, ["daily", "--format", "json"])
|
|
87
|
+
# Should output JSON
|
|
88
|
+
|
|
89
|
+
def test_weekly_report_help(self, runner):
|
|
90
|
+
result = runner.invoke(cli, ["weekly", "--help"])
|
|
91
|
+
assert result.exit_code == 0
|
|
92
|
+
assert "--week-start" in result.output
|
|
93
|
+
|
|
94
|
+
def test_monthly_report_help(self, runner):
|
|
95
|
+
result = runner.invoke(cli, ["monthly", "--help"])
|
|
96
|
+
assert result.exit_code == 0
|
|
97
|
+
assert "--year" in result.output
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestMetricsCommand:
|
|
101
|
+
"""Test metrics CLI commands."""
|
|
102
|
+
|
|
103
|
+
def test_metrics_help(self, runner):
|
|
104
|
+
result = runner.invoke(cli, ["metrics", "--help"])
|
|
105
|
+
assert result.exit_code == 0
|
|
106
|
+
assert "--author" in result.output
|
|
107
|
+
assert "--days" in result.output
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestInsightsCommand:
|
|
111
|
+
"""Test insights CLI command."""
|
|
112
|
+
|
|
113
|
+
def test_insights_help(self, runner):
|
|
114
|
+
result = runner.invoke(cli, ["insights", "--help"])
|
|
115
|
+
assert result.exit_code == 0
|
|
116
|
+
assert "--days" in result.output
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestHealthCommand:
|
|
120
|
+
"""Test health CLI command."""
|
|
121
|
+
|
|
122
|
+
def test_health_help(self, runner):
|
|
123
|
+
result = runner.invoke(cli, ["health", "--help"])
|
|
124
|
+
assert result.exit_code == 0
|
|
125
|
+
assert "--days" in result.output
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestHeatmapCommand:
|
|
129
|
+
"""Test heatmap CLI command."""
|
|
130
|
+
|
|
131
|
+
def test_heatmap_help(self, runner):
|
|
132
|
+
result = runner.invoke(cli, ["heatmap", "--help"])
|
|
133
|
+
assert result.exit_code == 0
|
|
134
|
+
assert "--author" in result.output
|
|
135
|
+
assert "--days" in result.output
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestGoalsCommands:
|
|
139
|
+
"""Test goals CLI commands."""
|
|
140
|
+
|
|
141
|
+
def test_goals_help(self, runner):
|
|
142
|
+
result = runner.invoke(cli, ["goals", "--help"])
|
|
143
|
+
assert result.exit_code == 0
|
|
144
|
+
|
|
145
|
+
def test_goals_add_help(self, runner):
|
|
146
|
+
result = runner.invoke(cli, ["goals", "add", "--help"])
|
|
147
|
+
assert result.exit_code == 0
|
|
148
|
+
assert "--title" in result.output
|
|
149
|
+
|
|
150
|
+
def test_goals_list_help(self, runner):
|
|
151
|
+
result = runner.invoke(cli, ["goals", "list", "--help"])
|
|
152
|
+
assert result.exit_code == 0
|
|
153
|
+
|
|
154
|
+
def test_goals_coach_help(self, runner):
|
|
155
|
+
result = runner.invoke(cli, ["goals", "coach", "--help"])
|
|
156
|
+
assert result.exit_code == 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestSprintCommands:
|
|
160
|
+
"""Test sprint CLI commands."""
|
|
161
|
+
|
|
162
|
+
def test_sprint_help(self, runner):
|
|
163
|
+
result = runner.invoke(cli, ["sprint", "--help"])
|
|
164
|
+
assert result.exit_code == 0
|
|
165
|
+
|
|
166
|
+
def test_sprint_snapshot_help(self, runner):
|
|
167
|
+
result = runner.invoke(cli, ["sprint", "snapshot", "--help"])
|
|
168
|
+
assert result.exit_code == 0
|
|
169
|
+
assert "--name" in result.output
|
|
170
|
+
|
|
171
|
+
def test_sprint_burndown_help(self, runner):
|
|
172
|
+
result = runner.invoke(cli, ["sprint", "burndown", "--help"])
|
|
173
|
+
assert result.exit_code == 0
|
|
174
|
+
|
|
175
|
+
def test_sprint_predict_help(self, runner):
|
|
176
|
+
result = runner.invoke(cli, ["sprint", "predict", "--help"])
|
|
177
|
+
assert result.exit_code == 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestQualityCommand:
|
|
181
|
+
"""Test quality CLI command."""
|
|
182
|
+
|
|
183
|
+
def test_quality_help(self, runner):
|
|
184
|
+
result = runner.invoke(cli, ["quality", "--help"])
|
|
185
|
+
assert result.exit_code == 0
|
|
186
|
+
assert "--repo" in result.output
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestServeCommand:
|
|
190
|
+
"""Test serve CLI command."""
|
|
191
|
+
|
|
192
|
+
def test_serve_help(self, runner):
|
|
193
|
+
result = runner.invoke(cli, ["serve", "--help"])
|
|
194
|
+
assert result.exit_code == 0
|
|
195
|
+
assert "--port" in result.output
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestDashboardCommand:
|
|
199
|
+
"""Test dashboard CLI command."""
|
|
200
|
+
|
|
201
|
+
def test_dashboard_help(self, runner):
|
|
202
|
+
result = runner.invoke(cli, ["dashboard", "--help"])
|
|
203
|
+
assert result.exit_code == 0
|
|
204
|
+
assert "--author" in result.output
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Tests for configuration management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from devpulse.core.config import Settings, get_settings, reset_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestSettings:
|
|
10
|
+
"""Test settings loading."""
|
|
11
|
+
|
|
12
|
+
def test_default_settings(self):
|
|
13
|
+
reset_settings()
|
|
14
|
+
# Create Settings directly to check defaults (env vars are set by conftest)
|
|
15
|
+
s = Settings(github_token="", github_username="")
|
|
16
|
+
assert s.api_port == 8742
|
|
17
|
+
assert s.log_level == "INFO"
|
|
18
|
+
assert s.redact_sensitive is True
|
|
19
|
+
|
|
20
|
+
def test_env_override(self, monkeypatch):
|
|
21
|
+
reset_settings()
|
|
22
|
+
monkeypatch.setenv("DEVPULSE_GITHUB_TOKEN", "ghp_test123")
|
|
23
|
+
monkeypatch.setenv("DEVPULSE_API_PORT", "9999")
|
|
24
|
+
settings = Settings()
|
|
25
|
+
assert settings.github_token == "ghp_test123"
|
|
26
|
+
assert settings.api_port == 9999
|
|
27
|
+
|
|
28
|
+
def test_get_settings_caches(self):
|
|
29
|
+
reset_settings()
|
|
30
|
+
s1 = get_settings()
|
|
31
|
+
s2 = get_settings()
|
|
32
|
+
assert s1 is s2
|
|
33
|
+
|
|
34
|
+
def test_reset_settings(self):
|
|
35
|
+
reset_settings()
|
|
36
|
+
s1 = get_settings()
|
|
37
|
+
reset_settings()
|
|
38
|
+
s2 = get_settings()
|
|
39
|
+
assert s1 is not s2
|
|
40
|
+
|
|
41
|
+
def test_database_path_default(self):
|
|
42
|
+
settings = Settings()
|
|
43
|
+
assert "devpulse.db" in settings.database_path
|
|
44
|
+
|
|
45
|
+
def test_burnout_threshold(self):
|
|
46
|
+
settings = Settings()
|
|
47
|
+
assert 0 < settings.burnout_threshold < 1
|