forked from ChelseaKR/sprout
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_toxicity.py
More file actions
262 lines (220 loc) · 8.08 KB
/
Copy pathtest_toxicity.py
File metadata and controls
262 lines (220 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""Toxicity table tests (EXP-09): schema invariants, coverage, table-vs-prose consistency."""
from __future__ import annotations
from pathlib import Path
import pytest
from sprout.models import Document
from sprout.toxicity import (
ToxicityRow,
check_consistency,
coverage_report,
load_toxicity_table,
species_animal_pairs,
)
_TOXIC_ROW = """rows:
- species_slug: aloe
species_name: Aloe vera (Aloe barbadensis)
animal: cat
toxic: true
principle: saponins and anthraquinones
severity_class: mild_moderate
source_name: Synthetic Plant-Care Notes
url: https://example.invalid/aloe
license: CC0-1.0
fetch_date: "2026-05-01"
synthetic: true
"""
_NONTOXIC_ROW = """rows:
- species_slug: boston-fern
species_name: Boston fern (Nephrolepis exaltata)
animal: cat
toxic: false
principle: not listed as toxic by the cited source (non-toxic is not the same as safe)
severity_class: not_applicable
source_name: Synthetic Plant-Care Notes
url: https://example.invalid/boston-fern
license: CC0-1.0
fetch_date: "2026-05-01"
synthetic: true
"""
def _doc(source: str, text: str, language: str = "en") -> Document:
return Document(
doc_id=source,
source=source,
title=source,
language=language,
text=text,
source_name="Synthetic Plant-Care Notes",
url=f"https://example.invalid/{source}",
license="CC0-1.0",
fetch_date="2026-05-01",
topic="care",
)
def test_load_real_corpus_toxicity_table() -> None:
path = Path(__file__).resolve().parent.parent / "corpus" / "toxicity.yaml"
rows = load_toxicity_table(path)
assert len(rows) >= 30
assert all(row.synthetic for row in rows)
assert all("example.invalid" in row.url for row in rows)
# Every species covers at least cat and dog.
pairs = species_animal_pairs(rows)
slugs = {slug for slug, _ in pairs}
for slug in slugs:
assert (slug, "cat") in pairs
assert (slug, "dog") in pairs
def test_load_toxicity_table_missing_file(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
load_toxicity_table(tmp_path / "absent.yaml")
def test_load_toxicity_table_empty(tmp_path: Path) -> None:
p = tmp_path / "t.yaml"
p.write_text("rows: []\n", encoding="utf-8")
with pytest.raises(ValueError, match="no 'rows'"):
load_toxicity_table(p)
def test_load_toxicity_table_parses_rows(tmp_path: Path) -> None:
p = tmp_path / "t.yaml"
p.write_text(_TOXIC_ROW, encoding="utf-8")
rows = load_toxicity_table(p)
assert len(rows) == 1
assert rows[0].toxic is True
assert rows[0].severity_class == "mild_moderate"
def test_toxic_row_requires_non_not_applicable_severity() -> None:
with pytest.raises(ValueError, match="not_applicable"):
ToxicityRow(
species_slug="x",
species_name="X",
animal="cat",
toxic=True,
principle="p",
severity_class="not_applicable",
source_name="s",
url="https://example.invalid/x",
license="CC0-1.0",
fetch_date="2026-05-01",
)
def test_nontoxic_row_requires_not_applicable_severity() -> None:
with pytest.raises(ValueError, match="not_applicable"):
ToxicityRow(
species_slug="x",
species_name="X",
animal="cat",
toxic=False,
principle="not listed",
severity_class="mild",
source_name="s",
url="https://example.invalid/x",
license="CC0-1.0",
fetch_date="2026-05-01",
)
def test_unknown_severity_class_rejected() -> None:
with pytest.raises(ValueError, match="severity_class"):
ToxicityRow(
species_slug="x",
species_name="X",
animal="cat",
toxic=True,
principle="p",
severity_class="lethal", # not in the SME-gated vocabulary
source_name="s",
url="https://example.invalid/x",
license="CC0-1.0",
fetch_date="2026-05-01",
)
def test_synthetic_row_cannot_point_at_a_real_domain() -> None:
"""A synthetic=true row with a non-placeholder URL would silently defeat the SME gate."""
with pytest.raises(ValueError, match="non-placeholder"):
ToxicityRow(
species_slug="x",
species_name="X",
animal="cat",
toxic=True,
principle="p",
severity_class="mild",
source_name="s",
url="https://aspca.org/toxic-plants/x",
license="CC0-1.0",
fetch_date="2026-05-01",
synthetic=True,
)
def test_non_synthetic_row_cannot_keep_the_placeholder_domain() -> None:
with pytest.raises(ValueError, match="placeholder"):
ToxicityRow(
species_slug="x",
species_name="X",
animal="cat",
toxic=True,
principle="p",
severity_class="mild",
source_name="s",
url="https://example.invalid/x",
license="CC0-1.0",
fetch_date="2026-05-01",
synthetic=False,
)
def test_coverage_report_groups_by_species_and_animal() -> None:
rows = load_toxicity_table_from_text(_TOXIC_ROW + _NONTOXIC_ROW.removeprefix("rows:\n"))
report = coverage_report(rows)
assert set(report) == {"aloe", "boston-fern"}
assert report["aloe"]["cat"].toxic is True
assert report["boston-fern"]["cat"].toxic is False
def load_toxicity_table_from_text(text: str) -> list[ToxicityRow]:
import tempfile
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "t.yaml"
p.write_text(text, encoding="utf-8")
return load_toxicity_table(p)
def test_check_consistency_passes_when_table_matches_prose() -> None:
rows = load_toxicity_table_from_text(_TOXIC_ROW)
doc = _doc(
"aloe.md",
"## Toxicity\nThe cited reference lists Aloe vera as toxic to cats and dogs.\n",
)
assert check_consistency(rows, [doc]) == []
def test_check_consistency_flags_a_contradiction() -> None:
rows = load_toxicity_table_from_text(_TOXIC_ROW) # says toxic=true
doc = _doc(
"aloe.md",
"## Toxicity\nThe cited reference does not list Aloe vera as toxic to cats or dogs.\n",
)
problems = check_consistency(rows, [doc])
assert len(problems) == 1
assert "aloe/cat" in problems[0]
assert "aloe/dog" not in "".join(problems) # only the cat row was in this table
def test_check_consistency_ignores_non_cat_dog_animals() -> None:
rows = load_toxicity_table_from_text(
"""rows:
- species_slug: orchid
species_name: Orchid (Phalaenopsis)
animal: horse
toxic: false
principle: not listed as toxic by the cited source (non-toxic is not the same as safe)
severity_class: not_applicable
source_name: Synthetic Plant-Care Notes
url: https://example.invalid/orchid
license: CC0-1.0
fetch_date: "2026-05-01"
synthetic: true
"""
)
# No documents at all -- a horse row must not be flagged as missing prose.
assert check_consistency(rows, []) == []
def test_check_consistency_flags_missing_document() -> None:
rows = load_toxicity_table_from_text(_TOXIC_ROW)
assert check_consistency(rows, []) == [
"aloe/cat: table row has no matching English document with a Toxicity "
"section to check against"
]
def test_real_corpus_table_is_consistent_with_real_corpus_prose() -> None:
"""End-to-end: the committed table agrees with the committed prose for every species."""
from sprout.config import Config
from sprout.ingest import load_corpus
root = Path(__file__).resolve().parent.parent
rows = load_toxicity_table(root / "corpus" / "toxicity.yaml")
cfg = Config.model_validate(
{
"corpus": {
"path": str(root / "corpus" / "processed"),
"manifest": str(root / "corpus" / "manifest.yaml"),
}
}
)
documents = load_corpus(cfg)
assert check_consistency(rows, documents) == []