"""角色卡解析器单元测试。""" from __future__ import annotations import base64 import io import json import struct import zipfile import zlib from app.modules.imports.card_parser import parse_character_card def _make_png_text(keyword: str, text: str) -> bytes: """构造含一个 tEXt chunk 的最小 PNG。""" def chunk(ctype: bytes, data: bytes) -> bytes: body = ctype + data return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF) payload = keyword.encode("latin-1") + b"\x00" + text.encode("latin-1") return b"\x89PNG\r\n\x1a\n" + chunk(b"tEXt", payload) + chunk(b"IEND", b"") def test_parse_v2_json() -> None: card = {"spec": "chara_card_v2", "spec_version": "2.0", "data": {"name": "狂三", "description": "desc", "personality": "腹黑"}} parsed = parse_character_card(json.dumps(card).encode("utf-8")) assert parsed.name == "狂三" assert parsed.personality == "腹黑" assert parsed.spec_version == "2.0" def test_parse_v1_flat_normalized() -> None: card = {"name": "V1角色", "description": "d", "personality": "p"} parsed = parse_character_card(json.dumps(card).encode("utf-8")) assert parsed.name == "V1角色" assert parsed.spec_version == "1" def test_parse_png_chara_chunk() -> None: card = {"spec": "chara_card_v2", "spec_version": "2.0", "data": {"name": "PNG卡"}} text = base64.b64encode(json.dumps(card).encode("utf-8")).decode("ascii") parsed = parse_character_card(_make_png_text("chara", text)) assert parsed.name == "PNG卡" def test_parse_png_ccv3_preferred_over_chara() -> None: # 同时含 chara(V2) 和 ccv3(V3),优先 ccv3 card_v3 = {"spec_version": "3.0", "data": {"name": "V3卡", "nickname": "三三"}} def chunk(ctype: bytes, data: bytes) -> bytes: body = ctype + data return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF) v3_text = base64.b64encode(json.dumps(card_v3).encode()).decode() payload_chara = b"chara\x00" + base64.b64encode(json.dumps({"data": {"name": "旧"}}).encode()) payload_ccv3 = b"ccv3\x00" + v3_text.encode("latin-1") png = b"\x89PNG\r\n\x1a\n" + chunk(b"tEXt", payload_chara) + chunk(b"tEXt", payload_ccv3) + chunk(b"IEND", b"") parsed = parse_character_card(png) assert parsed.name == "V3卡" assert parsed.nickname == "三三" def test_parse_charx_zip() -> None: card = {"spec_version": "3.0", "data": {"name": "CHARX卡"}} buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("card.json", json.dumps(card)) parsed = parse_character_card(buf.getvalue()) assert parsed.name == "CHARX卡" def test_character_book_preserved() -> None: card = { "spec": "chara_card_v2", "spec_version": "2.0", "data": { "name": "x", "character_book": {"entries": [{"keys": ["a"], "content": "b"}]}, }, } parsed = parse_character_card(json.dumps(card).encode()) assert parsed.character_book is not None assert parsed.character_book["entries"][0]["keys"] == ["a"] def test_alternate_greetings_and_tags() -> None: card = {"spec_version": "2.0", "data": {"name": "x", "alternate_greetings": ["hi", "yo"], "tags": ["anime"]}} parsed = parse_character_card(json.dumps(card).encode()) assert parsed.alternate_greetings == ["hi", "yo"] assert parsed.tags == ["anime"] def test_png_without_metadata_raises() -> None: import pytest def chunk(ctype: bytes, data: bytes) -> bytes: body = ctype + data return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF) png = b"\x89PNG\r\n\x1a\n" + chunk(b"IEND", b"") with pytest.raises(ValueError): parse_character_card(png)