forked from ChelseaKR/sprout
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_foundation.py
More file actions
249 lines (188 loc) · 7.96 KB
/
Copy pathtest_foundation.py
File metadata and controls
249 lines (188 loc) · 7.96 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
"""Tests for the deterministic foundation: hashing, text, language, config, models."""
from __future__ import annotations
import math
from pathlib import Path
import pytest
from pydantic import ValidationError
from sprout import __version__
from sprout.config import Config, load_config
from sprout.determinism import (
canonical_bytes,
sha256_of_file,
sha256_of_obj,
sha256_of_text,
short,
)
from sprout.lang import detect_language
from sprout.models import Answer, AnswerSentence, Chunk, Citation
from sprout.text import (
contains_phrase,
content_tokens,
coverage,
has_negation,
jaccard,
normalize,
split_sentences,
tokenize,
)
def test_version_matches_pyproject() -> None:
assert __version__ == "0.1.0"
# --- determinism -----------------------------------------------------------------
def test_canonical_bytes_is_order_independent() -> None:
assert canonical_bytes({"b": 1, "a": 2}) == canonical_bytes({"a": 2, "b": 1})
def test_hash_helpers_are_stable_and_consistent() -> None:
obj = {"x": [1, 2, 3], "y": "café"}
assert sha256_of_obj(obj) == sha256_of_obj(dict(reversed(list(obj.items()))))
assert sha256_of_text("hello") == sha256_of_text("hello")
assert sha256_of_text("hello") != sha256_of_text("world")
assert len(short(sha256_of_text("hello"))) == 12
def test_sha256_of_file(tmp_path: Path) -> None:
f = tmp_path / "x.txt"
f.write_text("contents", encoding="utf-8")
assert sha256_of_file(f) == sha256_of_text("contents")
# --- text ------------------------------------------------------------------------
def test_tokenize_folds_accents_and_case() -> None:
assert tokenize("Riego TAMBIÉN") == ["riego", "tambien"]
def test_tokenize_keeps_decimals_whole() -> None:
assert "1.5" in tokenize("let the top 1.5 inches dry")
def test_content_tokens_drop_stopwords_and_stem() -> None:
toks = content_tokens("the leaves are yellowing")
assert "the" not in toks
assert "are" not in toks
assert "leav" in toks # 'leaves' -> 'leav'
assert "yellow" in toks # 'yellowing' -> 'yellow'
def test_split_sentences_protects_decimals() -> None:
out = split_sentences("Let the top 1.5 inches dry. Then water deeply.")
assert out == ["Let the top 1.5 inches dry.", "Then water deeply."]
def test_split_sentences_empty() -> None:
assert split_sentences(" ") == []
@pytest.mark.parametrize(
("text", "expected"),
[
("Pothos is toxic to cats", False),
("Pothos is not toxic", True),
("It cannot hurt", True),
("no es tóxica", True),
("isn't fine", True),
],
)
def test_has_negation(text: str, expected: bool) -> None:
assert has_negation(text) is expected
def test_coverage_full_and_partial() -> None:
assert coverage("yellow leaves", "yellowing leaves indicate overwatering") == 1.0
assert coverage("", "anything") == 1.0
assert 0.0 < coverage("yellow fertilizer", "yellow leaves") < 1.0
def test_jaccard_bounds() -> None:
assert jaccard("water the plant", "water the plant") == 1.0
assert jaccard("", "") == 1.0
assert jaccard("cats toxic", "sunlight bright") == 0.0
def test_normalize_and_contains_phrase() -> None:
assert normalize(" A B\nC ") == "a b c"
assert contains_phrase("This Plant Is Toxic to Cats", "toxic to cats")
assert not contains_phrase("safe and sound", "toxic")
# --- language --------------------------------------------------------------------
@pytest.mark.parametrize(
("text", "expected"),
[
("Why are my Monstera leaves yellowing?", "en"),
("¿Por qué se amarillean las hojas?", "es"),
("Las hojas de la planta están amarillas", "es"),
("", "en"),
("12345 6789", "en"), # no markers -> default
],
)
def test_detect_language(text: str, expected: str) -> None:
assert detect_language(text) == expected
def test_detect_language_custom_default() -> None:
assert detect_language("", default="es") == "es"
# --- config ----------------------------------------------------------------------
def test_default_config_is_valid() -> None:
cfg = Config()
assert cfg.languages.reference == "en"
assert cfg.prompts.refusal_for("es").startswith("No tengo")
assert cfg.prompts.refusal_for("fr") == cfg.prompts.refusal_for("en") # fallback
assert "veterinario" in cfg.prompts.safety_route_for("es")
def test_safety_directive_has_en_es_parity_and_never_certifies() -> None:
# The urgency routing (E2), the non-toxic-caveat (R7), and the escalation card (E9)
# must each exist in every supported language, and the composed directive must never
# trip the never-certify-safe guard or carry an eval-suite forbidden phrase.
from sprout.guards import asserts_safety
cfg = Config()
langs = set(cfg.languages.supported)
for catalog in (
cfg.prompts.safety_route_by_lang,
cfg.prompts.nontoxic_caveat_by_lang,
cfg.prompts.escalation_card_by_lang,
):
assert set(catalog) == langs
forbidden = ["is safe", "non-toxic", "safe for", "harmless", "perfectly fine"]
for lang in langs:
directive = cfg.prompts.safety_directive_for(lang)
assert not asserts_safety(directive, lang, cfg.guards)
low = directive.lower()
assert not any(term in low for term in forbidden)
# The standardized escalation card names the real public authorities.
assert "888-426-4435" in directive and "855-764-7661" in directive
def test_config_rejects_unknown_keys() -> None:
with pytest.raises(ValidationError):
Config.model_validate({"corpus": {"nope": 1}})
def test_config_rejects_out_of_range() -> None:
with pytest.raises(ValidationError):
Config.model_validate({"retrieval": {"min_score": 2.0}})
def test_load_config_roundtrip(tmp_path: Path) -> None:
p = tmp_path / "c.yaml"
p.write_text("retrieval:\n top_k: 9\n", encoding="utf-8")
cfg = load_config(p)
assert cfg.retrieval.top_k == 9
def test_load_config_missing(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
load_config(tmp_path / "absent.yaml")
def test_load_config_empty_is_defaults(tmp_path: Path) -> None:
p = tmp_path / "empty.yaml"
p.write_text("", encoding="utf-8")
assert load_config(p).retrieval.top_k == Config().retrieval.top_k
def test_load_config_non_mapping(tmp_path: Path) -> None:
p = tmp_path / "bad.yaml"
p.write_text("- a\n- b\n", encoding="utf-8")
with pytest.raises(ValueError, match="mapping"):
load_config(p)
# --- models ----------------------------------------------------------------------
def _chunk() -> Chunk:
return Chunk(
chunk_id="c1",
doc_id="d1",
title="Monstera care",
source="monstera.md",
text="Yellowing leaves indicate overwatering.",
language="en",
topic="watering",
source_name="Synthetic Care Notes",
url="https://example.invalid/monstera",
license="CC0-1.0",
fetch_date="2026-05-01",
)
def test_chunk_citation_label() -> None:
assert _chunk().citation_label == "Monstera care — monstera.md (as of 2026-05-01)"
def test_answer_text_citations_and_coverage() -> None:
c = _chunk()
cit = Citation(
chunk_id=c.chunk_id,
doc_id=c.doc_id,
title=c.title,
source=c.source,
quote=c.text,
license=c.license,
fetch_date=c.fetch_date,
url=c.url,
)
s = AnswerSentence(text=c.text, chunk_id=c.chunk_id, citation=cit)
ans = Answer(question="why yellow?", language="en", sentences=(s, s))
assert ans.text == f"{c.text} {c.text}"
assert len(ans.citations) == 1 # deduped by chunk_id
assert math.isclose(ans.citation_coverage, 1.0)
def test_empty_answer_coverage_is_zero() -> None:
assert Answer(question="q", language="en").citation_coverage == 0.0
def test_models_are_frozen() -> None:
c = _chunk()
with pytest.raises(ValidationError):
c.text = "mutated" # frozen model rejects assignment