diff --git a/tests/tavern-card-validator.test.js b/tests/tavern-card-validator.test.js new file mode 100644 index 000000000..6eadae6d2 --- /dev/null +++ b/tests/tavern-card-validator.test.js @@ -0,0 +1,251 @@ +import { describe, test, expect } from '@jest/globals'; +import { TavernCardValidator } from '../src/validator/TavernCardValidator'; + +const V1_FIELDS = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example']; + +const V2_DATA_FIELDS = [ + 'name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example', + 'creator_notes', 'system_prompt', 'post_history_instructions', + 'alternate_greetings', 'tags', 'creator', 'character_version', 'extensions', +]; + +function makeV1Card() { + return Object.fromEntries(V1_FIELDS.map(f => [f, ''])); +} + +function makeV2Card() { + return { + spec: 'chara_card_v2', + spec_version: '2.0', + data: { + ...Object.fromEntries(V2_DATA_FIELDS.map(f => [f, ''])), + alternate_greetings: [], + tags: [], + extensions: {}, + }, + }; +} + +function makeV3Card() { + return { + spec: 'chara_card_v3', + spec_version: '3.0', + data: {}, + }; +} + +describe('TavernCardValidator', () => { + describe('validate', () => { + test('returns 1 for valid V1 card', () => { + const v = new TavernCardValidator(makeV1Card()); + expect(v.validate()).toBe(1); + }); + + test('returns 2 for valid V2 card', () => { + const v = new TavernCardValidator(makeV2Card()); + expect(v.validate()).toBe(2); + }); + + test('returns 3 for valid V3 card', () => { + const v = new TavernCardValidator(makeV3Card()); + expect(v.validate()).toBe(3); + }); + + test('returns false for empty object', () => { + const v = new TavernCardValidator({}); + expect(v.validate()).toBe(false); + }); + + test('prefers V1 when card satisfies both V1 and V2', () => { + const card = { ...makeV1Card(), ...makeV2Card() }; + const v = new TavernCardValidator(card); + expect(v.validate()).toBe(1); + }); + }); + + describe('validateV1', () => { + test('accepts card with all required fields', () => { + const v = new TavernCardValidator(makeV1Card()); + expect(v.validateV1()).toBe(true); + }); + + for (const field of V1_FIELDS) { + test(`rejects card missing ${field}`, () => { + const card = makeV1Card(); + delete card[field]; + const v = new TavernCardValidator(card); + expect(v.validateV1()).toBe(false); + expect(v.lastValidationError).toBe(field); + }); + } + }); + + describe('validateV2', () => { + test('accepts valid V2 card', () => { + const v = new TavernCardValidator(makeV2Card()); + expect(v.validateV2()).toBe(true); + }); + + test('rejects wrong spec string', () => { + const card = makeV2Card(); + card.spec = 'chara_card_v1'; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + expect(v.lastValidationError).toBe('spec'); + }); + + test('rejects wrong spec_version', () => { + const card = makeV2Card(); + card.spec_version = '1.0'; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + expect(v.lastValidationError).toBe('spec_version'); + }); + + test('rejects missing data', () => { + const card = makeV2Card(); + delete card.data; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + + for (const field of V2_DATA_FIELDS) { + test(`rejects card missing data.${field}`, () => { + const card = makeV2Card(); + delete card.data[field]; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + expect(v.lastValidationError).toBe(`data.${field}`); + }); + } + + test('rejects non-array alternate_greetings', () => { + const card = makeV2Card(); + card.data.alternate_greetings = 'not an array'; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + + test('rejects non-array tags', () => { + const card = makeV2Card(); + card.data.tags = 'not an array'; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + + test('rejects non-object extensions', () => { + const card = makeV2Card(); + card.data.extensions = 'not an object'; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + + test('accepts card with optional character_book', () => { + const card = makeV2Card(); + card.data.character_book = { + extensions: {}, + entries: [], + }; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(true); + }); + + test('rejects character_book missing entries', () => { + const card = makeV2Card(); + card.data.character_book = { extensions: {} }; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + expect(v.lastValidationError).toBe('data.character_book.entries'); + }); + + test('rejects character_book missing extensions', () => { + const card = makeV2Card(); + card.data.character_book = { entries: [] }; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + expect(v.lastValidationError).toBe('data.character_book.extensions'); + }); + + test('rejects character_book with non-array entries', () => { + const card = makeV2Card(); + card.data.character_book = { extensions: {}, entries: 'not array' }; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + + test('rejects character_book with non-object extensions', () => { + const card = makeV2Card(); + card.data.character_book = { extensions: 'not object', entries: [] }; + const v = new TavernCardValidator(card); + expect(v.validateV2()).toBe(false); + }); + }); + + describe('validateV3', () => { + test('accepts valid V3 card', () => { + const v = new TavernCardValidator(makeV3Card()); + expect(v.validateV3()).toBe(true); + }); + + test('rejects wrong spec string', () => { + const card = makeV3Card(); + card.spec = 'chara_card_v2'; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(false); + expect(v.lastValidationError).toBe('spec'); + }); + + test('rejects spec_version below 3.0', () => { + const card = makeV3Card(); + card.spec_version = '2.9'; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(false); + expect(v.lastValidationError).toBe('spec_version'); + }); + + test('rejects spec_version at or above 4.0', () => { + const card = makeV3Card(); + card.spec_version = '4.0'; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(false); + }); + + test('accepts spec_version 3.5', () => { + const card = makeV3Card(); + card.spec_version = '3.5'; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(true); + }); + + test('rejects missing data', () => { + const card = makeV3Card(); + delete card.data; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(false); + }); + + test('rejects non-object data', () => { + const card = makeV3Card(); + card.data = 'not an object'; + const v = new TavernCardValidator(card); + expect(v.validateV3()).toBe(false); + }); + }); + + describe('lastValidationError', () => { + test('is null after successful validation', () => { + const v = new TavernCardValidator(makeV1Card()); + v.validate(); + expect(v.lastValidationError).toBeNull(); + }); + + test('is reset on each validate call', () => { + const v = new TavernCardValidator({}); + v.validate(); + expect(v.lastValidationError).not.toBeNull(); + v.card = makeV1Card(); + v.validate(); + expect(v.lastValidationError).toBeNull(); + }); + }); +});