Files
2026-08-21 16:00:13 +08:00

105 lines
3.8 KiB
Python

"""角色卡解析器单元测试。"""
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)