From ce65bcd7f5b8a4e445f2169218d580941554288f Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Mon, 27 Jul 2026 01:38:53 +0800 Subject: [PATCH] feat(ccc): add forward-compatible CCv3 codec (#2113) Signed-off-by: RainbowBird --- packages/ccc/README.md | 48 +++- packages/ccc/package.json | 6 +- packages/ccc/src/codec/characterCardV3.ts | 210 ++++++++++++++++++ packages/ccc/src/codec/index.ts | 11 + packages/ccc/src/define/card.ts | 27 ++- packages/ccc/src/export/index.ts | 3 +- packages/ccc/src/export/json.ts | 9 +- packages/ccc/src/export/types/assets.ts | 8 - .../ccc/src/export/types/character_book.ts | 43 ---- .../ccc/src/export/types/character_card_v3.ts | 7 - packages/ccc/src/export/types/data.ts | 40 ---- packages/ccc/src/export/types/extensions.ts | 25 --- packages/ccc/src/export/types/index.ts | 5 - packages/ccc/src/index.ts | 1 + packages/ccc/test/characterCardV3.test.ts | 174 +++++++++++++++ packages/ccc/vitest.config.ts | 7 + pnpm-lock.yaml | 3 + vitest.config.ts | 1 + 18 files changed, 493 insertions(+), 135 deletions(-) create mode 100644 packages/ccc/src/codec/characterCardV3.ts create mode 100644 packages/ccc/src/codec/index.ts delete mode 100644 packages/ccc/src/export/types/assets.ts delete mode 100644 packages/ccc/src/export/types/character_book.ts delete mode 100644 packages/ccc/src/export/types/character_card_v3.ts delete mode 100644 packages/ccc/src/export/types/data.ts delete mode 100644 packages/ccc/src/export/types/extensions.ts delete mode 100644 packages/ccc/src/export/types/index.ts create mode 100644 packages/ccc/test/characterCardV3.test.ts create mode 100644 packages/ccc/vitest.config.ts diff --git a/packages/ccc/README.md b/packages/ccc/README.md index 4597443fd..22e18fdf8 100644 --- a/packages/ccc/README.md +++ b/packages/ccc/README.md @@ -1,6 +1,52 @@ # @proj-airi/ccc -Create Character Card in a modular way. +Character Card protocol primitives for AIRI. + +## What it owns + +- CCv3 JSON envelope validation and compatibility classification. +- Forward-compatible preservation of unknown fields during validation. +- Shared CCv3 TypeScript contracts. +- Character Card JSON, Markdown, PNG, and APNG export helpers. + +The package is intentionally runtime-agnostic. It does not own AIRI module +settings, local persistence, chat message assembly, or editor behavior. + +## Parse a CCv3 document + +```ts +import { parseCharacterCardV3 } from '@proj-airi/ccc' + +const { card, compatibility } = parseCharacterCardV3(jsonText) + +const characterName = card.data.name +const versionSupport = compatibility // 'older' | 'current' | 'newer' +``` + +The parser accepts either decoded JSON data or JSON text. Older and newer +`spec_version` values remain importable, while `compatibility` lets the caller +decide whether to warn the user. Unknown keys are preserved on the validated +output so future CCv3 fields are not silently deleted. + +Malformed JSON and invalid CCv3 structures throw +`InvalidCharacterCardError`. Use `isInvalidCharacterCardError` when a boundary +needs to distinguish protocol failures from storage or filesystem errors. + +## When to use it + +- Importing or exporting community Character Cards. +- Validating a CCv3 envelope before converting it into an application model. +- Building format adapters such as PNG, APNG, or CHARX. + +## When not to use it + +- Persisting AIRI-specific active-card state. +- Applying speech, vision, body-model, or agent configuration. +- Constructing provider messages from prompts, greetings, examples, or a + Lorebook. + +Those policies belong to AIRI's character runtime rather than the community +protocol codec. ## License diff --git a/packages/ccc/package.json b/packages/ccc/package.json index 3607f5de3..c050a395d 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -7,9 +7,11 @@ "test": "vitest run", "test:watch": "vitest", "lint": "eslint .", - "lint:fix": "eslint --fix ." + "lint:fix": "eslint --fix .", + "typecheck": "tsc --noEmit" }, "dependencies": { - "meta-png": "catalog:" + "meta-png": "catalog:", + "valibot": "catalog:" } } diff --git a/packages/ccc/src/codec/characterCardV3.ts b/packages/ccc/src/codec/characterCardV3.ts new file mode 100644 index 000000000..a6fa3fc61 --- /dev/null +++ b/packages/ccc/src/codec/characterCardV3.ts @@ -0,0 +1,210 @@ +import type { InferOutput } from 'valibot' + +import { + array, + boolean, + literal, + number, + objectWithRest, + optional, + picklist, + pipe, + record, + regex, + safeParse, + string, + union, + unknown, +} from 'valibot' + +const currentSpecVersion = 3 +const invalidCharacterCardMessage = 'Invalid Character Card V3.' + +const extensionsDepthPromptSchema = objectWithRest({ + depth: number(), + prompt: string(), + role: string(), +}, unknown()) + +const extensionsSchema = objectWithRest({ + depth_prompt: optional(extensionsDepthPromptSchema), + fav: optional(boolean()), + talkativeness: optional(number()), + world: optional(string()), +}, unknown()) + +const openExtensionsSchema = objectWithRest({}, unknown()) + +const assetSchema = objectWithRest({ + type: string(), + uri: string(), + name: string(), + ext: string(), +}, unknown()) + +const characterBookEntrySchema = objectWithRest({ + keys: array(string()), + content: string(), + extensions: openExtensionsSchema, + enabled: boolean(), + insertion_order: number(), + use_regex: boolean(), + case_sensitive: optional(boolean()), + constant: optional(boolean()), + id: optional(union([number(), string()])), + name: optional(string()), + comment: optional(string()), + priority: optional(number()), + selective: optional(boolean()), + secondary_keys: optional(array(string())), + position: optional(picklist(['before_char', 'after_char'])), +}, unknown()) + +const characterBookSchema = objectWithRest({ + name: optional(string()), + description: optional(string()), + scan_depth: optional(number()), + token_budget: optional(number()), + recursive_scanning: optional(boolean()), + extensions: openExtensionsSchema, + entries: array(characterBookEntrySchema), +}, unknown()) + +const characterCardDataSchema = objectWithRest({ + name: string(), + description: string(), + personality: string(), + scenario: string(), + first_mes: string(), + mes_example: string(), + alternate_greetings: array(string()), + character_book: optional(characterBookSchema), + character_version: string(), + creator: string(), + creator_notes: string(), + extensions: extensionsSchema, + post_history_instructions: string(), + system_prompt: string(), + tags: array(string()), + assets: optional(array(assetSchema)), + creation_date: optional(number()), + creator_notes_multilingual: optional(record(string(), string())), + group_only_greetings: array(string()), + modification_date: optional(number()), + nickname: optional(string()), + source: optional(array(string())), +}, unknown()) + +const characterCardV3Schema = objectWithRest({ + spec: literal('chara_card_v3'), + spec_version: pipe(string(), regex(/^\d+(?:\.\d+)*$/)), + data: characterCardDataSchema, +}, unknown()) + +/** One asset reference declared by a CCv3 document. */ +export type Asset = InferOutput +/** Asset references declared by a CCv3 document. */ +export type Assets = Asset[] +/** Character-specific Lorebook data. */ +export type CharacterBook = InferOutput +/** One independently matched and ordered Lorebook entry. */ +export type CharacterBookEntry = InferOutput +/** Application-defined Lorebook metadata. */ +export type CharacterBookExtensions = InferOutput +/** Application-defined Lorebook entry metadata. */ +export type CharacterBookEntryExtensions = InferOutput +/** Complete data object carried by a CCv3 envelope. */ +export type Data = InferOutput +/** Fields inherited from Character Card V1. */ +export type DataV1 = Pick +/** Fields inherited from Character Card V2. */ +export type DataV2 = Pick +/** Fields introduced or changed by Character Card V3. */ +export type DataV3 = Pick +/** Community extension fields carried by a character card. */ +export type Extensions = InferOutput +/** Standard `depth_prompt` extension value. */ +export type ExtensionsDepthPrompt = InferOutput +/** A Character Card document using the CCv3 envelope and data contract. */ +export type CharacterCardV3 = InferOutput + +/** Compatibility of a parsed document relative to AIRI's current CCv3 implementation. */ +export type CharacterCardCompatibility = 'older' | 'current' | 'newer' + +/** A validated CCv3 document together with its compatibility classification. */ +export interface ParsedCharacterCardV3 { + /** Validated card data. Unknown future fields remain attached to their owning objects. */ + card: CharacterCardV3 + /** Whether the source version is older than, equal to, or newer than CCv3 3.0. */ + compatibility: CharacterCardCompatibility +} + +/** Options retained on a CCv3 validation failure. */ +export interface InvalidCharacterCardErrorOptions { + /** Parser or schema issues that made the source unusable. */ + cause?: unknown + /** Original JSON text or object supplied by the caller. */ + source: unknown +} + +/** Error thrown when input cannot be interpreted as a Character Card V3 document. */ +export class InvalidCharacterCardError extends Error { + readonly source: unknown + + constructor(options: InvalidCharacterCardErrorOptions) { + super(invalidCharacterCardMessage, { cause: options.cause }) + this.name = 'InvalidCharacterCardError' + this.source = options.source + } +} + +/** Checks whether an error came from the CCv3 parsing boundary. */ +export function isInvalidCharacterCardError(error: unknown): error is InvalidCharacterCardError { + return error instanceof InvalidCharacterCardError +} + +/** + * Parses and validates a Character Card V3 object or JSON document. + * + * Unknown fields are preserved so a newer card can be inspected and exported + * without silently discarding data AIRI does not understand yet. Older and + * newer `spec_version` values are accepted and reported through + * `compatibility`; callers can decide how prominently to warn users. + */ +export function parseCharacterCardV3(source: unknown): ParsedCharacterCardV3 { + const candidate = parseJsonSource(source) + const result = safeParse(characterCardV3Schema, candidate) + + if (!result.success) { + throw new InvalidCharacterCardError({ + cause: result.issues, + source, + }) + } + + return { + card: result.output, + compatibility: resolveCompatibility(result.output.spec_version), + } +} + +function parseJsonSource(source: unknown): unknown { + if (typeof source !== 'string') + return source + + try { + return JSON.parse(source) + } + catch (cause) { + throw new InvalidCharacterCardError({ cause, source }) + } +} + +function resolveCompatibility(specVersion: string): CharacterCardCompatibility { + const sourceVersion = Number.parseFloat(specVersion) + if (sourceVersion < currentSpecVersion) + return 'older' + if (sourceVersion > currentSpecVersion) + return 'newer' + return 'current' +} diff --git a/packages/ccc/src/codec/index.ts b/packages/ccc/src/codec/index.ts new file mode 100644 index 000000000..6c89d9b28 --- /dev/null +++ b/packages/ccc/src/codec/index.ts @@ -0,0 +1,11 @@ +export { + InvalidCharacterCardError, + isInvalidCharacterCardError, + parseCharacterCardV3, +} from './characterCardV3' + +export type { + CharacterCardCompatibility, + InvalidCharacterCardErrorOptions, + ParsedCharacterCardV3, +} from './characterCardV3' diff --git a/packages/ccc/src/define/card.ts b/packages/ccc/src/define/card.ts index 8c8583323..fcacaac92 100644 --- a/packages/ccc/src/define/card.ts +++ b/packages/ccc/src/define/card.ts @@ -1,4 +1,4 @@ -import type { Data } from '../export/types' +import type { Data } from '../codec/characterCardV3' import type { Message } from './types/mes_example' interface CardCore { @@ -31,6 +31,21 @@ interface CardMeta { } interface CardAdditional { + /** + * Character assets such as icons, backgrounds, and emotions. + * - assets + */ + assets?: Data['assets'] + /** + * Character-specific Lorebook. + * - character_book + */ + characterBook?: Data['character_book'] + /** + * Creation time as a UTC Unix timestamp in seconds. + * - creation_date + */ + creationDate?: Data['creation_date'] /** * Extensions. * - extensions @@ -59,6 +74,16 @@ interface CardAdditional { * @see {@link https://github.com/kwaroran/character-card-spec-v3/blob/main/SPEC_V3.md#creator_notes_multilingual} */ notesMultilingual?: Data['creator_notes_multilingual'] + /** + * Last modification time as a UTC Unix timestamp in seconds. + * - modification_date + */ + modificationDate?: Data['modification_date'] + /** + * IDs or URLs describing the card's provenance. + * - source + */ + source?: Data['source'] } interface CardDescription { diff --git a/packages/ccc/src/export/index.ts b/packages/ccc/src/export/index.ts index e90f41169..0931f0a5f 100644 --- a/packages/ccc/src/export/index.ts +++ b/packages/ccc/src/export/index.ts @@ -1,5 +1,6 @@ +export type * as ccv3 from '../codec/characterCardV3' + export { exportToAPNG } from './apng' export { exportToJSON } from './json' export { exportToMD as exportToMarkdown, exportToMD } from './md' export { exportToPNG, exportToPNGBase64 } from './png' -export type * as ccv3 from './types' diff --git a/packages/ccc/src/export/json.ts b/packages/ccc/src/export/json.ts index b372f6ed2..b8360b2b9 100644 --- a/packages/ccc/src/export/json.ts +++ b/packages/ccc/src/export/json.ts @@ -1,5 +1,5 @@ +import type { CharacterCardV3 } from '../codec/characterCardV3' import type { Card } from '../define' -import type { CharacterCardV3 } from './types/character_card_v3' /** * Exports a Card object to CharacterCardV3 format @@ -23,6 +23,9 @@ function createCardData(data: Card): CharacterCardV3['data'] { return { name: data.name, nickname: data.nickname, + assets: data.assets, + character_book: data.characterBook, + creation_date: data.creationDate, description: data.description ?? '', personality: data.personality ?? '', scenario: data.scenario ?? '', @@ -36,6 +39,8 @@ function createCardData(data: Card): CharacterCardV3['data'] { system_prompt: data.systemPrompt ?? '', post_history_instructions: data.postHistoryInstructions ?? '', mes_example: formatMessageExample(data.messageExample), + modification_date: data.modificationDate, + source: data.source, tags: data.tags ?? [], extensions: createExtensions(data), } @@ -60,7 +65,7 @@ function formatMessageExample(messageExample: string[][] | undefined): string { * @param data Source card data * @returns Extensions object */ -function createExtensions(data: Card): Record { +function createExtensions(data: Card): CharacterCardV3['data']['extensions'] { return { depth_prompt: { depth: 4, diff --git a/packages/ccc/src/export/types/assets.ts b/packages/ccc/src/export/types/assets.ts deleted file mode 100644 index 37e48ff01..000000000 --- a/packages/ccc/src/export/types/assets.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Assets = Asset[] - -export interface Asset { - ext: string - name: string - type: string - uri: string -} diff --git a/packages/ccc/src/export/types/character_book.ts b/packages/ccc/src/export/types/character_book.ts deleted file mode 100644 index 93b5f438a..000000000 --- a/packages/ccc/src/export/types/character_book.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface CharacterBook { - description?: string - entries: CharacterBookEntry[] - extensions: CharacterBookExtensions - name?: string - recursive_scanning?: boolean - scan_depth?: number - token_budget?: number -} - -export interface CharacterBookEntry { - case_sensitive?: boolean - /** not used in prompt engineering */ - comment?: string - /** if true, always inserted in the prompt (within budget limit) */ - constant?: boolean - content: string - enabled: boolean - extensions: CharacterBookEntryExtensions - - // FIELDS WITH NO CURRENT EQUIVALENT IN SILLY - /** not used in prompt engineering */ - id?: number - /** if two entries inserted, lower "insertion order" = inserted higher */ - insertion_order: number - - // FIELDS WITH NO CURRENT EQUIVALENT IN AGNAI - keys: string[] - /** not used in prompt engineering */ - name?: string - /** whether the entry is placed before or after the character defs */ - position?: 'after_char' | 'before_char' - /** if token budget reached, lower priority value = discarded first */ - priority?: number - /** see field `selective`. ignored if selective == false */ - secondary_keys?: string[] - /** if `true`, require a key from both `keys` and `secondary_keys` to trigger the entry */ - selective?: boolean -} - -export interface CharacterBookExtensions extends Record {} - -export interface CharacterBookEntryExtensions extends Record {} diff --git a/packages/ccc/src/export/types/character_card_v3.ts b/packages/ccc/src/export/types/character_card_v3.ts deleted file mode 100644 index 33742be27..000000000 --- a/packages/ccc/src/export/types/character_card_v3.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Data } from './data' - -export interface CharacterCardV3 { - data: Data - spec: 'chara_card_v3' - spec_version: '3.0' -} diff --git a/packages/ccc/src/export/types/data.ts b/packages/ccc/src/export/types/data.ts deleted file mode 100644 index e7ab535ee..000000000 --- a/packages/ccc/src/export/types/data.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Assets } from './assets' -import type { CharacterBook } from './character_book' -import type { Extensions } from './extensions' - -/** @see {@link https://github.com/kwaroran/character-card-spec-v3/blob/main/SPEC_V3.md#charactercard-object} */ -export type Data = DataV1 & DataV2 & DataV3 - -/** @see {@link https://github.com/malfoyslastname/character-card-spec-v2/blob/main/spec_v1.md} */ -export interface DataV1 { - description: string - first_mes: string - mes_example: string - name: string - personality: string - scenario: string -} - -/** @see {@link https://github.com/malfoyslastname/character-card-spec-v2/blob/main/spec_v2.md} */ -export interface DataV2 { - alternate_greetings: string[] - character_book?: CharacterBook - character_version: string - creator: string - creator_notes: string - extensions: Extensions - post_history_instructions: string - system_prompt: string - tags: string[] -} - -/** @see {@link https://github.com/kwaroran/character-card-spec-v3/blob/main/SPEC_V3.md#charactercard-object} */ -export interface DataV3 { - assets?: Assets - creation_date?: number - creator_notes_multilingual?: Record - group_only_greetings: string[] - modification_date?: number - nickname?: string - source?: string[] -} diff --git a/packages/ccc/src/export/types/extensions.ts b/packages/ccc/src/export/types/extensions.ts deleted file mode 100644 index 23892783f..000000000 --- a/packages/ccc/src/export/types/extensions.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface Extensions extends Record { - /** - * @default - * ```ts - * { - * depth: 4, - * prompt: '', - * role: 'system', - * } - * ``` - */ - depth_prompt?: ExtensionsDepthPrompt - /** @default `false` */ - fav?: boolean - /** @default `0.5` */ - talkativeness?: number - /** @default `undefined` */ - world?: string -} - -export interface ExtensionsDepthPrompt { - depth: number - prompt: string - role: 'system' | ({} & string) -} diff --git a/packages/ccc/src/export/types/index.ts b/packages/ccc/src/export/types/index.ts deleted file mode 100644 index 78af1b679..000000000 --- a/packages/ccc/src/export/types/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { Asset, Assets } from './assets' -export type { CharacterBook, CharacterBookEntry, CharacterBookEntryExtensions, CharacterBookExtensions } from './character_book' -export type { CharacterCardV3 } from './character_card_v3' -export type { Data, DataV2, DataV3 } from './data' -export type { Extensions, ExtensionsDepthPrompt } from './extensions' diff --git a/packages/ccc/src/index.ts b/packages/ccc/src/index.ts index 2bf7032d6..5c1d0d07e 100644 --- a/packages/ccc/src/index.ts +++ b/packages/ccc/src/index.ts @@ -1,3 +1,4 @@ +export * from './codec' export * from './define' export * from './export' export * from './utils' diff --git a/packages/ccc/test/characterCardV3.test.ts b/packages/ccc/test/characterCardV3.test.ts new file mode 100644 index 000000000..314dde9b2 --- /dev/null +++ b/packages/ccc/test/characterCardV3.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it } from 'vitest' + +import { + exportToJSON, + InvalidCharacterCardError, + isInvalidCharacterCardError, + parseCharacterCardV3, +} from '../src' + +const completeCard = { + spec: 'chara_card_v3', + spec_version: '3.0', + data: { + name: 'ReLU', + description: 'A curious AI.', + personality: 'Warm and precise.', + scenario: 'A quiet observatory.', + first_mes: 'Welcome, {{user}}.', + mes_example: '\n{{user}}: Hello\n{{char}}: Hi!', + alternate_greetings: ['Good evening.'], + group_only_greetings: ['Hello, everyone.'], + character_version: '1.0.0', + creator: 'AIRI', + creator_notes: 'Created for codec coverage.', + creator_notes_multilingual: { + ja: 'コーデックテスト用です。', + }, + system_prompt: 'Stay in character.', + post_history_instructions: 'Answer the latest message.', + tags: ['assistant'], + source: ['https://example.com/relu'], + creation_date: 1_700_000_000, + modification_date: 1_700_000_100, + assets: [ + { + type: 'icon', + uri: 'ccdefault:', + name: 'main', + ext: 'png', + future_asset_field: 'preserved', + }, + ], + character_book: { + name: 'Observatory', + extensions: { + vendor: 'airi', + }, + entries: [ + { + keys: ['comet'], + content: 'The comet returns every 72 years.', + extensions: {}, + enabled: true, + insertion_order: 10, + use_regex: false, + id: 'comet-entry', + future_entry_field: true, + }, + ], + future_book_field: 42, + }, + extensions: { + airi: { + modules: {}, + }, + }, + future_data_field: { + keep: true, + }, + }, + future_envelope_field: 'preserved', +} as const + +describe('character card V3 codec', () => { + it('parses the complete CCv3 contract without discarding future fields', () => { + const result = parseCharacterCardV3(completeCard) + + expect(result.compatibility).toBe('current') + expect(result.card).toEqual(completeCard) + expect(result.card.data.character_book?.entries[0]?.id).toBe('comet-entry') + expect(result.card.data.character_book?.entries[0]?.use_regex).toBe(false) + }) + + it('parses JSON text through the same validation boundary', () => { + const result = parseCharacterCardV3(JSON.stringify(completeCard)) + + expect(result.card.data.name).toBe('ReLU') + expect(result.compatibility).toBe('current') + }) + + it('accepts compatible newer documents and reports their compatibility', () => { + const result = parseCharacterCardV3({ + ...completeCard, + spec_version: '4.0', + }) + + expect(result.card.spec_version).toBe('4.0') + expect(result.compatibility).toBe('newer') + }) + + it('accepts older documents with a V3 envelope and reports their compatibility', () => { + const result = parseCharacterCardV3({ + ...completeCard, + spec_version: '2.0', + }) + + expect(result.compatibility).toBe('older') + }) + + it('exports every standard CCv3 field represented by the domain card', () => { + const exported = exportToJSON({ + name: completeCard.data.name, + nickname: 'Re', + version: completeCard.data.character_version, + assets: [...completeCard.data.assets], + characterBook: completeCard.data.character_book, + creationDate: completeCard.data.creation_date, + description: completeCard.data.description, + greetings: [ + completeCard.data.first_mes, + ...completeCard.data.alternate_greetings, + ], + greetingsGroupOnly: [...completeCard.data.group_only_greetings], + modificationDate: completeCard.data.modification_date, + notes: completeCard.data.creator_notes, + notesMultilingual: completeCard.data.creator_notes_multilingual, + personality: completeCard.data.personality, + postHistoryInstructions: completeCard.data.post_history_instructions, + scenario: completeCard.data.scenario, + source: [...completeCard.data.source], + systemPrompt: completeCard.data.system_prompt, + tags: [...completeCard.data.tags], + extensions: completeCard.data.extensions, + }) + + expect(exported.data.assets).toEqual(completeCard.data.assets) + expect(exported.data.character_book).toEqual(completeCard.data.character_book) + expect(exported.data.creation_date).toBe(completeCard.data.creation_date) + expect(exported.data.modification_date).toBe(completeCard.data.modification_date) + expect(exported.data.source).toEqual(completeCard.data.source) + }) + + it('reports one domain error for malformed JSON or invalid cards', () => { + for (const source of [ + '{', + { + ...completeCard, + spec: 'chara_card_v2', + }, + { + ...completeCard, + data: { + ...completeCard.data, + character_book: { + ...completeCard.data.character_book, + entries: [{ + ...completeCard.data.character_book.entries[0], + use_regex: undefined, + }], + }, + }, + }, + ]) { + expect(() => parseCharacterCardV3(source)).toThrow(InvalidCharacterCardError) + + try { + parseCharacterCardV3(source) + } + catch (error) { + expect(isInvalidCharacterCardError(error)).toBe(true) + } + } + }) +}) diff --git a/packages/ccc/vitest.config.ts b/packages/ccc/vitest.config.ts new file mode 100644 index 000000000..3e4279747 --- /dev/null +++ b/packages/ccc/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e0b41977..5ccbabbfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3302,6 +3302,9 @@ importers: meta-png: specifier: 'catalog:' version: 1.0.6 + valibot: + specifier: 'catalog:' + version: 1.3.1(typescript@5.9.3) packages/core-agent: dependencies: diff --git a/vitest.config.ts b/vitest.config.ts index 17293779b..a0952d411 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ 'apps/ui-server-auth', 'apps/stage-tamagotchi', 'packages/cap-vite', + 'packages/ccc', 'packages/core-agent', 'packages/better-ws', 'packages/plugin-sdk',