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,338 @@
import {
getVideoUploadConcurrency,
setVideoUploadConcurrency,
} from '@snowluma/protocol/highway/upload-settings';
// video-upload tests cover the fingerprint (fast-upload) path. The
// regular load path exercises ffmpeg + the OS temp dir + a streaming
// SHA1 implementation; that machinery has its own seams worth testing
// separately, and isn't what makes video-upload distinct at the API
// surface.
//
// What we're checking here: the two-sub-file shape (main + thumb), the
// thumb's source routing (`upload.subFileInfos[0]`), the per-sub-file
// fastOnlyError difference (main has it; thumb doesn't — FALLBACK_THUMB
// always provides bytes), and the video-specific OIDB fields.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { promises as fsp } from 'fs';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { pathToFileURL } from 'url';
import { computeHashes } from '@snowluma/protocol/highway/utils';
vi.mock('@snowluma/protocol/highway/pipeline', () => ({
runNtv2Upload: vi.fn(async () => ({ msgInfo: { msgInfoBody: [], extBizInfo: {} } })),
finalizeMediaMsgInfo: vi.fn(() => new Uint8Array([0x12, 0x34])),
hexToBytes: vi.fn((hex: string) => new Uint8Array(hex.length / 2)),
}));
import * as pipeline from '@snowluma/protocol/highway/pipeline';
import {
uploadVideoMsgInfo,
GROUP_VIDEO_CMD_ID,
GROUP_VIDEO_THUMB_CMD_ID,
PRIVATE_VIDEO_CMD_ID,
PRIVATE_VIDEO_THUMB_CMD_ID,
} from '@snowluma/protocol/highway/video-upload';
const FINGERPRINT = {
noByteFallback: true,
md5Hex: 'aa',
sha1Hex: 'bb',
fileSize: 1024,
width: 320,
height: 240,
duration: 10,
videoFormat: 0,
} as any;
describe('video-upload', () => {
afterEach(() => setVideoUploadConcurrency(1));
beforeEach(() => {
vi.mocked(pipeline.runNtv2Upload).mockClear();
vi.mocked(pipeline.finalizeMediaMsgInfo).mockClear();
});
it('snapshots the global concurrency for video bodies without parallelizing covers', async () => {
expect(getVideoUploadConcurrency()).toBe(1);
setVideoUploadConcurrency(4);
const first = uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
setVideoUploadConcurrency(2);
await first;
const firstUpload = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(firstUpload.uploads[0]!.concurrency).toBe(4);
expect(firstUpload.uploads[1]!.concurrency).toBeUndefined();
await uploadVideoMsgInfo({} as any, false, 'recipient-uid', FINGERPRINT);
const secondUpload = vi.mocked(pipeline.runNtv2Upload).mock.calls[1]![0];
expect(secondUpload.uploads[0]!.concurrency).toBe(2);
expect(secondUpload.uploads[1]!.concurrency).toBeUndefined();
});
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 uploadVideoMsgInfo({} as any, true, 12345, element);
expect(element).toMatchObject({
fileId: 'uploaded-resource',
mediaNode: node,
md5Hex: 'aa',
sha1Hex: 'bb',
});
expect(element.fileName).toBeTruthy();
});
it('group: 0x11EA_100 + GROUP_VIDEO_CMD_ID for main, GROUP_VIDEO_THUMB_CMD_ID for thumb', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x11ea);
expect(args.serviceCmd).toBe('OidbSvcTrpcTcp.0x11ea_100');
expect(args.requestId).toBe(3);
expect(args.businessType).toBe(2);
expect(args.compatQmsgSceneType).toBe(2);
expect(args.uploads).toHaveLength(2);
expect(args.uploads[0]!.cmdId).toBe(GROUP_VIDEO_CMD_ID);
expect(args.uploads[1]!.cmdId).toBe(GROUP_VIDEO_THUMB_CMD_ID);
});
it('c2c: 0x11E9_100 + PRIVATE cmd ids', async () => {
await uploadVideoMsgInfo({} as any, false, 'recipient-uid', FINGERPRINT);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0];
expect(args.oidbCmd).toBe(0x11e9);
expect(args.uploads[0]!.cmdId).toBe(PRIVATE_VIDEO_CMD_ID);
expect(args.uploads[1]!.cmdId).toBe(PRIVATE_VIDEO_THUMB_CMD_ID);
});
it('main file routes via "top", thumb routes via subFileInfos[0]', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploads = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads;
expect(uploads[0]!.source).toBe('top');
expect(uploads[0]!.subFileIndex).toBe(0);
expect(uploads[1]!.source).toBe(0);
expect(uploads[1]!.subFileIndex).toBe(1);
});
it('main fastOnlyError is set; thumb is silent (FALLBACK_THUMB always has bytes)', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploads = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads;
expect(uploads[0]!.fastOnlyError).toMatch(/video fast-upload not available/);
expect(uploads[1]!.fastOnlyError).toBeUndefined();
});
it('does NOT set a force-full retry flag on the main file (#145 — proven ineffective)', async () => {
// The old forceFullOnFastPath retry was removed: real-machine + kernel RE
// showed it never forces a fresh upload (the server fast-paths by md5
// metadata regardless of tryFastUploadCompleted). An expired-but-indexed
// video resource is a platform limitation, not an upload-flow fix.
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploads = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads;
expect(
(uploads[0] as unknown as Record<string, unknown>).forceFullOnFastPath,
).toBeUndefined();
});
it('uploadInfo carries TWO entries (main mp4 + thumb jpg) with subFileType 0 and 100', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploadInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect(uploadInfo).toHaveLength(2);
expect((uploadInfo[0] as any).subFileType).toBe(0);
expect((uploadInfo[1] as any).subFileType).toBe(100);
expect((uploadInfo[0] as any).fileInfo.type.type).toBe(2); // video
expect((uploadInfo[1] as any).fileInfo.type.type).toBe(1); // pic (thumb)
});
it('[#145] group video carries real width/height; c2c stays 0/0', async () => {
// A real QQ group video's MsgInfo has real dimensions (e.g. 296x640);
// sending 0x0 makes QQ-NT receivers render 文件已过期 even though the
// resource is fresh (verified on tempserver: 0x0 → expired, real dims →
// renders). c2c is left at 0 — the server rejects non-zero dims there.
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const groupInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((groupInfo[0] as any).fileInfo.width).toBe(320); // FINGERPRINT.width
expect((groupInfo[0] as any).fileInfo.height).toBe(240); // FINGERPRINT.height
vi.mocked(pipeline.runNtv2Upload).mockClear();
await uploadVideoMsgInfo({} as any, false, 'recipient-uid', FINGERPRINT);
const c2cInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((c2cInfo[0] as any).fileInfo.width).toBe(0);
expect((c2cInfo[0] as any).fileInfo.height).toBe(0);
});
it('[#145] fast-upload with 0x0 dims (e.g. forwarding a dimensionless video) falls back to portrait for group', async () => {
// A forwarded video whose cached dims are 0 must NOT ship 0x0 to a group
// (→ 文件已过期). videoPayloadFromFingerprint substitutes a neutral 720x1280
// portrait; c2c stays 0 (server rejects non-zero there).
const noDims = { ...FINGERPRINT, width: 0, height: 0 } as any;
await uploadVideoMsgInfo({} as any, true, 12345, noDims);
const groupInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((groupInfo[0] as any).fileInfo.width).toBe(720);
expect((groupInfo[0] as any).fileInfo.height).toBe(1280);
vi.mocked(pipeline.runNtv2Upload).mockClear();
await uploadVideoMsgInfo({} as any, false, 'recipient-uid', noDims);
const c2cInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((c2cInfo[0] as any).fileInfo.width).toBe(0);
expect((c2cInfo[0] as any).fileInfo.height).toBe(0);
});
it('[#145] zero-trust: fallback cover declared dims equal the video fallback (content == declared)', async () => {
// The fast-upload cover is a real 720x1280 image; the thumb sub-file must
// declare those same pixels, not a 1x1 lying about its size, and they match
// the video fallback so tile + cover agree.
const noDims = { ...FINGERPRINT, width: 0, height: 0 } as any;
await uploadVideoMsgInfo({} as any, true, 12345, noDims);
const info = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((info[0] as any).fileInfo.width).toBe(720); // video tile
expect((info[0] as any).fileInfo.height).toBe(1280);
expect((info[1] as any).fileInfo.width).toBe(720); // cover sub-file (subFileType 100)
expect((info[1] as any).fileInfo.height).toBe(1280);
});
it('[#145] zero-trust: cached duration 0 → time 1, never 00:00', async () => {
const noDur = { ...FINGERPRINT, duration: 0 } as any;
await uploadVideoMsgInfo({} as any, true, 12345, noDur);
const info = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((info[0] as any).fileInfo.time).toBe(1);
});
it('[#145] real video dims are preserved on the main file even though the cover is the 720x1280 fallback', async () => {
// FINGERPRINT has real 320x240 dims; the fast-upload cover is always the
// 720x1280 fallback (no real cover cached on the forward path). The video
// tile keeps the real dims — the cover/tile aspect mismatch is tolerated,
// only a 0x0 MAIN file triggers 文件已过期.
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const info = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((info[0] as any).fileInfo.width).toBe(320); // main video: real dims preserved
expect((info[0] as any).fileInfo.height).toBe(240);
expect((info[1] as any).fileInfo.width).toBe(720); // cover: fallback
expect((info[1] as any).fileInfo.height).toBe(1280);
});
it('main video carries the real `time` (duration in seconds) — regression: was 0', async () => {
// NTV2 server bakes the `time` field into the resulting MsgInfo
// bytes that ride along as `commonElem.pbElem`; the receiver
// reads it back via `VideoFile.fileTime` and renders it as the
// playable duration. NapCat ships `time: 0` only because it sits
// on top of QQ-NT's IPC layer which patches the value in before
// the wire send; we're a protocol-direct client (same position
// as acidify) and must populate it ourselves. Without this the
// receiver shows "00:00" on every video the bot sends.
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploadInfo = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploadInfo;
expect((uploadInfo[0] as any).fileInfo.time).toBe(10); // matches FINGERPRINT.duration
expect((uploadInfo[1] as any).fileInfo.time).toBe(0); // thumb stays at 0 (matches acidify)
});
it('thumb falls back to a synthesized 1x1 PNG with real bytes', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploads = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads;
expect(uploads[1]!.bytes.length).toBeGreaterThan(0); // FALLBACK_THUMB is non-empty
});
it('main file uses per-1MB-block sha1 (an Uint8Array[]) — fingerprint path uses an empty array', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const uploads = vi.mocked(pipeline.runNtv2Upload).mock.calls[0]![0].uploads;
// The fingerprint payload sets sha1Blocks: [] (no real bytes to chunk).
expect(Array.isArray(uploads[0]!.sha1)).toBe(true);
expect((uploads[0]!.sha1 as Uint8Array[]).length).toBe(0);
});
it('fingerprint mode rejects when md5Hex or sha1Hex is missing', async () => {
await expect(
uploadVideoMsgInfo({} as any, true, 12345, { noByteFallback: true } as any),
).rejects.toThrow(/requires md5Hex/);
});
it('finalize is called without a defaultPic', async () => {
await uploadVideoMsgInfo({} as any, true, 12345, FINGERPRINT);
const args = vi.mocked(pipeline.finalizeMediaMsgInfo).mock.calls[0]!;
expect(args[1]).toBeUndefined();
});
});
describe('video-upload — streaming path (real on-disk source)', () => {
beforeEach(() => {
vi.mocked(pipeline.runNtv2Upload).mockClear();
});
it('stages + stream-hashes the source and PUTs via fileSource, never buffering bytes', async () => {
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sl-vidstream-'));
const file = path.join(dir, 'clip.mp4');
const data = Buffer.alloc(3 * 1024 * 1024 + 77); // multi-1MiB-block + tail
for (let i = 0; i < data.length; i++) {
data[i] = (i * 37 + 5) & 0xff;
}
fs.writeFileSync(file, data);
try {
const bridge = { identity: { uin: '10001' } } as any;
await uploadVideoMsgInfo(bridge, true, 12345, {
type: 'video',
url: pathToFileURL(file).href,
} as any);
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls.at(-1)![0];
const main = args.uploads[0]!;
// Main file streams from disk — fileSource set, no in-memory bytes.
expect(main.fileSource).toBeDefined();
expect(main.fileSource!.fileSize).toBe(data.length);
expect(main.bytes.length).toBe(0);
// The OIDB fileInfo carries the STREAMED hashes/size — must equal the
// buffered hash of the same bytes (byte-identical guarantee end-to-end).
const fi = (args.uploadInfo[0] as any).fileInfo;
expect(fi.fileSize).toBe(data.length);
expect(fi.fileHash).toBe(computeHashes(data).md5Hex);
expect(fi.fileSha1).toBe(computeHashes(data).sha1Hex);
// The staged temp is a copy/hardlink under the stage dir (NOT the source)
// and is cleaned up after the send.
expect(main.fileSource!.filePath).not.toBe(file);
expect(fs.existsSync(main.fileSource!.filePath)).toBe(false);
} finally {
await fsp.rm(dir, { recursive: true, force: true });
}
});
it('cleans up the staged temp even when the upload throws', async () => {
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sl-vidfail-'));
const file = path.join(dir, 'clip.mp4');
fs.writeFileSync(file, Buffer.alloc(2048, 7));
try {
vi.mocked(pipeline.runNtv2Upload).mockRejectedValueOnce(new Error('upload boom'));
await expect(
uploadVideoMsgInfo({ identity: { uin: '1' } } as any, true, 12345, {
type: 'video',
url: pathToFileURL(file).href,
} as any),
).rejects.toThrow(/upload boom/);
// The staged temp created in loadVideo must be removed by the finally.
const args = vi.mocked(pipeline.runNtv2Upload).mock.calls.at(-1)![0];
const stagedPath = args.uploads[0]!.fileSource!.filePath;
expect(fs.existsSync(stagedPath)).toBe(false);
} finally {
await fsp.rm(dir, { recursive: true, force: true });
}
});
it('rejects an empty video source (0 bytes)', async () => {
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'sl-vidempty-'));
const file = path.join(dir, 'empty.mp4');
fs.writeFileSync(file, new Uint8Array(0));
try {
await expect(
uploadVideoMsgInfo({ identity: { uin: '1' } } as any, true, 12345, {
type: 'video',
url: pathToFileURL(file).href,
} as any),
).rejects.toThrow(/empty/);
} finally {
await fsp.rm(dir, { recursive: true, force: true });
}
});
});