main
py 148 lines 5.93 KB
Raw
1 """Validate the editorial style guide contains all required safety sections.
2
3 This test ensures the Signal Check editorial style guide maintains required
4 safety boundaries, disclosure requirements, and structural constraints that
5 prevent unsafe content generation.
6 """
7
8 import re
9 from pathlib import Path
10
11 import pytest
12
13 GUIDE_PATH = Path(__file__).resolve().parent.parent / "docs" / "editorial-style-guide.md"
14 SEGMENT_TABLE_PATTERN = re.compile(
15 r"^\|\s*\d+\s*\|\s*\*\*(?P<segment>[^*]+)\*\*\s*\|", re.MULTILINE
16 )
17
18
19 @pytest.fixture
20 def guide_content() -> str:
21 assert GUIDE_PATH.exists(), f"Editorial style guide not found at {GUIDE_PATH}"
22 return GUIDE_PATH.read_text(encoding="utf-8")
23
24
25 class TestEditorialStyleGuideStructure:
26 """Verify the guide contains all required structural elements."""
27
28 def test_guide_exists(self):
29 assert GUIDE_PATH.exists()
30
31 def test_has_host_roles(self, guide_content: str):
32 assert "Host A" in guide_content
33 assert "Host B" in guide_content
34 assert "Curator" in guide_content
35 assert "Skeptic" in guide_content
36
37 def test_has_all_segments_in_order(self, guide_content: str):
38 required_segments = [
39 "Cold Open",
40 "The Signal",
41 "The Noise Check",
42 "The Gap",
43 "Receipts Round",
44 "Week Ahead",
45 "Outro",
46 ]
47 for segment in required_segments:
48 assert segment in guide_content, f"Missing segment: {segment}"
49 # Verify locked order (segments must not be reordered)
50 positions = [guide_content.index(s) for s in required_segments]
51 assert positions == sorted(positions), "Segments are not in the required locked order"
52
53 def test_has_word_count_target(self, guide_content: str):
54 assert "1,200" in guide_content
55 assert "1,700" in guide_content
56
57
58 class TestEditorialStyleGuideSafety:
59 """Verify safety boundaries are documented."""
60
61 def test_ai_disclosure_requirement(self, guide_content: str):
62 lower = guide_content.lower()
63 assert "first 60 seconds" in lower, "Must require AI disclosure in first 60 seconds"
64 assert "ai-generated voice" in lower or "ai_generated" in lower, (
65 "Must mention AI-generated voice disclosure"
66 )
67 # Disclosure should also be required in the outro
68 assert "outro" in lower
69
70 def test_prohibited_content_section(self, guide_content: str):
71 required_prohibitions = [
72 "Real-person mimicry",
73 "Copied expression",
74 "Unsupported facts",
75 "Defamatory motive",
76 "Fake sponsorship",
77 "Individual-targeting humor",
78 "Financial advice",
79 "Security vulnerability disclosure",
80 ]
81 for prohibition in required_prohibitions:
82 assert prohibition in guide_content, f"Missing prohibition: {prohibition}"
83
84 def test_corrections_path(self, guide_content: str):
85 assert "corrections" in guide_content.lower()
86 # Must link to the specific SquadScope issues page, not just any github.com URL
87 assert "github.com/jmservera/SquadScope/issues" in guide_content, (
88 "Corrections path must link to the SquadScope issues page"
89 )
90
91 def test_claim_ledger_requirement(self, guide_content: str):
92 assert "claim ledger" in guide_content.lower()
93 assert "must map" in guide_content.lower() or "MUST map" in guide_content
94
95 def test_distinctiveness_requirements(self, guide_content: str):
96 lower = guide_content.lower()
97 assert "hard fork" in lower, "Must reference Hard Fork as distinctiveness example"
98 assert "must not replicate" in lower, (
99 "Must state the requirement not to replicate other podcasts"
100 )
101
102 def test_sponsorship_guardrails(self, guide_content: str):
103 assert "FTC" in guide_content or "sponsorship" in guide_content.lower()
104 assert "brought to you by" in guide_content.lower()
105
106
107 class TestPodcastConfigAlignedWithGuide:
108 """Verify config/podcast.json aligns with the editorial style guide."""
109
110 @pytest.fixture
111 def podcast_config(self) -> dict:
112 import json
113
114 config_path = Path(__file__).resolve().parent.parent / "config" / "podcast.json"
115 assert config_path.exists(), "config/podcast.json not found"
116 return json.loads(config_path.read_text(encoding="utf-8"))
117
118 def test_config_references_style_guide(self, podcast_config: dict):
119 assert "editorial_style_guide" in podcast_config
120 guide_path = (
121 Path(__file__).resolve().parent.parent / podcast_config["editorial_style_guide"]
122 )
123 assert guide_path.exists(), (
124 f"Style guide path {podcast_config['editorial_style_guide']} does not exist"
125 )
126
127 def test_segment_order_matches_guide(self, podcast_config: dict, guide_content: str):
128 config_segments = podcast_config["script_directions"]["episode_style"]["segment_order"]
129 guide_segments = [match.strip() for match in SEGMENT_TABLE_PATTERN.findall(guide_content)]
130
131 assert guide_segments, "Could not extract locked segment order from style guide"
132 assert len(guide_segments) == len(set(guide_segments)), (
133 "Style guide contains duplicate segments"
134 )
135 assert config_segments == guide_segments, (
136 "Podcast config segment_order must exactly match the style guide's locked segment order "
137 f"(guide={guide_segments}, config={config_segments})"
138 )
139
140 def test_ai_disclosure_in_config(self, podcast_config: dict):
141 opening = podcast_config["script_directions"]["opening_cues"]
142 assert "ai_disclosure" in opening
143 assert "first 60 seconds" in opening["ai_disclosure"].lower()
144
145 def test_word_count_target_in_config(self, podcast_config: dict):
146 fmt = podcast_config["script_directions"]["episode_style"]["format"]
147 assert "1200" in fmt or "1,200" in fmt
148 assert "1700" in fmt or "1,700" in fmt