Chunk long final answers in Discord editMessage instead of failing
A final answer over 2000 chars edited straight into the tracked progress message was rejected by Discord (content[BASE_TYPE_MAX_LENGTH]), leaving a failed edit + stale progress stub before the send fallback. editMessage now edits the tracked message with the first 2000-char chunk and sends the remainder as follow-up messages, so long finals land cleanly in order. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user