SnowLuma v2.4.2 source snapshot
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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,144 @@
// image-upload tests: with the pipeline mocked out, these check the
// shape of the params each call passes — oidbCmd / serviceCmd / pic
// extBizInfo, the Highway cmdId mapping for group vs c2c, and the
// fast-upload (fingerprint) path.
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('@snowluma/protocol/highway/pipeline', () => ({
runNtv2Upload: vi.fn(async () => ({
msgInfo: { msgInfoBody: [{ index: { fileUuid: 'uploaded-image' } }], extBizInfo: {} },
})),
finalizeMediaMsgInfo: vi.fn(() => new Uint8Array([0xca, 0xfe])),
hexToBytes: vi.fn((hex: string) => new Uint8Array(hex.length / 2)),
}));
vi.mock('@snowluma/protocol/highway/utils', () => ({
loadBinarySource: vi.fn(async () => ({
bytes: new Uint8Array([1, 2, 3]),
fileName: 'src.jpg',
})),
computeHashes: vi.fn(() => ({
md5: new Uint8Array(16),
sha1: new Uint8Array(20),
md5Hex: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
sha1Hex: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
})),
detectImageFormat: vi.fn(() => ({ format: 1000, width: 800, height: 600 })),
}));
import * as pipeline from '@snowluma/protocol/highway/pipeline';
import * as utils from '@snowluma/protocol/highway/utils';
import { uploadImageMsgInfo } from '@snowluma/protocol/highway/image-upload';
import { GROUP_IMAGE_CMD_ID, PRIVATE_IMAGE_CMD_ID } from '@snowluma/protocol/highway';
describe('image-upload', () => {
beforeEach(() => {
vi.mocked(pipeline.runNtv2Upload).mockClear();
vi.mocked(pipeline.finalizeMediaMsgInfo).mockClear();
vi.mocked(utils.loadBinarySource).mockClear();
});
it('group: uses 0x11C4_100 + GROUP_IMAGE_CMD_ID and tags scene as group', async () => {
const out = await uploadImageMsgInfo({} as any, true, 12345, { url: 'http://x' } as any);
expect(out).toEqual(new Uint8Array([0xca, 0xfe]));
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x11c4);
expect(args.serviceCmd).toBe('OidbSvcTrpcTcp.0x11c4_100');
expect(args.requestId).toBe(1);
expect(args.businessType).toBe(1);
expect(args.compatQmsgSceneType).toBe(2);
expect(args.uploads[0]!.cmdId).toBe(GROUP_IMAGE_CMD_ID);
expect(args.extBizInfo.pic?.extData).toBeDefined();
expect(args.extBizInfo.pic?.bytesPbReserveC2c).toBeUndefined();
});
it('c2c: uses 0x11C5_100 + PRIVATE_IMAGE_CMD_ID and tags scene as c2c', async () => {
await uploadImageMsgInfo({} as any, false, 'recipient-uid', { url: 'http://x' } as any);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x11c5);
expect(args.serviceCmd).toBe('OidbSvcTrpcTcp.0x11c5_100');
expect(args.compatQmsgSceneType).toBe(1);
expect(args.uploads[0]!.cmdId).toBe(PRIVATE_IMAGE_CMD_ID);
expect(args.extBizInfo.pic?.bytesPbReserveC2c).toBeDefined();
expect(args.extBizInfo.pic?.extData).toBeUndefined();
});
it('uses receive-side imageUrl when url/fileId are absent', async () => {
await uploadImageMsgInfo({} as any, true, 12345, {
type: 'image',
imageUrl: 'https://gchat.qpic.cn/received.jpg',
});
expect(utils.loadBinarySource).toHaveBeenCalledWith(
'https://gchat.qpic.cn/received.jpg',
'image',
);
});
it('encodes the detected picFormat and image dimensions into fileInfo', async () => {
await uploadImageMsgInfo({} as any, true, 12345, { url: 'http://x' } as any);
const fileInfo = (vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo[0] as any)
.fileInfo;
expect(fileInfo.type.picFormat).toBe(1000);
expect(fileInfo.width).toBe(800);
expect(fileInfo.height).toBe(600);
expect(fileInfo.original).toBe(1);
});
it('forwards subType=1 (sticker) into bizType + Chinese textSummary `[动画表情]`', async () => {
// QQ chat-list bubble preview text — must match the literal
// strings mobile QQ / Lagrange.Core / NapCat all emit.
await uploadImageMsgInfo({} as any, true, 12345, { url: 'http://x', subType: 1 } as any);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect((args.extBizInfo as any).pic.bizType).toBe(1);
expect((args.extBizInfo as any).pic.textSummary).toBe('[动画表情]');
});
it('fingerprint path (noByteFallback): zero bytes, fastOnly error message ready', async () => {
await uploadImageMsgInfo({} as any, true, 12345, {
noByteFallback: true,
md5Hex: 'aa',
sha1Hex: 'bb',
summary: '[图片]',
} as any);
const sub = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads[0]!;
expect(sub.bytes.length).toBe(0);
expect(sub.fastOnlyError).toMatch(/fast-upload not available/);
});
it('fingerprint path rejects when md5Hex or sha1Hex is missing', async () => {
await expect(
uploadImageMsgInfo({} as any, true, 12345, {
noByteFallback: true,
} as any),
).rejects.toThrow(/requires md5Hex/);
});
it('finalize is invoked with pic defaults for image (Chinese `[图片]`)', async () => {
await uploadImageMsgInfo({} as any, true, 12345, { url: 'http://x', subType: 0 } as any);
expect(pipeline.finalizeMediaMsgInfo).toHaveBeenCalledOnce();
const [, defaultPic] = vi.mocked(pipeline.finalizeMediaMsgInfo).mock.calls[0]!;
expect(defaultPic).toEqual({
bizType: 0,
textSummary: '[图片]',
extData: { subType: 0, textSummary: '[图片]' },
});
});
it('caller-supplied summary wins over the Chinese default', async () => {
// OneBot clients can pass `summary: '[心形图片]'` (or any custom
// text) — that overrides our default and goes through verbatim.
await uploadImageMsgInfo({} as any, true, 12345, {
url: 'http://x',
subType: 0,
summary: '[custom-bubble]',
} as any);
const [, defaultPic] = vi.mocked(pipeline.finalizeMediaMsgInfo).mock.calls[0]!;
expect(defaultPic).toEqual({
bizType: 0,
textSummary: '[custom-bubble]',
extData: { subType: 0, textSummary: '[custom-bubble]' },
});
});
});