Files
SnowLuma/packages/core/tests/highway/ptt-upload.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

123 lines
5.4 KiB
TypeScript

// ptt-upload tests focus on the fingerprint (fast-upload) path: the
// regular load path goes through ffmpeg's silk encoder + the OS temp
// directory, which we don't want to exercise in a unit test. The
// fingerprint path covers most of what makes ptt-upload distinct
// (group/c2c requestId difference, command id mapping, the group-only
// bytesGeneralFlags blob, voiceFormat honouring).
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('@snowluma/protocol/highway/pipeline', () => ({
runNtv2Upload: vi.fn(async () => ({ msgInfo: { msgInfoBody: [], extBizInfo: {} } })),
finalizeMediaMsgInfo: vi.fn(() => new Uint8Array([0xaa, 0xbb])),
hexToBytes: vi.fn((hex: string) => new Uint8Array(hex.length / 2)),
}));
import * as pipeline from '@snowluma/protocol/highway/pipeline';
import {
uploadPttMsgInfo,
GROUP_PTT_CMD_ID,
PRIVATE_PTT_CMD_ID,
} from '@snowluma/protocol/highway/ptt-upload';
const FINGERPRINT = {
noByteFallback: true,
md5Hex: 'aa',
sha1Hex: 'bb',
fileSize: 1234,
duration: 5,
} as any;
describe('ptt-upload', () => {
beforeEach(() => {
vi.mocked(pipeline.runNtv2Upload).mockClear();
vi.mocked(pipeline.finalizeMediaMsgInfo).mockClear();
});
it('retains successful upload metadata for self-sent history', async () => {
const node = { fileUuid: 'uploaded-resource', storeId: 1, info: { fileSize: 1234 } };
vi.mocked(pipeline.runNtv2Upload).mockResolvedValueOnce({
msgInfo: { msgInfoBody: [{ index: node }] },
} as any);
const element = { ...FINGERPRINT };
await uploadPttMsgInfo({} as any, true, 12345, element);
expect(element).toMatchObject({
fileId: 'uploaded-resource',
mediaNode: node,
md5Hex: 'aa',
sha1Hex: 'bb',
});
expect(element.fileName).toBeTruthy();
});
it('group: 0x126E_100 + GROUP_PTT_CMD_ID + group-flavored bytesGeneralFlags', async () => {
await uploadPttMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x126e);
expect(args.serviceCmd).toBe('OidbSvcTrpcTcp.0x126e_100');
expect(args.requestId).toBe(1); // group
expect(args.businessType).toBe(3);
expect(args.uploads[0]!.cmdId).toBe(GROUP_PTT_CMD_ID);
const groupFlags = (args.extBizInfo as any).ptt.bytesGeneralFlags;
expect(groupFlags).toEqual(
new Uint8Array([0x9a, 0x01, 0x07, 0xaa, 0x03, 0x04, 0x08, 0x08, 0x12, 0x00]),
);
});
it('c2c: 0x126D_100 + PRIVATE_PTT_CMD_ID + requestId=4 + no generalFlags', async () => {
await uploadPttMsgInfo({} as any, false, 'recipient-uid', FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x126d);
expect(args.serviceCmd).toBe('OidbSvcTrpcTcp.0x126d_100');
expect(args.requestId).toBe(4);
expect(args.uploads[0]!.cmdId).toBe(PRIVATE_PTT_CMD_ID);
// Private voice must not carry a generalFlags blob: with the NapCat c2c
// one present the receiver drops the progress bar even though the waveform
// is in the payload. Group keeps its 10-byte blob (covered above).
expect((args.extBizInfo as any).ptt.bytesGeneralFlags).toBeUndefined();
expect((args.extBizInfo as any).ptt.waveform).toBeUndefined();
// Reserve stays the bare legacy 4 bytes, same as group voice.
expect([...(args.extBizInfo as any).ptt.bytesReserve]).toEqual([0x08, 0x00, 0x38, 0x00]);
});
it('fingerprint path does not invent a waveform', async () => {
await uploadPttMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect((args.extBizInfo as any).ptt.waveform).toBeUndefined();
expect(pipeline.finalizeMediaMsgInfo).toHaveBeenCalledOnce();
});
it('fingerprint payload: zero bytes, fastOnlyError ready, voiceFormat defaults to 1', async () => {
await uploadPttMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.uploads[0]!.bytes.length).toBe(0);
expect(args.uploads[0]!.fastOnlyError).toMatch(/record fast-upload not available/);
const fileInfo: any = (args.uploadInfo[0] as any).fileInfo;
expect(fileInfo.type.voiceFormat).toBe(1);
expect(fileInfo.time).toBe(5);
});
it('honours an explicit voiceFormat from the fingerprint', async () => {
await uploadPttMsgInfo({} as any, true, 12345, { ...FINGERPRINT, voiceFormat: 2 });
const fileInfo: any = (
vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo[0] as any
).fileInfo;
expect(fileInfo.type.voiceFormat).toBe(2);
});
it('fingerprint mode rejects when md5Hex or sha1Hex is missing', async () => {
await expect(
uploadPttMsgInfo({} as any, true, 12345, { noByteFallback: true } as any),
).rejects.toThrow(/requires md5Hex/);
});
it('finalize is called without a defaultPic (ptt copies pic from server only)', async () => {
await uploadPttMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.finalizeMediaMsgInfo).mock.calls[0]!;
expect(args[1]).toBeUndefined();
});
});