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
642 lines
23 KiB
TypeScript
642 lines
23 KiB
TypeScript
// Regression coverage for the group-file-in-send_group_msg bug.
|
|
//
|
|
// Symptom in the wild: bot uploaded a group file via `upload_group_file`,
|
|
// then sent `{type:'file', file_id}` through `send_group_msg`; the
|
|
// element-builder wrapped it as `transElem(elemType=24, ...)` inside
|
|
// `richText.elems` and shipped a regular `MessageSvc.PbSendMsg`. The
|
|
// QQ-NT server rejected the message with `result=79` because that wire
|
|
// shape is RECEIVE-side only (rich-body-decoder unpacks it into a
|
|
// FileEntity for inbound messages, but the server's intake validator
|
|
// flags it on the send side).
|
|
//
|
|
// Fix: split file segments off at the OneBot layer (same pattern as
|
|
// the c2c-file split for private messages) and dispatch through
|
|
// `bridge.apis.groupFile.publish`, which calls dedicated OIDB
|
|
// `OidbSvcTrpcTcp.0x6d9_4` — Lagrange.Core V2's
|
|
// `GroupSendFileService.cs`.
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { BridgeInterface } from '../../src/bridge/bridge-interface';
|
|
import type { OneBotInstanceContext } from '../src/instance-context';
|
|
import { MessageStore } from '../src/message-store';
|
|
import { sendGroupMessage } from '../src/modules/message-actions';
|
|
|
|
function fakeBridge(overrides: Partial<BridgeInterface> = {}): BridgeInterface {
|
|
return new Proxy(
|
|
{
|
|
identity: { nickname: 'SnowLuma' },
|
|
recallUploadedFile: vi.fn(),
|
|
...overrides,
|
|
} as BridgeInterface,
|
|
{
|
|
get(target, prop) {
|
|
if (prop in target) {
|
|
return (target as any)[prop];
|
|
}
|
|
|
|
throw new Error(`fakeBridge: '${String(prop)}' was not stubbed for this test`);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function makeCtx(bridge: BridgeInterface, messageStore?: MessageStore): OneBotInstanceContext {
|
|
const store =
|
|
messageStore ??
|
|
({ findEvent: () => null, resolveReplySequence: () => 0, storeEvent: vi.fn() } as any);
|
|
return {
|
|
uin: '10001',
|
|
selfId: 10001,
|
|
bridge,
|
|
messageStore: store,
|
|
cacheMessageMeta: messageStore
|
|
? async (messageId, meta) => await messageStore.storeMeta(messageId, meta)
|
|
: vi.fn(),
|
|
mediaStore: {} as any,
|
|
converterCtx: {
|
|
selfId: 10001,
|
|
imageUrlResolver: null,
|
|
mediaUrlResolver: null,
|
|
messageIdResolver: null,
|
|
mediaSegmentSink: null,
|
|
},
|
|
musicSignUrl: '',
|
|
} as unknown as OneBotInstanceContext;
|
|
}
|
|
|
|
const goodReceipt = {
|
|
messageId: 1,
|
|
sequence: 100,
|
|
clientSequence: 0,
|
|
random: 1,
|
|
timestamp: 1700000000,
|
|
};
|
|
|
|
describe('send_group_msg with {type:"file"} segment', () => {
|
|
it('file-only message routes through apis.groupFile.publish (not apis.message.sendGroup)', async () => {
|
|
// Pure-file case — the elems[] path used to ship a transElem(24)
|
|
// and got result=79. The dedicated OIDB-0x6d9_4 route must take
|
|
// over for group file publishing.
|
|
const sendGroupMessage_bridge = vi.fn();
|
|
// Declare params on the fn so `mock.calls[0]` infers a tuple of
|
|
// [groupId, fileId] instead of `[]` (which makes the destructuring
|
|
// a tsc error under noUncheckedIndexedAccess).
|
|
const publish = vi.fn(async (_groupId: number, _fileId: string) => undefined);
|
|
const bridge = fakeBridge({
|
|
apis: {
|
|
message: { sendGroup: sendGroupMessage_bridge },
|
|
groupFile: { publish },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge);
|
|
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{
|
|
type: 'file',
|
|
data: { file_id: 'gfid-abc', name: 'doc.txt', size: 123 },
|
|
},
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(publish).toHaveBeenCalledOnce();
|
|
expect(sendGroupMessage_bridge).not.toHaveBeenCalled();
|
|
|
|
const [groupId, fileId] = publish.mock.calls[0]!;
|
|
expect(groupId).toBe(12345);
|
|
expect(fileId).toBe('gfid-abc');
|
|
});
|
|
|
|
it('keeps an OIDB-only group file id separate from a QQ server sequence (#254)', async () => {
|
|
const messageStore = new MessageStore(':memory:');
|
|
await messageStore.initialization;
|
|
|
|
try {
|
|
const publish = vi.fn(async (_groupId: number, _fileId: string) => undefined);
|
|
const bridge = fakeBridge({
|
|
identity: { nickname: 'Bot' },
|
|
apis: {
|
|
message: { sendGroup: vi.fn() },
|
|
groupFile: { publish },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge, messageStore);
|
|
|
|
const result = await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{
|
|
type: 'file',
|
|
data: { file_id: 'gfid-abc', name: 'doc.txt', size: 123 },
|
|
},
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(result.messageId).not.toBe(0);
|
|
expect(await messageStore.findMeta(result.messageId)).toMatchObject({
|
|
sequence: 0,
|
|
sequenceAuthoritative: false,
|
|
});
|
|
expect(await messageStore.findEvent(result.messageId)).toMatchObject({
|
|
message_seq: 0,
|
|
});
|
|
expect(await messageStore.findLatestAuthoritativeSequence(true, 12345)).toBeNull();
|
|
} finally {
|
|
await messageStore.close();
|
|
}
|
|
});
|
|
|
|
it('skips an OIDB-only group-file reply and sends the remaining content (#254)', async () => {
|
|
const messageStore = new MessageStore(':memory:');
|
|
await messageStore.initialization;
|
|
|
|
try {
|
|
const sendGroup = vi.fn(async (_groupId: number, _elements: any[]) => goodReceipt);
|
|
const publish = vi.fn(async (_groupId: number, _fileId: string) => undefined);
|
|
const bridge = fakeBridge({
|
|
identity: { nickname: 'Bot' },
|
|
apis: {
|
|
message: { sendGroup },
|
|
groupFile: { publish },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge, messageStore);
|
|
|
|
const file = await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{
|
|
type: 'file',
|
|
data: { file_id: 'gfid-abc', name: 'doc.txt', size: 123 },
|
|
},
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'reply', data: { id: file.messageId } },
|
|
{ type: 'text', data: { text: 'received' } },
|
|
] as any,
|
|
false,
|
|
),
|
|
).resolves.toMatchObject({ messageId: expect.any(Number) });
|
|
expect(sendGroup).toHaveBeenCalledWith(12345, [{ type: 'text', text: 'received' }]);
|
|
} finally {
|
|
await messageStore.close();
|
|
}
|
|
});
|
|
|
|
it('skips a local-only reply id even when its full event was not cached', async () => {
|
|
const messageStore = new MessageStore(':memory:');
|
|
await messageStore.initialization;
|
|
|
|
try {
|
|
const sendGroup = vi.fn(async (_groupId: number, _elements: any[]) => goodReceipt);
|
|
const bridge = fakeBridge({
|
|
identity: { nickname: 'Bot' },
|
|
apis: { message: { sendGroup } } as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge, messageStore);
|
|
await messageStore.storeMeta(77, {
|
|
isGroup: true,
|
|
targetId: 12345,
|
|
sequence: 0,
|
|
sequenceAuthoritative: false,
|
|
eventName: 'group_message',
|
|
clientSequence: 0,
|
|
random: 42,
|
|
timestamp: 1700000000,
|
|
});
|
|
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'reply', data: { id: 77 } },
|
|
{ type: 'text', data: { text: 'received' } },
|
|
] as any,
|
|
false,
|
|
),
|
|
).resolves.toMatchObject({ messageId: expect.any(Number) });
|
|
expect(sendGroup).toHaveBeenCalledWith(12345, [{ type: 'text', text: 'received' }]);
|
|
} finally {
|
|
await messageStore.close();
|
|
}
|
|
});
|
|
|
|
it('does not apply local-only reply metadata from another group', async () => {
|
|
const messageStore = new MessageStore(':memory:');
|
|
await messageStore.initialization;
|
|
|
|
try {
|
|
const sendGroup = vi.fn(async (_groupId: number, _elements: any[]) => goodReceipt);
|
|
const bridge = fakeBridge({
|
|
identity: { nickname: 'Bot' },
|
|
apis: { message: { sendGroup } } as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge, messageStore);
|
|
await messageStore.storeMeta(77, {
|
|
isGroup: true,
|
|
targetId: 99999,
|
|
sequence: 0,
|
|
sequenceAuthoritative: false,
|
|
eventName: 'group_message',
|
|
clientSequence: 0,
|
|
random: 42,
|
|
timestamp: 1700000000,
|
|
});
|
|
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'reply', data: { id: 77 } },
|
|
{ type: 'text', data: { text: 'direct sequence' } },
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(sendGroup).toHaveBeenCalledWith(12345, [
|
|
{ type: 'reply', replySeq: 77 },
|
|
{ type: 'text', text: 'direct sequence' },
|
|
]);
|
|
} finally {
|
|
await messageStore.close();
|
|
}
|
|
});
|
|
|
|
it('mixed text + file splits across two sends (text via elems[], file via OIDB)', async () => {
|
|
const sendGroupMessage_bridge = vi.fn(
|
|
async (_gid: number, _elements: any[]) => goodReceipt,
|
|
);
|
|
const publish = vi.fn(async (_groupId: number, _fileId: string) => undefined);
|
|
const bridge = fakeBridge({
|
|
apis: {
|
|
message: { sendGroup: sendGroupMessage_bridge },
|
|
groupFile: { publish },
|
|
} as any,
|
|
resolveUserUid: vi.fn(async () => 'u_peer'),
|
|
} as any);
|
|
const ctx = makeCtx(bridge);
|
|
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'text', data: { text: 'here is the file:' } },
|
|
{ type: 'file', data: { file_id: 'gfid-xyz', name: 'pkg.zip' } },
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(sendGroupMessage_bridge).toHaveBeenCalledOnce();
|
|
expect(publish).toHaveBeenCalledOnce();
|
|
|
|
const [textGid, textElements] = sendGroupMessage_bridge.mock.calls[0]!;
|
|
expect(textGid).toBe(12345);
|
|
expect(textElements).toEqual([{ type: 'text', text: 'here is the file:' }]);
|
|
|
|
const [fileGid, fileFileId] = publish.mock.calls[0]!;
|
|
expect(fileGid).toBe(12345);
|
|
expect(fileFileId).toBe('gfid-xyz');
|
|
});
|
|
|
|
it('rejects the whole message when a file segment has neither file_id nor url', async () => {
|
|
const sendGroupMessage_bridge = vi.fn(
|
|
async (_gid: number, _elements: any[]) => goodReceipt,
|
|
);
|
|
const publish = vi.fn();
|
|
const upload = vi.fn();
|
|
const bridge = fakeBridge({
|
|
apis: {
|
|
message: { sendGroup: sendGroupMessage_bridge },
|
|
groupFile: { publish, upload },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge);
|
|
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'text', data: { text: 'with bad file segment' } },
|
|
{ type: 'file', data: {} }, // no file_id and no url
|
|
] as any,
|
|
false,
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 'MISSING_FIELD',
|
|
elementType: 'file',
|
|
});
|
|
|
|
expect(sendGroupMessage_bridge).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
expect(upload).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects text + image + temporary file before any chat publication', async () => {
|
|
const sendGroup = vi.fn(async () => goodReceipt);
|
|
const publish = vi.fn();
|
|
const upload = vi.fn(async () => ({ fileId: 'temp', busId: 104 }));
|
|
const ctx = makeCtx(
|
|
fakeBridge({
|
|
apis: { message: { sendGroup }, groupFile: { upload, publish } } as any,
|
|
}),
|
|
);
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'text', data: { text: 'caption' } },
|
|
{ type: 'image', data: { file: 'base64://AQID' } },
|
|
{ type: 'file', data: { file: '/tmp/report.pdf' } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toThrow(/temporary group files.*only file or image/i);
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects cached temporary file + text before publishing an earlier permanent file', async () => {
|
|
const sendGroup = vi.fn(async () => goodReceipt);
|
|
const publish = vi.fn();
|
|
const ctx = makeCtx(
|
|
fakeBridge({
|
|
apis: { message: { sendGroup }, groupFile: { publish } } as any,
|
|
recallUploadedFile: vi.fn((id) =>
|
|
id === 'temp'
|
|
? {
|
|
fileId: id,
|
|
scope: 'group',
|
|
groupId: 12345,
|
|
busId: 104,
|
|
}
|
|
: undefined,
|
|
),
|
|
} as any),
|
|
);
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'file', data: { file_id: 'permanent' } },
|
|
{ type: 'text', data: { text: 'caption' } },
|
|
{ type: 'file', data: { file_id: 'temp' } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toThrow(/temporary group files.*only file or image/i);
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([false, true])(
|
|
'allows temporary files with only file/image content (image=%s)',
|
|
async (withImage) => {
|
|
const sendGroup = vi.fn(async () => goodReceipt);
|
|
const publish = vi.fn();
|
|
const upload = vi.fn(async () => {
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
return { fileId: 'temp', busId: 104 };
|
|
});
|
|
const ctx = makeCtx(
|
|
fakeBridge({
|
|
apis: { message: { sendGroup }, groupFile: { upload, publish } } as any,
|
|
}),
|
|
);
|
|
const message = [
|
|
{ type: 'file', data: { file: '/tmp/report.pdf' } },
|
|
...(withImage ? [{ type: 'image', data: { file: 'base64://AQID' } }] : []),
|
|
];
|
|
await sendGroupMessage(ctx, 12345, message, false);
|
|
expect(upload).toHaveBeenCalledWith(
|
|
12345,
|
|
'/tmp/report.pdf',
|
|
'report.pdf',
|
|
'/',
|
|
true,
|
|
false,
|
|
{ allowTemporary: true },
|
|
);
|
|
expect(publish).toHaveBeenCalledExactlyOnceWith(12345, 'temp', 104);
|
|
expect(sendGroup).toHaveBeenCalledTimes(withImage ? 1 : 0);
|
|
},
|
|
);
|
|
|
|
it('preserves ordinary image + text without entering the group-file path', async () => {
|
|
const sendGroup = vi.fn(async () => goodReceipt);
|
|
const upload = vi.fn();
|
|
const publish = vi.fn();
|
|
const ctx = makeCtx(
|
|
fakeBridge({ apis: { message: { sendGroup }, groupFile: { upload, publish } } as any }),
|
|
);
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'image', data: { file: 'base64://AQID' } },
|
|
{ type: 'text', data: { text: 'caption' } },
|
|
],
|
|
false,
|
|
);
|
|
expect(sendGroup).toHaveBeenCalledOnce();
|
|
expect(sendGroup.mock.calls[0]).toMatchObject([
|
|
12345,
|
|
[
|
|
{ type: 'image', url: 'base64://AQID' },
|
|
{ type: 'text', text: 'caption' },
|
|
],
|
|
]);
|
|
expect(upload).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps permanent file + text sends, but checks uploads before sending the text', async () => {
|
|
const sendGroup = vi.fn(async () => goodReceipt);
|
|
const publish = vi.fn();
|
|
const upload = vi.fn(async () => {
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
return { fileId: 'permanent', busId: 102 };
|
|
});
|
|
const ctx = makeCtx(
|
|
fakeBridge({ apis: { message: { sendGroup }, groupFile: { upload, publish } } as any }),
|
|
);
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'text', data: { text: 'caption' } },
|
|
{ type: 'file', data: { file: '/tmp/report.pdf' } },
|
|
],
|
|
false,
|
|
);
|
|
expect(upload).toHaveBeenCalledWith(
|
|
12345,
|
|
'/tmp/report.pdf',
|
|
'report.pdf',
|
|
'/',
|
|
true,
|
|
false,
|
|
{ allowTemporary: false },
|
|
);
|
|
expect(sendGroup).toHaveBeenCalledOnce();
|
|
expect(publish).toHaveBeenCalledExactlyOnceWith(12345, 'permanent', 102);
|
|
});
|
|
|
|
it('does not publish the first file or text if a later upload fails', async () => {
|
|
const sendGroup = vi.fn();
|
|
const publish = vi.fn();
|
|
const upload = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ fileId: 'first', busId: 102 })
|
|
.mockRejectedValueOnce(
|
|
new Error('temporary group files require only file or image segments'),
|
|
);
|
|
const ctx = makeCtx(
|
|
fakeBridge({ apis: { message: { sendGroup }, groupFile: { upload, publish } } as any }),
|
|
);
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'text', data: { text: 'caption' } },
|
|
{ type: 'file', data: { file: '/tmp/one.pdf' } },
|
|
{ type: 'file', data: { file: '/tmp/two.pdf' } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toThrow(/temporary group files/);
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
expect(publish).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('reports a failed file publication with its identity and never retries the send', async () => {
|
|
const cause = new Error('chat post rejected');
|
|
const publish = vi.fn().mockRejectedValueOnce(cause);
|
|
const upload = vi.fn(async () => ({ fileId: 'temp', busId: 104 }));
|
|
const ctx = makeCtx(
|
|
fakeBridge({
|
|
apis: { message: { sendGroup: vi.fn() }, groupFile: { upload, publish } } as any,
|
|
}),
|
|
);
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[{ type: 'file', data: { file: '/tmp/report.pdf' } }],
|
|
false,
|
|
),
|
|
).rejects.toMatchObject({
|
|
message: 'group file chat post failed (fileId=temp): chat post rejected',
|
|
cause,
|
|
});
|
|
expect(upload).toHaveBeenCalledOnce();
|
|
expect(publish).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('uploads a file source without publishing, then publishes it exactly once', async () => {
|
|
// Happy path for inline file sending: the bot passes a local path
|
|
// directly. Delay chat publication until all uploads are checked.
|
|
const upload = vi.fn(
|
|
async (
|
|
_gid: number,
|
|
_src: string,
|
|
_name: string,
|
|
_folder: string,
|
|
_doUpload: boolean,
|
|
) => ({ fileId: 'auto-fid', fileHash: null }),
|
|
);
|
|
const publish = vi.fn();
|
|
const bridge = fakeBridge({
|
|
apis: {
|
|
message: { sendGroup: vi.fn() },
|
|
groupFile: { upload, publish },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge);
|
|
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{
|
|
type: 'file',
|
|
data: { file: '/tmp/audio.wav', name: 'audio.wav' },
|
|
},
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(upload).toHaveBeenCalledOnce();
|
|
const [gid, src, name, folder, doUpload] = upload.mock.calls[0]!;
|
|
expect(gid).toBe(12345);
|
|
expect(src).toBe('/tmp/audio.wav');
|
|
expect(name).toBe('audio.wav');
|
|
expect(folder).toBe('/');
|
|
expect(doUpload).toBe(true);
|
|
expect(upload).toHaveBeenCalledWith(
|
|
12345,
|
|
'/tmp/audio.wav',
|
|
'audio.wav',
|
|
'/',
|
|
true,
|
|
false,
|
|
{ allowTemporary: true },
|
|
);
|
|
expect(publish).toHaveBeenCalledExactlyOnceWith(12345, 'auto-fid', undefined);
|
|
});
|
|
|
|
it('percent-decodes a file:// URL basename when name is omitted (#354)', async () => {
|
|
const upload = vi.fn(async () => ({ fileId: 'auto-fid', fileHash: null }));
|
|
const bridge = fakeBridge({
|
|
apis: {
|
|
message: { sendGroup: vi.fn() },
|
|
groupFile: { upload, publish: vi.fn() },
|
|
} as any,
|
|
resolveUserUid: vi.fn(),
|
|
} as any);
|
|
const ctx = makeCtx(bridge);
|
|
|
|
await sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{
|
|
type: 'file',
|
|
data: { file: 'file:///D:/test/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.xlsx' },
|
|
},
|
|
] as any,
|
|
false,
|
|
);
|
|
|
|
expect(upload).toHaveBeenCalledOnce();
|
|
const [, src, name] = upload.mock.calls[0]!;
|
|
expect(src).toBe('file:///D:/test/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.xlsx');
|
|
expect(name).toBe('测试文件.xlsx');
|
|
});
|
|
});
|