@@ -32,3 +32,90 @@ def test_save_load_preserves_encoding(tmp_path):
3232 s = "the quick café <|end|>"
3333 assert reloaded .encode (s ) == tok .encode (s )
3434 assert reloaded .decode (reloaded .encode (s )) == s
35+
36+
37+ def test_load_restores_saved_special_ids_exactly (tmp_path ):
38+ import json
39+
40+ tok = _trained (tmp_path )
41+ path = tmp_path / "tok.json"
42+ tok .save (path )
43+
44+ # perturb the stored ids so they are no longer the contiguous re-derivable block
45+ data = json .loads (path .read_text ())
46+ data ["special" ] = {t : i + 100 for t , i in data ["special" ].items ()}
47+ path .write_text (json .dumps (data ))
48+
49+ reloaded = Tokenizer .load (path )
50+ assert reloaded .special == data ["special" ]
51+ for text , i in data ["special" ].items ():
52+ assert reloaded .encode (text ) == [i ]
53+
54+
55+ def test_add_special_across_calls_never_collides (tmp_path ):
56+ tok = _trained (tmp_path )
57+ tok .add_special (["<|tool|>" ])
58+ tok .add_special (["<|result|>" ])
59+ ids = list (tok .special .values ())
60+ assert len (ids ) == len (set (ids ))
61+
62+
63+ def test_add_special_after_load_does_not_collide (tmp_path ):
64+ tok = _trained (tmp_path )
65+ path = tmp_path / "tok.json"
66+ tok .save (path )
67+
68+ reloaded = Tokenizer .load (path )
69+ reloaded .add_special (["<|tool|>" ])
70+ ids = list (reloaded .special .values ())
71+ assert len (ids ) == len (set (ids ))
72+ assert reloaded .special ["<|tool|>" ] > max (tok .special .values ())
73+
74+
75+ def test_train_after_add_special_raises (tmp_path ):
76+ import pytest
77+
78+ tok = Tokenizer ()
79+ tok .add_special (["<|end|>" ])
80+ with pytest .raises (ValueError , match = "after training" ):
81+ tok .train ("some text " * 20 , vocab_size = 300 )
82+
83+
84+ def test_load_rejects_colliding_special_ids (tmp_path ):
85+ import json
86+
87+ import pytest
88+
89+ tok = _trained (tmp_path )
90+ path = tmp_path / "tok.json"
91+ tok .save (path )
92+
93+ data = json .loads (path .read_text ())
94+ first = next (iter (data ["special" ]))
95+ data ["special" ][first ] = 260 # a merge id
96+ path .write_text (json .dumps (data ))
97+ with pytest .raises (ValueError , match = "special token ids" ):
98+ Tokenizer .load (path )
99+
100+
101+ def test_longer_special_token_wins_over_its_prefix (tmp_path ):
102+ tok = _trained (tmp_path )
103+ tok .add_special (["<|t|>" , "<|t|>x" ])
104+ assert tok .encode ("<|t|>x" ) == [tok .special ["<|t|>x" ]]
105+
106+
107+ def test_special_id_and_vocab_fit_helpers (tmp_path ):
108+ import pytest
109+
110+ from textllm .tokenizer import check_vocab_fit , special_id
111+
112+ tok = _trained (tmp_path )
113+ assert special_id (tok , "<|pad|>" ) == tok .special ["<|pad|>" ]
114+ check_vocab_fit (tok , tok .max_token_id + 1 )
115+ with pytest .raises (ValueError , match = "vocabulary" ):
116+ check_vocab_fit (tok , tok .max_token_id )
117+
118+ bare = Tokenizer ()
119+ bare .train ("some text here " * 20 , vocab_size = 300 )
120+ with pytest .raises (ValueError , match = "special" ):
121+ special_id (bare , "<|pad|>" )
0 commit comments