Files
SnowLuma/packages/protocol/tests/sys-faces.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

909 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { QFaceExtra, QSmallFaceExtra } from '@snowluma/proto-defs/element';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbFetchSysFacesReq,
OidbFetchSysFacesResp,
OidbFetchSysFaceReq,
OidbFetchSysFaceResp,
} from '@snowluma/proto-defs/oidb-actions/sys-faces';
import {
FetchSysFaces,
findFaceEntity,
isSuperFaceEntry,
isSuperFaceId,
type SysFaceEntry,
type SysFacePackEntry,
} from '../src/oidb-services/sys-faces/fetch-sys-faces';
import {
faceWireFor,
SysFaceStore,
sysFaceStore,
type SysFaceCatalogSnapshot,
type SysFaceCatalogStorage,
} from '../src/sys-face-store';
import { buildSendElems } from '../src/element-builder';
import { FetchSysFace } from '../src/oidb-services/sys-faces/fetch-sys-face';
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (error: Error) => void;
const promise = new Promise<T>((yes, no) => {
resolve = yes;
reject = no;
});
return { promise, resolve, reject };
}
function entry(
qSid: string,
aniStickerType: number | null,
aniStickerPackId: number | null,
aniStickerId: number | null,
qDes = '',
emojiNameAlias: string[] = [],
): SysFaceEntry {
return {
qSid,
qDes,
emCode: '',
qCid: null,
aniStickerType,
aniStickerPackId,
aniStickerId,
url: null,
emojiNameAlias,
aniStickerWidth: null,
aniStickerHeight: null,
};
}
// Animated faces include ordinary type-1 stickers as well as special types.
const CATALOG: SysFacePackEntry[] = [
{ packName: '经典', emojis: [entry('14', null, null, null, '/微笑', ['smile'])] },
{
packName: '超级表情',
emojis: [
entry('392', 3, 2, 38, '/龙年快乐'), // super → CommonElem 37
entry('424', 1, 1, 52),
entry('358', 2, 1, 33), // super (packId 1 but type 2) → CommonElem 37
],
},
];
class MemoryCatalogStorage implements SysFaceCatalogStorage {
saved: SysFaceCatalogSnapshot[] = [];
constructor(private readonly initial: SysFaceCatalogSnapshot | null = null) {}
async load(): Promise<SysFaceCatalogSnapshot | null> {
return this.initial;
}
async save(snapshot: SysFaceCatalogSnapshot): Promise<void> {
this.saved.push(snapshot);
}
}
describe('isSuperFaceEntry / isSuperFaceId', () => {
it('recognizes every animated sticker type', () => {
expect(isSuperFaceEntry(entry('x', 3, 2, 1))).toBe(true);
expect(isSuperFaceEntry(entry('x', 2, 1, 1))).toBe(true); // type≠1
expect(isSuperFaceEntry(entry('x', 1, 2, 1))).toBe(true); // pack≠1
expect(isSuperFaceEntry(entry('x', 1, 1, 52))).toBe(true);
expect(isSuperFaceEntry(entry('x', 0, 0, 0))).toBe(false);
expect(isSuperFaceEntry(entry('x', null, null, null))).toBe(false); // not aniSticker
});
it('isSuperFaceId walks the packs by id', () => {
expect(isSuperFaceId(CATALOG, 392)).toBe(true);
expect(isSuperFaceId(CATALOG, 424)).toBe(true);
expect(isSuperFaceId(CATALOG, 14)).toBe(false);
expect(isSuperFaceId(CATALOG, 99999)).toBe(false); // unknown
});
it('findFaceEntity returns the matching emoji or null', () => {
expect(findFaceEntity(CATALOG, 392)?.qSid).toBe('392');
expect(findFaceEntity(CATALOG, 99999)).toBeNull();
});
});
describe('faceWireFor — classification', () => {
it('super face → super wire with pack/sticker ids', () => {
expect(faceWireFor(entry('392', 3, 2, 38), 392)).toEqual({
kind: 'super',
packId: '2',
stickerId: '38',
stickerType: 3,
});
});
it('rejects incomplete super-face metadata instead of filling guessed ids', () => {
expect(() => faceWireFor(entry('392', 3, 2, null), 392)).toThrow(
/incomplete super-face metadata/,
);
});
it('uses the animation metadata for type-1 stickers too', () => {
expect(faceWireFor(entry('424', 1, 1, 52), 424)).toEqual({
kind: 'super',
packId: '1',
stickerId: '52',
stickerType: 1,
});
});
it('permits uncatalogued ids using their small representation', () => {
expect(faceWireFor(null, 14)).toEqual({ kind: 'classic' });
expect(faceWireFor(undefined, 504)).toEqual({ kind: 'small' });
});
});
describe('FetchSysFaces protocol contract', () => {
it('encodes the native 0x9154_1 request layout', () => {
const body = FetchSysFaces.serialize({} as never, {});
expect(body).toEqual({
field1: 0,
field2: 7,
field3: 0,
});
const decoded = protobuf_decode<OidbBase<OidbFetchSysFacesReq>>(
FetchSysFaces.encode({ body }),
);
expect(decoded.body).toEqual({
field1: null,
field2: 7,
field3: null,
field4: null,
});
});
});
describe('FetchSysFace protocol contract', () => {
it('queries the requested id and preserves animation metadata from the response', async () => {
const sendRawPacket = vi.fn(async (_command: string, _body: Uint8Array) => ({
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: protobuf_encode<OidbBase<OidbFetchSysFaceResp>>({
body: {
emoji: {
qSid: '507',
qDes: '/test',
aniStickerType: 1,
aniStickerPackId: 4,
aniStickerId: 107,
aniStickerWidth: 200,
aniStickerHeight: 240,
},
},
}),
}));
const store = new SysFaceStore({ fetchCatalog: async () => CATALOG });
await expect(store.resolveWire({ sendRawPacket }, 507)).resolves.toEqual({
kind: 'super',
packId: '4',
stickerId: '107',
stickerType: 1,
});
expect(sendRawPacket).toHaveBeenCalledOnce();
const [command, bytes] = sendRawPacket.mock.calls[0]!;
expect(command).toBe('OidbSvcTrpcTcp.0x9155_1');
expect(protobuf_decode<OidbBase<OidbFetchSysFaceReq>>(bytes)).toMatchObject({
command: 0x9155,
subCommand: 1,
body: { qSid: '507' },
});
expect(store.lookup(507)).toMatchObject({ aniStickerWidth: 200, aniStickerHeight: 240 });
});
it('does not invent metadata for an empty lookup result', () => {
expect(FetchSysFace.deserialize({} as never, {})).toBeNull();
});
});
describe('FetchSysFaces.deserialize', () => {
it('decodes every group in the third panel, including unnamed and hidden faces (#482)', () => {
const wire = protobuf_encode<OidbFetchSysFacesResp>({
specialMagicFace: {
emojiList: [
{ emojiPackName: 'MagicFace', emojiDetail: [{ qSid: '358' }] },
{ emojiDetail: [{ qSid: '504', unknown10: 1 }, { qSid: '505' }] },
{ emojiPackName: 'Limited', emojiDetail: [{ qSid: '506' }, { qSid: '507' }] },
],
},
});
const packs = FetchSysFaces.deserialize(
{} as never,
protobuf_decode<OidbFetchSysFacesResp>(wire),
);
expect(packs.map((pack) => pack.packName)).toEqual(['MagicFace', 'MagicFace', 'Limited']);
expect(packs.flatMap((pack) => pack.emojis.map((face) => face.qSid))).toEqual([
'358',
'504',
'505',
'506',
'507',
]);
});
it('flattens common + big + magic packs', () => {
const packs = FetchSysFaces.deserialize(
{} as never,
{
commonFace: {
emojiList: [{ emojiPackName: '经典', emojiDetail: [{ qSid: '14' }] }],
},
specialBigFace: {
emojiList: [
{
emojiPackName: '超级',
emojiDetail: [{ qSid: '392', aniStickerType: 3, aniStickerPackId: 2 }],
},
],
},
specialMagicFace: { emojiList: [{ emojiDetail: [{ qSid: '999' }] }] },
} as never,
);
expect(packs.map((p) => p.packName)).toEqual(['经典', '超级', 'MagicFace']);
expect(packs[1].emojis[0].aniStickerType).toBe(3);
});
it('preserves Unicode catalog identifiers returned by QQ', () => {
const packs = FetchSysFaces.deserialize(
{} as never,
{
commonFace: {
emojiList: [
{
emojiPackName: 'emoji',
emojiDetail: [
{ qSid: '😊', qDes: '/嘿嘿', qCid: 128522, emCode: '400832' },
],
},
],
},
} as never,
);
expect(packs).toEqual([
{
packName: 'emoji',
emojis: [
expect.objectContaining({
qSid: '😊',
qDes: '/嘿嘿',
qCid: 128522,
emCode: '400832',
}),
],
},
]);
});
it('fails closed when an id field uses an unsupported wire encoding', () => {
const driftedFace = { qDes: '/未知表情' };
Object.defineProperty(driftedFace, Symbol.for('snowluma.proton.unknownFields'), {
value: {
fields: [{ fieldNumber: 1, wireType: 0, count: 1, totalByteLength: 1 }],
totalOccurrences: 1,
omittedOccurrences: 0,
omittedByteLength: 0,
},
});
expect(() =>
FetchSysFaces.deserialize(
{} as never,
{
specialMagicFace: { emojiList: [{ emojiDetail: [driftedFace] }] },
} as never,
),
).toThrow(/unsupported wire encoding.*wireTypes=0/);
});
});
describe('SysFaceStore — persistent catalog and query seam', () => {
const sender = {} as never;
it.each(['snapshot', 'read error'] as const)(
'does not replace a refreshed catalog with a late cache %s',
async (result) => {
const cache = deferred<SysFaceCatalogSnapshot | null>();
const store = new SysFaceStore({
storage: { load: () => cache.promise, save: async () => {} },
fetchCatalog: async () => CATALOG,
});
const ready = store.ensureReady(sender);
await store.refresh(sender);
if (result === 'snapshot') {
cache.resolve({ schemaVersion: 1, fetchedAt: 1, packs: CATALOG.slice(0, 1) });
} else {
cache.reject(new Error('obsolete cache read failed'));
}
await ready;
await expect(store.getCatalog(sender)).resolves.toEqual(CATALOG);
expect(store.lookup(392)?.qSid).toBe('392');
},
);
it('waits for a refresh that started during the disk read', async () => {
const cache = deferred<SysFaceCatalogSnapshot | null>();
const remote = deferred<SysFacePackEntry[]>();
const store = new SysFaceStore({
storage: { load: () => cache.promise, save: async () => {} },
fetchCatalog: () => remote.promise,
});
let ready = false;
const reading = store.ensureReady(sender).then(() => {
ready = true;
});
const refreshing = store.refresh(sender);
cache.resolve({ schemaVersion: 1, fetchedAt: 1, packs: CATALOG.slice(0, 1) });
await new Promise((resolve) => setImmediate(resolve));
expect(ready).toBe(false);
remote.resolve(CATALOG);
await Promise.all([reading, refreshing]);
expect(store.lookup(392)?.qSid).toBe('392');
});
it('retains a failed cache read until an explicit refresh repairs the catalog', async () => {
const load = vi.fn(async () => {
throw new Error('corrupt cache');
});
const fetchCatalog = vi.fn(async () => CATALOG);
const store = new SysFaceStore({ storage: { load, save: async () => {} }, fetchCatalog });
await expect(store.ensureReady(sender)).rejects.toThrow('corrupt cache');
await expect(store.ensureReady(sender)).rejects.toThrow('corrupt cache');
expect(load).toHaveBeenCalledOnce();
expect(fetchCatalog).not.toHaveBeenCalled();
await store.refresh(sender);
await expect(store.getCatalog(sender)).resolves.toEqual(CATALOG);
});
it('retries a failed login prewarm, then shares the completed job', async () => {
const load = vi.fn(async () => null);
const fetchCatalog = vi
.fn()
.mockRejectedValueOnce(new Error('network failed'))
.mockResolvedValueOnce(CATALOG);
const store = new SysFaceStore({ storage: { load, save: async () => {} }, fetchCatalog });
await expect(store.prewarm(sender)).rejects.toThrow('network failed');
const retry = store.prewarm(sender);
expect(store.prewarm(sender)).toBe(retry);
await retry;
await store.prewarm(sender);
expect(load).toHaveBeenCalledOnce();
expect(fetchCatalog).toHaveBeenCalledTimes(2);
});
it('accepts face id 0 from the authoritative catalog', async () => {
const zero = entry('0', null, null, null, '/惊讶');
const store = new SysFaceStore({
storage: new MemoryCatalogStorage({
schemaVersion: 1,
fetchedAt: 123,
packs: [{ packName: '经典', emojis: [zero] }],
}),
fetchCatalog: async () => {
throw new Error('network must not be used');
},
});
await store.ensureReady(sender);
expect(store.lookup(0)?.qDes).toBe('/惊讶');
expect(store.classify(0)).toEqual({ kind: 'classic' });
});
it('loads a persisted catalog and resolves ids, descriptions, aliases, and pack names', async () => {
const storage = new MemoryCatalogStorage({
schemaVersion: 1,
fetchedAt: 123,
packs: CATALOG,
});
const store = new SysFaceStore({
storage,
fetchCatalog: async () => {
throw new Error('network must not be used');
},
});
await store.ensureReady(sender);
expect(store.lookup(392)?.qDes).toBe('/龙年快乐');
expect(store.search('龙年').map((face) => face.qSid)).toEqual(['392']);
expect(store.search('SMILE').map((face) => face.qSid)).toEqual(['14']);
expect(store.search('经典').map((face) => face.qSid)).toEqual(['14']);
});
it('persists and searches Unicode catalog ids without exposing them as numeric face ids', async () => {
const unicodeFace = entry('😊', null, null, null, '/嘿嘿');
unicodeFace.qCid = 128522;
unicodeFace.emCode = '400832';
const catalog: SysFacePackEntry[] = [{ packName: 'emoji', emojis: [unicodeFace] }];
const storage = new MemoryCatalogStorage();
const store = new SysFaceStore({
storage,
now: () => 456,
fetchCatalog: async () => catalog,
});
await expect(store.refresh(sender)).resolves.toEqual(catalog);
expect(storage.saved[0]?.packs).toEqual(catalog);
expect(store.search('😊').map((face) => face.qSid)).toEqual(['😊']);
expect(store.search('嘿嘿').map((face) => face.qSid)).toEqual(['😊']);
expect(store.lookup(128522)).toBeNull();
});
it('preserves one face across multiple catalog packs', async () => {
const commonFace = entry('474', 1, 4, 76, '/给你一拳');
commonFace.emCode = '10474';
const specialFace: SysFaceEntry = {
...commonFace,
qDes: '/给你一拳(黄脸)',
emojiNameAlias: ['punch'],
};
const catalog: SysFacePackEntry[] = [
{ packName: '超级表情', emojis: [commonFace] },
{ packName: 'QQ黄脸', emojis: [specialFace] },
];
const storage = new MemoryCatalogStorage();
const store = new SysFaceStore({
storage,
now: () => 456,
fetchCatalog: async () => catalog,
});
await expect(store.refresh(sender)).resolves.toEqual(catalog);
expect(storage.saved[0]?.packs).toEqual(catalog);
await expect(store.getCatalog(sender)).resolves.toEqual(catalog);
expect(store.lookup(474)).toEqual(commonFace);
expect(store.search('超级表情').map((face) => face.qSid)).toEqual(['474']);
expect(store.search('QQ黄脸').map((face) => face.qSid)).toEqual(['474']);
expect(store.search('punch').map((face) => face.qSid)).toEqual(['474']);
const restored = new SysFaceStore({
storage: new MemoryCatalogStorage(storage.saved[0]!),
fetchCatalog: async () => {
throw new Error('network must not be used');
},
});
await restored.ensureReady(sender);
await expect(restored.getCatalog(sender)).resolves.toEqual(catalog);
expect(restored.search('QQ黄脸').map((face) => face.qSid)).toEqual(['474']);
});
it('still rejects empty identifiers and duplicates within one pack', () => {
const store = new SysFaceStore();
expect(() =>
store.load([{ packName: 'emoji', emojis: [entry('', null, null, null)] }]),
).toThrow(/invalid system face id/);
expect(() =>
store.load([
{
packName: 'emoji',
emojis: [entry('😊', null, null, null), entry('😊', null, null, null)],
},
]),
).toThrow(/duplicate system face id/);
expect(() =>
store.load([
{ packName: '超级表情', emojis: [entry('474', 1, 4, 76)] },
{
packName: 'QQ黄脸',
emojis: [entry('474', 1, 4, 76), entry('474', 1, 4, 76)],
},
]),
).toThrow(/duplicate system face id 474 within pack 1/);
});
it('rejects cross-pack copies with conflicting send metadata', () => {
const store = new SysFaceStore();
expect(() =>
store.load([
{ packName: '超级表情', emojis: [entry('474', 1, 4, 76)] },
{ packName: 'QQ黄脸', emojis: [entry('474', 3, 2, 38)] },
]),
).toThrow(/conflicting system face send metadata.*pack 0.*pack 1/);
});
it('allows non-addressable Unicode entries to carry panel-specific metadata', () => {
const store = new SysFaceStore();
expect(() =>
store.load([
{ packName: 'emoji', emojis: [entry('😊', null, null, null, '/微笑')] },
{ packName: '最近使用', emojis: [entry('😊', 3, 2, 38, '/最近使用')] },
]),
).not.toThrow();
expect(store.search('最近使用').map((face) => face.qSid)).toEqual(['😊']);
});
it('coalesces concurrent refreshes and persists the authoritative result once', async () => {
const storage = new MemoryCatalogStorage();
let fetches = 0;
let release!: (packs: SysFacePackEntry[]) => void;
const pending = new Promise<SysFacePackEntry[]>((resolve) => {
release = resolve;
});
const store = new SysFaceStore({
storage,
now: () => 456,
fetchCatalog: async () => {
fetches += 1;
return pending;
},
});
const first = store.refresh(sender);
const second = store.refresh(sender);
release(CATALOG);
await Promise.all([first, second]);
expect(fetches).toBe(1);
expect(storage.saved).toEqual([
{
schemaVersion: 1,
fetchedAt: 456,
packs: CATALOG,
},
]);
});
it('keeps usable faces when a server pack contains an id-less record', async () => {
const storage = new MemoryCatalogStorage();
const response = FetchSysFaces.deserialize(sender, {
commonFace: {
emojiList: [
{ emojiPackName: '经典', emojiDetail: [{ qSid: '14', qDes: '/微笑' }] },
{ emojiPackName: '小表情', emojiDetail: [{ qSid: '424', qDes: '/比心' }] },
],
},
specialBigFace: {
emojiList: [
{
emojiPackName: '超级表情',
emojiDetail: [{ qSid: '392', qDes: '/龙年快乐' }],
},
],
},
specialMagicFace: {
emojiList: [
{
emojiDetail: [{ qDes: '/目录占位' }, { qSid: '999', qDes: '/魔法表情' }],
},
],
},
} as never);
const store = new SysFaceStore({
storage,
now: () => 456,
fetchCatalog: async () => response,
});
await expect(store.refresh(sender)).resolves.toHaveLength(4);
expect(store.lookup(14)?.qDes).toBe('/微笑');
expect(store.lookup(999)?.qDes).toBe('/魔法表情');
expect(storage.saved[0]?.packs[3]?.emojis.map((face) => face.qSid)).toEqual(['999']);
});
it('refreshes a cache miss once, then keeps an authoritative not-found result', async () => {
const storage = new MemoryCatalogStorage({
schemaVersion: 1,
fetchedAt: 123,
packs: CATALOG.slice(0, 1),
});
let fetches = 0;
const store = new SysFaceStore({
fetchFace: async () => null,
storage,
fetchCatalog: async () => {
fetches += 1;
return CATALOG;
},
});
expect((await store.resolve(sender, 392))?.qSid).toBe('392');
expect(await store.resolve(sender, 99999)).toBeNull();
expect(await store.resolve(sender, 99999)).toBeNull();
expect(await store.resolveWire(sender, 99999)).toEqual({ kind: 'small' });
expect(fetches).toBe(1);
});
it('prewarms from login only once per process while sharing the result across accounts', async () => {
let fetches = 0;
const store = new SysFaceStore({
storage: new MemoryCatalogStorage(),
fetchCatalog: async () => {
fetches += 1;
return CATALOG;
},
});
await Promise.all([
store.prewarm({} as never),
store.prewarm({} as never),
store.prewarm({} as never),
]);
await store.prewarm({} as never);
expect(fetches).toBe(1);
});
it('waits for an in-flight login refresh before resolving a cached face', async () => {
const staleCatalog: SysFacePackEntry[] = [
{
packName: '旧目录',
emojis: [entry('392', 1, 1, 52)],
},
];
let release!: (packs: SysFacePackEntry[]) => void;
let markFetchStarted!: () => void;
const fetchStarted = new Promise<void>((resolve) => {
markFetchStarted = resolve;
});
const pending = new Promise<SysFacePackEntry[]>((resolve) => {
release = resolve;
});
const store = new SysFaceStore({
storage: new MemoryCatalogStorage({
schemaVersion: 1,
fetchedAt: 123,
packs: staleCatalog,
}),
fetchCatalog: async () => {
markFetchStarted();
return pending;
},
});
const prewarm = store.prewarm(sender);
await fetchStarted;
const resolved = store.resolveWire(sender, 392);
let settled = false;
void resolved.finally(() => {
settled = true;
});
await Promise.resolve();
expect(settled).toBe(false);
release(CATALOG);
await expect(resolved).resolves.toMatchObject({ kind: 'super' });
await prewarm;
});
});
describe('makeFaceElem (via buildSendElems) — three-way wire encoding', () => {
const ctx = { bridge: {} as never };
it.each(['0', '2', 'variant'])(
'preserves an explicit animation result %s',
async (resultId) => {
sysFaceStore.load(CATALOG);
const [element] = await buildSendElems([{ type: 'face', faceId: 392, resultId }], ctx);
expect(protobuf_decode<QFaceExtra>(element.commonElem!.pbElem!)).toMatchObject({
qsid: 392,
resultId,
randomType: 1,
});
},
);
it('keeps the default animation choice when no result is supplied', async () => {
sysFaceStore.load(CATALOG);
const [element] = await buildSendElems([{ type: 'face', faceId: 392 }], ctx);
const payload = protobuf_decode<QFaceExtra>(element.commonElem!.pbElem!);
expect(payload.resultId).toBeNull();
expect(payload.randomType).toBe(1);
});
it.each([14, 504])(
'rejects an animation result without animation metadata for %i',
async (faceId) => {
sysFaceStore.load(CATALOG);
await expect(
buildSendElems([{ type: 'face', faceId, resultId: '0' }]),
).rejects.toMatchObject({
code: 'INVALID_FIELD',
elementType: 'face',
field: 'resultId',
});
},
);
it('rejects incompatible animation options before loading the catalog', async () => {
await expect(
buildSendElems([{ type: 'face', faceId: 392, large: false, resultId: '0' }], ctx),
).rejects.toMatchObject({ code: 'INVALID_FIELD', field: 'resultId' });
});
it('awaits an authoritative refresh when the first send misses the persisted catalog', async () => {
sysFaceStore.load(CATALOG.slice(0, 1));
let requests = 0;
const responseData = protobuf_encode<OidbBase<OidbFetchSysFacesResp>>({
body: {
commonFace: {
emojiList: [
{
emojiPackName: '经典',
emojiDetail: [{ qSid: '14', qDes: '/微笑' }],
},
],
},
specialBigFace: {
emojiList: [
{
emojiPackName: '超级表情',
emojiDetail: [
{
qSid: '392',
qDes: '/龙年快乐',
aniStickerType: 3,
aniStickerPackId: 2,
aniStickerId: 38,
},
],
},
],
},
},
});
const liveCtx = {
bridge: {
sendRawPacket: async () => {
requests += 1;
return {
success: true,
gotResponse: true,
errorCode: 0,
responseData,
};
},
} as never,
};
const [elem] = await buildSendElems([{ type: 'face', faceId: 392 }], liveCtx);
expect(requests).toBe(1);
expect(elem.commonElem?.serviceType).toBe(37);
});
it('encodes each face id by its catalog classification', async () => {
sysFaceStore.load(CATALOG); // warm → ensureWarm no-ops, classify uses the catalog
const [superElem] = await buildSendElems([{ type: 'face', faceId: 392 }], ctx);
expect(superElem.commonElem?.serviceType).toBe(37);
const big = protobuf_decode<QFaceExtra>(superElem.commonElem!.pbElem!);
expect(big.qsid).toBe(392);
expect(big.packId).toBe('2');
expect(big.stickerId).toBe('38');
expect(superElem.commonElem?.businessType).toBe(3);
const [animated] = await buildSendElems([{ type: 'face', faceId: 424 }], ctx);
expect(animated.commonElem?.serviceType).toBe(37);
expect(protobuf_decode<QFaceExtra>(animated.commonElem!.pbElem!).stickerId).toBe('52');
const [smallElem] = await buildSendElems(
[{ type: 'face', faceId: 424, large: false }],
ctx,
);
expect(smallElem.commonElem?.serviceType).toBe(33);
const small = protobuf_decode<QSmallFaceExtra>(smallElem.commonElem!.pbElem!);
expect(small.faceId).toBe(424);
const [classicElem] = await buildSendElems([{ type: 'face', faceId: 14 }], ctx);
expect(classicElem.face?.index).toBe(14);
});
it('allows special animated faces to be sent small and normalizes newer business types', async () => {
sysFaceStore.load([{ packName: 'special', emojis: [entry('504', 4, 1, 90)] }]);
const [large] = await buildSendElems([{ type: 'face', faceId: 504, large: true }]);
expect(large.commonElem?.businessType).toBe(1);
expect(protobuf_decode<QFaceExtra>(large.commonElem!.pbElem!).stickerType).toBe(4);
const [small] = await buildSendElems([{ type: 'face', faceId: 504, large: false }]);
expect(small.commonElem?.serviceType).toBe(33);
expect(protobuf_decode<QSmallFaceExtra>(small.commonElem!.pbElem!).faceId).toBe(504);
});
it('sends uncatalogued ids without inventing animation metadata', async () => {
sysFaceStore.load(CATALOG);
const [element] = await buildSendElems([{ type: 'face', faceId: 504 }]);
expect(element.commonElem?.serviceType).toBe(33);
expect(protobuf_decode<QSmallFaceExtra>(element.commonElem!.pbElem!).faceId).toBe(504);
});
});
describe('system faces outside visible panels', () => {
it.each(['result', 'error'] as const)(
'ignores an older catalog’s lookup %s after refresh',
async (outcome) => {
const stale = deferred<SysFaceEntry | null>();
const started = deferred<void>();
const latest = entry('507', 1, 4, 108);
const fetchFace = vi
.fn()
.mockImplementationOnce(() => {
started.resolve();
return stale.promise;
})
.mockResolvedValue(latest);
const store = new SysFaceStore({ fetchCatalog: async () => CATALOG, fetchFace });
const first = store.resolveWire({} as never, 507);
await started.promise;
await store.refresh({} as never);
const second = store.resolveWire({} as never, 507);
if (outcome === 'result') {
stale.resolve(entry('507', 1, 4, 107));
} else {
stale.reject(new Error('outdated lookup failed'));
}
const expected = { kind: 'super', packId: '4', stickerId: '108', stickerType: 1 };
await expect(first).resolves.toEqual(expected);
await expect(second).resolves.toEqual(expected);
expect(fetchFace).toHaveBeenCalledTimes(2);
expect(store.lookup(507)).toEqual(latest);
},
);
it('uses an explicitly loaded catalog when an older single-face lookup completes', async () => {
const stale = deferred<SysFaceEntry | null>();
const started = deferred<void>();
const fetchFace = vi.fn(() => {
started.resolve();
return stale.promise;
});
const store = new SysFaceStore({ fetchCatalog: async () => CATALOG, fetchFace });
const result = store.resolveWire({} as never, 507);
await started.promise;
const latest = entry('507', 1, 4, 108);
store.load([{ packName: '最新', emojis: [latest] }]);
stale.resolve(entry('507', 1, 4, 107));
await expect(result).resolves.toMatchObject({ stickerId: '108' });
expect(store.lookup(507)).toEqual(latest);
});
it('resolves missing animation metadata once and shares concurrent lookups', async () => {
const fetchFace = vi.fn(async () => entry('507', 1, 4, 107));
const store = new SysFaceStore({ fetchCatalog: async () => CATALOG, fetchFace });
const [first, second] = await Promise.all([
store.resolveWire({} as never, 507),
store.resolveWire({} as never, 507),
]);
expect(first).toEqual({ kind: 'super', packId: '4', stickerId: '107', stickerType: 1 });
expect(second).toEqual(first);
expect(store.lookup(507)?.qSid).toBe('507');
await store.resolveWire({} as never, 507);
expect(fetchFace).toHaveBeenCalledOnce();
});
it('does not silently send a small face after a lookup failure and permits retry', async () => {
const fetchFace = vi
.fn()
.mockRejectedValueOnce(new Error('lookup failed'))
.mockResolvedValueOnce(entry('507', 1, 4, 107));
const store = new SysFaceStore({ fetchCatalog: async () => CATALOG, fetchFace });
await expect(store.resolveWire({} as never, 507)).rejects.toThrow('lookup failed');
await expect(store.resolveWire({} as never, 507)).resolves.toMatchObject({ kind: 'super' });
});
it('rejects mismatched or incomplete animation metadata', async () => {
for (const face of [entry('506', 1, 4, 107), entry('507', 1, 4, null)]) {
const store = new SysFaceStore({
fetchCatalog: async () => CATALOG,
fetchFace: async () => face,
});
await expect(store.resolveWire({} as never, 507)).rejects.toThrow();
expect(store.lookup(507)).toBeNull();
}
});
});