Skip to content

Commit 905d6be

Browse files
committed
improve test coverage from 69% to 85%+ with targeted unit tests
- Fix syntax errors in agentic modules (unparenthesized except clauses) - Add tests for insights helpers (_score_bucket, _flatten_tags, _parse_iso, _collect_impl_tags) - Add tests for SEO helpers (_lastmod, _build_sitemap_xml, BOT_HTML_TEMPLATE) - Add tests for plots filter helpers (all 15 helper functions) - Add tests for API schemas (all 8 Pydantic models) - Add tests for config resolve_model (CLI/tier mapping) - Add tests for database models (Spec, Library, Impl) - Add tests for database repositories (CRUD, upsert, queries) - Add tests for sync_to_postgres helpers (_validate_quality_score, _validate_spec_id, _parse_markdown_section) - Add extended analytics tests (all 24+ platform detection patterns) https://claude.ai/code/session_01KhAhJKpEoqCzmWzcALSfW6
1 parent 2b4a96e commit 905d6be

12 files changed

Lines changed: 1735 additions & 5 deletions

File tree

agentic/workflows/modules/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
144144
return [target_type.model_validate(item) for item in parsed]
145145
return target_type.model_validate(parsed)
146146
return parsed
147-
except json.JSONDecodeError, ValueError:
147+
except (json.JSONDecodeError, ValueError):
148148
pass
149149

150150
# Strategy 2: Strip markdown code fences
@@ -162,7 +162,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
162162
return [target_type.model_validate(item) for item in parsed]
163163
return target_type.model_validate(parsed)
164164
return parsed
165-
except json.JSONDecodeError, ValueError:
165+
except (json.JSONDecodeError, ValueError):
166166
pass
167167

168168
# Strategy 3: Find first JSON array or object in output
@@ -182,7 +182,7 @@ def parse_json(output: str, target_type: Type[T] = None) -> Any:
182182
return [target_type.model_validate(item) for item in parsed]
183183
return target_type.model_validate(parsed)
184184
return parsed
185-
except json.JSONDecodeError, ValueError:
185+
except (json.JSONDecodeError, ValueError):
186186
continue
187187

188188
raise json.JSONDecodeError("No valid JSON found in output", output, 0)

agentic/workflows/modules/orchestrator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ def extract_run_id(stdout: str) -> str | None:
4545
try:
4646
data = json.loads(stdout.strip())
4747
return data.get("run_id")
48-
except json.JSONDecodeError, ValueError:
48+
except (json.JSONDecodeError, ValueError):
4949
return None

agentic/workflows/modules/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def from_stdin(cls) -> Optional["WorkflowState"]:
172172
state = cls(run_id=run_id, prompt=data.get("prompt", ""))
173173
state.data = data
174174
return state
175-
except json.JSONDecodeError, EOFError:
175+
except (json.JSONDecodeError, EOFError):
176176
return None
177177

178178
def to_stdout(self) -> None:
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
Extended tests for analytics module.
3+
4+
Covers edge cases and additional platform patterns.
5+
"""
6+
7+
import pytest
8+
9+
from api.analytics import (
10+
PLATFORM_PATTERNS,
11+
_detect_whatsapp_variant,
12+
detect_platform,
13+
)
14+
15+
16+
class TestDetectWhatsappVariant:
17+
"""Tests for _detect_whatsapp_variant edge cases."""
18+
19+
def test_no_whatsapp(self) -> None:
20+
assert _detect_whatsapp_variant("Mozilla/5.0") is None
21+
22+
def test_real_whatsapp_ios(self) -> None:
23+
assert _detect_whatsapp_variant("WhatsApp/2.23.18.78 i") == "whatsapp"
24+
25+
def test_real_whatsapp_android(self) -> None:
26+
assert _detect_whatsapp_variant("WhatsApp/2.21.22.23 A") == "whatsapp"
27+
28+
def test_signal_spoofed_simple(self) -> None:
29+
assert _detect_whatsapp_variant("WhatsApp") == "whatsapp-lite"
30+
31+
def test_signal_spoofed_short_version(self) -> None:
32+
assert _detect_whatsapp_variant("WhatsApp/2") == "whatsapp-lite"
33+
34+
def test_signal_spoofed_two_part_version(self) -> None:
35+
assert _detect_whatsapp_variant("WhatsApp/2.23") == "whatsapp-lite"
36+
37+
def test_case_insensitive(self) -> None:
38+
assert _detect_whatsapp_variant("whatsapp/2.23.18.78 i") == "whatsapp"
39+
40+
41+
class TestDetectPlatformExtended:
42+
"""Additional platform detection tests for full coverage."""
43+
44+
def test_slack(self) -> None:
45+
assert detect_platform("Slackbot 1.0 (+https://api.slack.com/robots)") == "slack"
46+
47+
def test_discord(self) -> None:
48+
assert detect_platform("Mozilla/5.0 (compatible; Discordbot/2.0)") == "discord"
49+
50+
def test_telegram(self) -> None:
51+
assert detect_platform("TelegramBot/1.0") == "telegram"
52+
53+
def test_linkedin(self) -> None:
54+
assert detect_platform("LinkedInBot/1.0") == "linkedin"
55+
56+
def test_pinterest(self) -> None:
57+
assert detect_platform("Pinterestbot/1.0") == "pinterest"
58+
59+
def test_reddit(self) -> None:
60+
assert detect_platform("redditbot/1.0") == "reddit"
61+
62+
def test_google(self) -> None:
63+
assert detect_platform("Mozilla/5.0 (compatible; Googlebot/2.1)") == "google"
64+
65+
def test_bing(self) -> None:
66+
assert detect_platform("Mozilla/5.0 (compatible; bingbot/2.0)") == "bing"
67+
68+
def test_mastodon(self) -> None:
69+
assert detect_platform("http.rb/5.0.0 (Mastodon/4.0; +https://instance.social/)") == "mastodon"
70+
71+
def test_viber(self) -> None:
72+
assert detect_platform("Viber/13.0") == "viber"
73+
74+
def test_skype(self) -> None:
75+
assert detect_platform("SkypeUriPreview") == "skype"
76+
77+
def test_teams(self) -> None:
78+
assert detect_platform("Mozilla/5.0 Microsoft Teams") == "teams"
79+
80+
def test_snapchat(self) -> None:
81+
assert detect_platform("Snapchat/10.0") == "snapchat"
82+
83+
def test_yandex(self) -> None:
84+
assert detect_platform("Mozilla/5.0 (compatible; YandexBot/3.0)") == "yandex"
85+
86+
def test_duckduckgo(self) -> None:
87+
assert detect_platform("DuckDuckBot/1.0") == "duckduckgo"
88+
89+
def test_baidu(self) -> None:
90+
assert detect_platform("Mozilla/5.0 (compatible; Baiduspider/2.0)") == "baidu"
91+
92+
def test_apple(self) -> None:
93+
assert detect_platform("Applebot/0.1") == "apple"
94+
95+
def test_embedly(self) -> None:
96+
assert detect_platform("Embedly/0.2") == "embedly"
97+
98+
def test_quora(self) -> None:
99+
assert detect_platform("Quora Link Preview/1.0") == "quora"
100+
101+
def test_tumblr(self) -> None:
102+
assert detect_platform("Tumblr/14.0") == "tumblr"
103+
104+
def test_unknown_agent(self) -> None:
105+
assert detect_platform("Some Random Bot/1.0") == "unknown"
106+
107+
def test_empty_agent(self) -> None:
108+
assert detect_platform("") == "unknown"
109+
110+
def test_whatsapp_takes_priority(self) -> None:
111+
"""WhatsApp detection happens before general pattern matching."""
112+
assert detect_platform("WhatsApp/2.23.18.78 i") == "whatsapp"
113+
114+
def test_platform_patterns_not_empty(self) -> None:
115+
"""Ensure we have a comprehensive set of platform patterns."""
116+
assert len(PLATFORM_PATTERNS) >= 20
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""
2+
Tests for insights helper functions.
3+
4+
Directly tests the pure helper functions in api/routers/insights.py
5+
that don't require database or HTTP setup.
6+
"""
7+
8+
from datetime import datetime, timezone
9+
10+
import pytest
11+
12+
from api.routers.insights import (
13+
_collect_impl_tags,
14+
_flatten_tags,
15+
_parse_iso,
16+
_score_bucket,
17+
)
18+
19+
20+
class TestScoreBucket:
21+
"""Tests for _score_bucket mapping."""
22+
23+
def test_minimum_score(self) -> None:
24+
assert _score_bucket(50) == "50-55"
25+
26+
def test_maximum_score(self) -> None:
27+
assert _score_bucket(100) == "95-100"
28+
29+
def test_middle_scores(self) -> None:
30+
assert _score_bucket(72) == "70-75"
31+
assert _score_bucket(85) == "85-90"
32+
assert _score_bucket(90) == "90-95"
33+
34+
def test_boundary_at_55(self) -> None:
35+
assert _score_bucket(55) == "55-60"
36+
37+
def test_below_50_clamped(self) -> None:
38+
assert _score_bucket(30) == "50-55"
39+
40+
def test_above_100_clamped(self) -> None:
41+
assert _score_bucket(110) == "95-100"
42+
43+
def test_exact_boundary(self) -> None:
44+
assert _score_bucket(75) == "75-80"
45+
assert _score_bucket(80) == "80-85"
46+
47+
def test_fractional_score(self) -> None:
48+
assert _score_bucket(92.5) == "90-95"
49+
assert _score_bucket(87.9) == "85-90"
50+
51+
52+
class TestFlattenTags:
53+
"""Tests for _flatten_tags."""
54+
55+
def test_none_tags(self) -> None:
56+
assert _flatten_tags(None) == set()
57+
58+
def test_empty_dict(self) -> None:
59+
assert _flatten_tags({}) == set()
60+
61+
def test_single_category(self) -> None:
62+
tags = {"plot_type": ["scatter"]}
63+
assert _flatten_tags(tags) == {"plot_type:scatter"}
64+
65+
def test_multiple_categories(self) -> None:
66+
tags = {"plot_type": ["scatter", "line"], "domain": ["statistics"]}
67+
result = _flatten_tags(tags)
68+
assert result == {"plot_type:scatter", "plot_type:line", "domain:statistics"}
69+
70+
def test_non_list_values_skipped(self) -> None:
71+
tags = {"plot_type": ["scatter"], "invalid": "not-a-list"}
72+
result = _flatten_tags(tags)
73+
assert result == {"plot_type:scatter"}
74+
75+
def test_empty_list(self) -> None:
76+
tags = {"plot_type": []}
77+
assert _flatten_tags(tags) == set()
78+
79+
80+
class TestParseIso:
81+
"""Tests for _parse_iso."""
82+
83+
def test_none_input(self) -> None:
84+
assert _parse_iso(None) is None
85+
86+
def test_empty_string(self) -> None:
87+
assert _parse_iso("") is None
88+
89+
def test_valid_iso_with_z(self) -> None:
90+
result = _parse_iso("2025-01-15T10:30:00Z")
91+
assert result is not None
92+
assert result.year == 2025
93+
assert result.month == 1
94+
assert result.tzinfo is not None
95+
96+
def test_valid_iso_with_offset(self) -> None:
97+
result = _parse_iso("2025-01-15T10:30:00+02:00")
98+
assert result is not None
99+
assert result.tzinfo is not None
100+
101+
def test_naive_datetime_gets_utc(self) -> None:
102+
result = _parse_iso("2025-01-15T10:30:00")
103+
assert result is not None
104+
assert result.tzinfo == timezone.utc
105+
106+
def test_invalid_string(self) -> None:
107+
assert _parse_iso("not-a-date") is None
108+
109+
def test_date_only(self) -> None:
110+
result = _parse_iso("2025-01-15")
111+
assert result is not None
112+
assert result.year == 2025
113+
114+
115+
class TestCollectImplTags:
116+
"""Tests for _collect_impl_tags."""
117+
118+
def test_spec_with_no_tags(self) -> None:
119+
from unittest.mock import MagicMock
120+
121+
spec = MagicMock()
122+
spec.tags = None
123+
spec.impls = []
124+
result = _collect_impl_tags(spec)
125+
assert result == set()
126+
127+
def test_spec_with_tags_and_impl_tags(self) -> None:
128+
from unittest.mock import MagicMock
129+
130+
impl = MagicMock()
131+
impl.library_id = "matplotlib"
132+
impl.impl_tags = {"techniques": ["annotations"]}
133+
134+
spec = MagicMock()
135+
spec.tags = {"plot_type": ["scatter"]}
136+
spec.impls = [impl]
137+
138+
result = _collect_impl_tags(spec)
139+
assert "plot_type:scatter" in result
140+
assert "techniques:annotations" in result
141+
142+
def test_filter_by_library(self) -> None:
143+
from unittest.mock import MagicMock
144+
145+
impl1 = MagicMock()
146+
impl1.library_id = "matplotlib"
147+
impl1.impl_tags = {"techniques": ["annotations"]}
148+
149+
impl2 = MagicMock()
150+
impl2.library_id = "seaborn"
151+
impl2.impl_tags = {"techniques": ["regression"]}
152+
153+
spec = MagicMock()
154+
spec.tags = {"plot_type": ["scatter"]}
155+
spec.impls = [impl1, impl2]
156+
157+
result = _collect_impl_tags(spec, library="matplotlib")
158+
assert "techniques:annotations" in result
159+
assert "techniques:regression" not in result
160+
161+
def test_impl_tags_none(self) -> None:
162+
from unittest.mock import MagicMock
163+
164+
impl = MagicMock()
165+
impl.library_id = "matplotlib"
166+
impl.impl_tags = None
167+
168+
spec = MagicMock()
169+
spec.tags = {"plot_type": ["scatter"]}
170+
spec.impls = [impl]
171+
172+
result = _collect_impl_tags(spec)
173+
assert result == {"plot_type:scatter"}
174+
175+
def test_impl_tags_not_dict(self) -> None:
176+
from unittest.mock import MagicMock
177+
178+
impl = MagicMock()
179+
impl.library_id = "matplotlib"
180+
impl.impl_tags = "not-a-dict"
181+
182+
spec = MagicMock()
183+
spec.tags = {}
184+
spec.impls = [impl]
185+
186+
result = _collect_impl_tags(spec)
187+
assert result == set()

0 commit comments

Comments
 (0)