diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts index cdf4fc6..37c4221 100644 --- a/src/channels/discord.test.ts +++ b/src/channels/discord.test.ts @@ -1050,6 +1050,57 @@ describe('sendMessage', () => { }); }); +// --- editMessage --- + +describe('editMessage', () => { + it('edits in place when text is within the 2000 char limit', async () => { + const opts = createTestOpts(); + const channel = new DiscordChannel('test-token', opts); + await channel.connect(); + + const mockMsg = { edit: vi.fn().mockResolvedValue(undefined) }; + const mockChannel = { + messages: { fetch: vi.fn().mockResolvedValue(mockMsg) }, + send: vi.fn().mockResolvedValue(undefined), + }; + currentClient().channels.fetch.mockResolvedValue(mockChannel); + + await channel.editMessage('dc:1234567890123456', 'msg-1', 'short final'); + + expect(mockMsg.edit).toHaveBeenCalledWith('short final'); + expect(mockChannel.send).not.toHaveBeenCalled(); + }); + + it('edits the first chunk and sends the remainder when text exceeds 2000 chars', async () => { + const opts = createTestOpts(); + const channel = new DiscordChannel('test-token', opts); + await channel.connect(); + + const mockMsg = { edit: vi.fn().mockResolvedValue(undefined) }; + const mockChannel = { + messages: { fetch: vi.fn().mockResolvedValue(mockMsg) }, + send: vi.fn().mockResolvedValue(undefined), + }; + currentClient().channels.fetch.mockResolvedValue(mockChannel); + + const longText = 'y'.repeat(4500); + await channel.editMessage('dc:1234567890123456', 'msg-1', longText); + + // First 2000 chars replace the tracked message, remainder as follow-ups. + expect(mockMsg.edit).toHaveBeenCalledTimes(1); + expect(mockMsg.edit).toHaveBeenCalledWith('y'.repeat(2000)); + expect(mockChannel.send).toHaveBeenCalledTimes(2); + expect(mockChannel.send).toHaveBeenNthCalledWith(1, { + content: 'y'.repeat(2000), + flags: 1 << 2, + }); + expect(mockChannel.send).toHaveBeenNthCalledWith(2, { + content: 'y'.repeat(500), + flags: 1 << 2, + }); + }); +}); + // --- ownsJid --- describe('ownsJid', () => { @@ -1159,4 +1210,50 @@ describe('channel properties', () => { const channel = new DiscordChannel('test-token', createTestOpts()); expect(channel.name).toBe('discord'); }); + + it('fetches missing channel metadata individually after a guild batch', async () => { + const channel = new DiscordChannel('test-token', createTestOpts()); + await channel.connect(); + + const firstGuild = { + channels: { + fetch: vi.fn().mockResolvedValue( + new Map([ + [ + '111', + { + name: 'first-chat', + position: 1, + parent: { name: 'First Guild', position: 1 }, + }, + ], + ]), + ), + }, + }; + currentClient().channels.fetch.mockImplementation(async (id: string) => { + if (id === '111') { + return { + name: 'first-chat', + position: 1, + parent: { name: 'First Guild', position: 1 }, + guild: firstGuild, + }; + } + if (id === '222') { + return { + name: 'second-chat', + position: 2, + parent: { name: 'Second Guild', position: 2 }, + guild: { channels: { fetch: vi.fn() } }, + }; + } + return null; + }); + + const metadata = await channel.getChannelMeta(['dc:111', 'dc:222']); + + expect(metadata.get('dc:111')?.name).toBe('first-chat'); + expect(metadata.get('dc:222')?.name).toBe('second-chat'); + }); }); diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 5b2de36..2a7e239 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -675,6 +675,21 @@ export class DiscordChannel implements Channel { categoryPosition: channel.parent?.position ?? 999, }); } + + for (const jid of dcJids) { + if (result.has(jid)) continue; + const channelId = jid.replace(/^dc:/, ''); + const channel = await this.client.channels.fetch(channelId); + if (channel && 'position' in channel) { + const tc = channel as TextChannel; + result.set(jid, { + name: tc.name, + position: tc.position, + category: tc.parent?.name || '', + categoryPosition: tc.parent?.position ?? 999, + }); + } + } } catch { // Fallback: individual fetches for (const jid of dcJids) { @@ -769,7 +784,23 @@ export class DiscordChannel implements Channel { throw new Error(`Discord channel not found or not editable: ${jid}`); } const msg = await (channel as TextChannel).messages.fetch(messageId); - await msg.edit(text); + // Discord caps a single message (edits included) at 2000 chars. A long + // final answer edited straight in is rejected with + // content[BASE_TYPE_MAX_LENGTH]; edit the tracked message with the first + // chunk and send the remainder as follow-up messages so nothing is + // dropped and no failed-edit fallback is needed. + const MAX_LENGTH = 2000; + if (text.length <= MAX_LENGTH) { + await msg.edit(text); + } else { + await msg.edit(text.slice(0, MAX_LENGTH)); + for (let i = MAX_LENGTH; i < text.length; i += MAX_LENGTH) { + await (channel as TextChannel).send({ + content: text.slice(i, i + MAX_LENGTH), + flags: MessageFlags.SuppressEmbeds, + }); + } + } logger.info( { jid,