Add unit tests for TavernCardValidator (#5366) * Add unit tests for TavernCardValidator

8d8e3833f6e7ae41ab0c6c7aecfc4b8df5103192

Tony Gies <tgies@tgies.net>

Signed
1 files changed, +251 -0Showing whitespace changes
tests/tavern-card-validator.test.js+251 -0
@@ -0,0 +1,251 @@
1+import { describe, test, expect } from '@jest/globals';
2+import { TavernCardValidator } from '../src/validator/TavernCardValidator';
3+
4+const V1_FIELDS = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example'];
5+
6+const V2_DATA_FIELDS = [
7+ 'name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example',
8+ 'creator_notes', 'system_prompt', 'post_history_instructions',
9+ 'alternate_greetings', 'tags', 'creator', 'character_version', 'extensions',
10+];
11+
12+function makeV1Card() {
13+ return Object.fromEntries(V1_FIELDS.map(f => [f, '']));
14+}
15+
16+function makeV2Card() {
17+ return {
18+ spec: 'chara_card_v2',
19+ spec_version: '2.0',
20+ data: {
21+ ...Object.fromEntries(V2_DATA_FIELDS.map(f => [f, ''])),
22+ alternate_greetings: [],
23+ tags: [],
24+ extensions: {},
25+ },
26+ };
27+}
28+
29+function makeV3Card() {
30+ return {
31+ spec: 'chara_card_v3',
32+ spec_version: '3.0',
33+ data: {},
34+ };
35+}
36+
37+describe('TavernCardValidator', () => {
38+ describe('validate', () => {
39+ test('returns 1 for valid V1 card', () => {
40+ const v = new TavernCardValidator(makeV1Card());
41+ expect(v.validate()).toBe(1);
42+ });
43+
44+ test('returns 2 for valid V2 card', () => {
45+ const v = new TavernCardValidator(makeV2Card());
46+ expect(v.validate()).toBe(2);
47+ });
48+
49+ test('returns 3 for valid V3 card', () => {
50+ const v = new TavernCardValidator(makeV3Card());
51+ expect(v.validate()).toBe(3);
52+ });
53+
54+ test('returns false for empty object', () => {
55+ const v = new TavernCardValidator({});
56+ expect(v.validate()).toBe(false);
57+ });
58+
59+ test('prefers V1 when card satisfies both V1 and V2', () => {
60+ const card = { ...makeV1Card(), ...makeV2Card() };
61+ const v = new TavernCardValidator(card);
62+ expect(v.validate()).toBe(1);
63+ });
64+ });
65+
66+ describe('validateV1', () => {
67+ test('accepts card with all required fields', () => {
68+ const v = new TavernCardValidator(makeV1Card());
69+ expect(v.validateV1()).toBe(true);
70+ });
71+
72+ for (const field of V1_FIELDS) {
73+ test(`rejects card missing ${field}`, () => {
74+ const card = makeV1Card();
75+ delete card[field];
76+ const v = new TavernCardValidator(card);
77+ expect(v.validateV1()).toBe(false);
78+ expect(v.lastValidationError).toBe(field);
79+ });
80+ }
81+ });
82+
83+ describe('validateV2', () => {
84+ test('accepts valid V2 card', () => {
85+ const v = new TavernCardValidator(makeV2Card());
86+ expect(v.validateV2()).toBe(true);
87+ });
88+
89+ test('rejects wrong spec string', () => {
90+ const card = makeV2Card();
91+ card.spec = 'chara_card_v1';
92+ const v = new TavernCardValidator(card);
93+ expect(v.validateV2()).toBe(false);
94+ expect(v.lastValidationError).toBe('spec');
95+ });
96+
97+ test('rejects wrong spec_version', () => {
98+ const card = makeV2Card();
99+ card.spec_version = '1.0';
100+ const v = new TavernCardValidator(card);
101+ expect(v.validateV2()).toBe(false);
102+ expect(v.lastValidationError).toBe('spec_version');
103+ });
104+
105+ test('rejects missing data', () => {
106+ const card = makeV2Card();
107+ delete card.data;
108+ const v = new TavernCardValidator(card);
109+ expect(v.validateV2()).toBe(false);
110+ });
111+
112+ for (const field of V2_DATA_FIELDS) {
113+ test(`rejects card missing data.${field}`, () => {
114+ const card = makeV2Card();
115+ delete card.data[field];
116+ const v = new TavernCardValidator(card);
117+ expect(v.validateV2()).toBe(false);
118+ expect(v.lastValidationError).toBe(`data.${field}`);
119+ });
120+ }
121+
122+ test('rejects non-array alternate_greetings', () => {
123+ const card = makeV2Card();
124+ card.data.alternate_greetings = 'not an array';
125+ const v = new TavernCardValidator(card);
126+ expect(v.validateV2()).toBe(false);
127+ });
128+
129+ test('rejects non-array tags', () => {
130+ const card = makeV2Card();
131+ card.data.tags = 'not an array';
132+ const v = new TavernCardValidator(card);
133+ expect(v.validateV2()).toBe(false);
134+ });
135+
136+ test('rejects non-object extensions', () => {
137+ const card = makeV2Card();
138+ card.data.extensions = 'not an object';
139+ const v = new TavernCardValidator(card);
140+ expect(v.validateV2()).toBe(false);
141+ });
142+
143+ test('accepts card with optional character_book', () => {
144+ const card = makeV2Card();
145+ card.data.character_book = {
146+ extensions: {},
147+ entries: [],
148+ };
149+ const v = new TavernCardValidator(card);
150+ expect(v.validateV2()).toBe(true);
151+ });
152+
153+ test('rejects character_book missing entries', () => {
154+ const card = makeV2Card();
155+ card.data.character_book = { extensions: {} };
156+ const v = new TavernCardValidator(card);
157+ expect(v.validateV2()).toBe(false);
158+ expect(v.lastValidationError).toBe('data.character_book.entries');
159+ });
160+
161+ test('rejects character_book missing extensions', () => {
162+ const card = makeV2Card();
163+ card.data.character_book = { entries: [] };
164+ const v = new TavernCardValidator(card);
165+ expect(v.validateV2()).toBe(false);
166+ expect(v.lastValidationError).toBe('data.character_book.extensions');
167+ });
168+
169+ test('rejects character_book with non-array entries', () => {
170+ const card = makeV2Card();
171+ card.data.character_book = { extensions: {}, entries: 'not array' };
172+ const v = new TavernCardValidator(card);
173+ expect(v.validateV2()).toBe(false);
174+ });
175+
176+ test('rejects character_book with non-object extensions', () => {
177+ const card = makeV2Card();
178+ card.data.character_book = { extensions: 'not object', entries: [] };
179+ const v = new TavernCardValidator(card);
180+ expect(v.validateV2()).toBe(false);
181+ });
182+ });
183+
184+ describe('validateV3', () => {
185+ test('accepts valid V3 card', () => {
186+ const v = new TavernCardValidator(makeV3Card());
187+ expect(v.validateV3()).toBe(true);
188+ });
189+
190+ test('rejects wrong spec string', () => {
191+ const card = makeV3Card();
192+ card.spec = 'chara_card_v2';
193+ const v = new TavernCardValidator(card);
194+ expect(v.validateV3()).toBe(false);
195+ expect(v.lastValidationError).toBe('spec');
196+ });
197+
198+ test('rejects spec_version below 3.0', () => {
199+ const card = makeV3Card();
200+ card.spec_version = '2.9';
201+ const v = new TavernCardValidator(card);
202+ expect(v.validateV3()).toBe(false);
203+ expect(v.lastValidationError).toBe('spec_version');
204+ });
205+
206+ test('rejects spec_version at or above 4.0', () => {
207+ const card = makeV3Card();
208+ card.spec_version = '4.0';
209+ const v = new TavernCardValidator(card);
210+ expect(v.validateV3()).toBe(false);
211+ });
212+
213+ test('accepts spec_version 3.5', () => {
214+ const card = makeV3Card();
215+ card.spec_version = '3.5';
216+ const v = new TavernCardValidator(card);
217+ expect(v.validateV3()).toBe(true);
218+ });
219+
220+ test('rejects missing data', () => {
221+ const card = makeV3Card();
222+ delete card.data;
223+ const v = new TavernCardValidator(card);
224+ expect(v.validateV3()).toBe(false);
225+ });
226+
227+ test('rejects non-object data', () => {
228+ const card = makeV3Card();
229+ card.data = 'not an object';
230+ const v = new TavernCardValidator(card);
231+ expect(v.validateV3()).toBe(false);
232+ });
233+ });
234+
235+ describe('lastValidationError', () => {
236+ test('is null after successful validation', () => {
237+ const v = new TavernCardValidator(makeV1Card());
238+ v.validate();
239+ expect(v.lastValidationError).toBeNull();
240+ });
241+
242+ test('is reset on each validate call', () => {
243+ const v = new TavernCardValidator({});
244+ v.validate();
245+ expect(v.lastValidationError).not.toBeNull();
246+ v.card = makeV1Card();
247+ v.validate();
248+ expect(v.lastValidationError).toBeNull();
249+ });
250+ });
251+});