import { beforeAll, beforeEach, describe, expect, it, vi } from
"vitest" ;
import {
createDiscordOutboundHoisted,
expectDiscordThreadBotSend,
installDiscordOutboundModuleSpies,
mockDiscordBoundThreadManager,
resetDiscordOutboundMocks,
} from
"./outbound-adapter.test-harness.js" ;
const hoisted = createDiscordOutboundHoisted();
await installDiscordOutboundModuleSpies(hoisted);
let normalizeDiscordOutboundTarget:
typeof import (
"./normalize.js" ).normalizeDiscor
dOutboundTarget;
let discordOutbound: typeof import ("./outbound-adapter.js" ).discordOutbound;
beforeAll(async () => {
({ normalizeDiscordOutboundTarget } = await import ("./normalize.js" ));
({ discordOutbound } = await import ("./outbound-adapter.js" ));
});
describe("normalizeDiscordOutboundTarget" , () => {
it("normalizes bare numeric IDs to channel: prefix" , () => {
expect(normalizeDiscordOutboundTarget("1470130713209602050" )).toEqual({
ok: true ,
to: "channel:1470130713209602050" ,
});
});
it("passes through channel: prefixed targets" , () => {
expect(normalizeDiscordOutboundTarget("channel:123" )).toEqual({ ok: true , to: "channel:123" });
});
it("passes through user: prefixed targets" , () => {
expect(normalizeDiscordOutboundTarget("user:123" )).toEqual({ ok: true , to: "user:123" });
});
it("passes through channel name strings" , () => {
expect(normalizeDiscordOutboundTarget("general" )).toEqual({ ok: true , to: "general" });
});
it("returns error for empty target" , () => {
expect(normalizeDiscordOutboundTarget("" ).ok).toBe(false );
});
it("returns error for undefined target" , () => {
expect(normalizeDiscordOutboundTarget(undefined).ok).toBe(false );
});
it("trims whitespace" , () => {
expect(normalizeDiscordOutboundTarget(" 123 " )).toEqual({ ok: true , to: "channel:123" });
});
});
describe("discordOutbound" , () => {
beforeEach(() => {
resetDiscordOutboundMocks(hoisted);
});
it("routes text sends to thread target when threadId is provided" , async () => {
const result = await discordOutbound.sendText?.({
cfg: {},
to: "channel:parent-1" ,
text: "hello" ,
accountId: "default" ,
threadId: "thread-1" ,
});
expectDiscordThreadBotSend({
hoisted,
text: "hello" ,
result,
});
});
it("forwards explicit formatting options to Discord text sends" , async () => {
await discordOutbound.sendText?.({
cfg: {},
to: "channel:123456" ,
text: "formatted" ,
accountId: "default" ,
formatting: {
textLimit: 1234 ,
maxLinesPerMessage: 7 ,
tableMode: "off" ,
chunkMode: "newline" ,
},
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"formatted" ,
expect.objectContaining({
textLimit: 1234 ,
maxLinesPerMessage: 7 ,
tableMode: "off" ,
chunkMode: "newline" ,
}),
);
});
it.each([500 , 429 ])("retries transient Discord text send status %i" , async (status) => {
hoisted.sendMessageDiscordMock
.mockRejectedValueOnce(Object.assign(new Error(`discord ${status}`), { status }))
.mockResolvedValueOnce({
messageId: "msg-retry-ok" ,
channelId: "ch-1" ,
});
const result = await discordOutbound.sendText?.({
cfg: {
channels: {
discord: {
token: "test-token" ,
retry: { attempts: 2 , minDelayMs: 0 , maxDelayMs: 0 , jitter: 0 },
},
},
},
to: "channel:123456" ,
text: "retry me" ,
accountId: "default" ,
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledTimes(2 );
expect(result).toEqual({
channel: "discord" ,
messageId: "msg-retry-ok" ,
channelId: "ch-1" ,
});
});
it("uses webhook persona delivery for bound thread text replies" , async () => {
mockDiscordBoundThreadManager(hoisted);
const cfg = {
channels: {
discord: {
token: "resolved-token" ,
},
},
};
const result = await discordOutbound.sendText?.({
cfg,
to: "channel:parent-1" ,
text: "hello from persona" ,
accountId: "default" ,
threadId: "thread-1" ,
replyToId: "reply-1" ,
identity: {
name: "Codex" ,
avatarUrl: "https://example.com/avatar.png ",
},
});
expect(hoisted.sendWebhookMessageDiscordMock).toHaveBeenCalledWith(
"hello from persona" ,
expect.objectContaining({
webhookId: "wh-1" ,
webhookToken: "tok-1" ,
accountId: "default" ,
threadId: "thread-1" ,
replyTo: "reply-1" ,
username: "Codex" ,
avatarUrl: "https://example.com/avatar.png ",
}),
);
expect(
(hoisted.sendWebhookMessageDiscordMock.mock.calls[0 ]?.[1 ] as { cfg?: unknown } | undefined)
?.cfg,
).toBe(cfg);
expect(hoisted.sendMessageDiscordMock).not.toHaveBeenCalled();
expect(result).toEqual({
channel: "discord" ,
messageId: "msg-webhook-1" ,
channelId: "thread-1" ,
});
});
it("falls back to bot send for silent delivery on bound threads" , async () => {
mockDiscordBoundThreadManager(hoisted);
const result = await discordOutbound.sendText?.({
cfg: {},
to: "channel:parent-1" ,
text: "silent update" ,
accountId: "default" ,
threadId: "thread-1" ,
silent: true ,
});
expect(hoisted.sendWebhookMessageDiscordMock).not.toHaveBeenCalled();
expectDiscordThreadBotSend({
hoisted,
text: "silent update" ,
result,
options: { silent: true },
});
});
it("falls back to bot send when webhook send fails" , async () => {
mockDiscordBoundThreadManager(hoisted);
hoisted.sendWebhookMessageDiscordMock.mockRejectedValueOnce(new Error("rate limited" ));
const result = await discordOutbound.sendText?.({
cfg: {},
to: "channel:parent-1" ,
text: "fallback" ,
accountId: "default" ,
threadId: "thread-1" ,
});
expect(hoisted.sendWebhookMessageDiscordMock).toHaveBeenCalledTimes(1 );
expectDiscordThreadBotSend({
hoisted,
text: "fallback" ,
result,
});
});
it("routes poll sends to thread target when threadId is provided" , async () => {
const result = await discordOutbound.sendPoll?.({
cfg: {},
to: "channel:parent-1" ,
poll: {
question: "Best snack?" ,
options: ["banana" , "apple" ],
},
accountId: "default" ,
threadId: "thread-1" ,
});
expect(hoisted.sendPollDiscordMock).toHaveBeenCalledWith(
"channel:thread-1" ,
{
question: "Best snack?" ,
options: ["banana" , "apple" ],
},
expect.objectContaining({
accountId: "default" ,
}),
);
expect(result).toEqual({
channel: "discord" ,
messageId: "poll-1" ,
channelId: "ch-1" ,
});
});
it("routes audioAsVoice payloads through the Discord voice send helper" , async () => {
const result = await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "voice note" ,
mediaUrls: ["https://example.com/voice.ogg ", "https://example.com/extra.png"],
audioAsVoice: true ,
},
accountId: "default" ,
replyToId: "reply-1" ,
replyToMode: "first" ,
});
expect(hoisted.sendVoiceMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"https://example.com/voice.ogg ",
expect.objectContaining({
accountId: "default" ,
replyTo: "reply-1" ,
}),
);
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"voice note" ,
expect.objectContaining({
accountId: "default" ,
replyTo: undefined,
}),
);
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"" ,
expect.objectContaining({
accountId: "default" ,
mediaUrl: "https://example.com/extra.png ",
replyTo: undefined,
}),
);
expect(result).toEqual({
channel: "discord" ,
messageId: "msg-1" ,
channelId: "ch-1" ,
});
});
it("keeps replyToId on every internal audioAsVoice send when replyToMode is all" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "voice note" ,
mediaUrls: ["https://example.com/voice.ogg ", "https://example.com/extra.png"],
audioAsVoice: true ,
},
accountId: "default" ,
replyToId: "reply-1" ,
replyToMode: "all" ,
});
expect(
(hoisted.sendVoiceMessageDiscordMock.mock.calls[0 ]?.[2 ] as { replyTo?: unknown } | undefined)
?.replyTo,
).toBe("reply-1" );
expect(
hoisted.sendMessageDiscordMock.mock.calls.map(
(call) => (call[2 ] as { replyTo?: unknown } | undefined)?.replyTo,
),
).toEqual(["reply-1" , "reply-1" ]);
});
it("preserves explicit audioAsVoice payload replies when replyToMode is off" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "voice note" ,
mediaUrls: ["https://example.com/voice.ogg ", "https://example.com/extra.png"],
audioAsVoice: true ,
},
accountId: "default" ,
replyToId: "explicit-reply-1" ,
replyToMode: "off" ,
});
expect(
(hoisted.sendVoiceMessageDiscordMock.mock.calls[0 ]?.[2 ] as { replyTo?: unknown } | undefined)
?.replyTo,
).toBe("explicit-reply-1" );
expect(
hoisted.sendMessageDiscordMock.mock.calls.map(
(call) => (call[2 ] as { replyTo?: unknown } | undefined)?.replyTo,
),
).toEqual(["explicit-reply-1" , "explicit-reply-1" ]);
});
it("sends video captions as text before a media-only video follow-up" , async () => {
await discordOutbound.sendMedia?.({
cfg: {},
to: "channel:123456" ,
text: "rendered clip" ,
mediaUrl: "/tmp/render.mp4" ,
accountId: "default" ,
replyToId: "reply-1" ,
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"rendered clip" ,
expect.objectContaining({
accountId: "default" ,
replyTo: "reply-1" ,
}),
);
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"" ,
expect.objectContaining({
accountId: "default" ,
mediaUrl: "/tmp/render.mp4" ,
}),
);
});
it("touches bound thread activity after shared outbound delivery succeeds" , async () => {
const touchThread = vi.fn();
hoisted.getThreadBindingManagerMock.mockReturnValue({
getByThreadId: () => ({ threadId: "thread-1" }),
touchThread,
});
await discordOutbound.afterDeliverPayload?.({
cfg: {},
target: {
channel: "discord" ,
to: "channel:parent-1" ,
accountId: "default" ,
threadId: "thread-1" ,
},
payload: { text: "delivered" },
results: [{ channel: "discord" , messageId: "msg-1" }],
});
expect(touchThread).toHaveBeenCalledWith({ threadId: "thread-1" });
});
it("sends component payload media sequences with the component message first" , async () => {
hoisted.sendDiscordComponentMessageMock.mockResolvedValueOnce({
messageId: "component-1" ,
channelId: "ch-1" ,
});
hoisted.sendMessageDiscordMock.mockResolvedValueOnce({
messageId: "msg-2" ,
channelId: "ch-1" ,
});
const payload = await discordOutbound.renderPresentation?.({
payload: {
text: "hello" ,
mediaUrls: ["https://example.com/1.png ", "https://example.com/2.png"],
},
presentation: {
blocks: [{ type: "buttons" , buttons: [{ label: "Open" , value: "open" }] }],
},
ctx: {
cfg: {},
to: "channel:123456" ,
},
} as never);
if (!payload) {
throw new Error("expected Discord presentation payload" );
}
const result = await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload,
accountId: "default" ,
mediaLocalRoots: ["/tmp/media" ],
replyToId: "reply-1" ,
replyToMode: "first" ,
});
expect(hoisted.sendDiscordComponentMessageMock).toHaveBeenCalledWith(
"channel:123456" ,
expect.objectContaining({ text: "hello" }),
expect.objectContaining({
mediaUrl: "https://example.com/1.png ",
mediaLocalRoots: ["/tmp/media" ],
accountId: "default" ,
replyTo: "reply-1" ,
}),
);
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"" ,
expect.objectContaining({
mediaUrl: "https://example.com/2.png ",
mediaLocalRoots: ["/tmp/media" ],
accountId: "default" ,
replyTo: undefined,
}),
);
expect(result).toEqual({
channel: "discord" ,
messageId: "msg-2" ,
channelId: "ch-1" ,
});
});
it("keeps replyToId on every internal component media send when replyToMode is all" , async () => {
const payload = await discordOutbound.renderPresentation?.({
payload: {
text: "hello" ,
mediaUrls: ["https://example.com/1.png ", "https://example.com/2.png"],
},
presentation: {
blocks: [{ type: "buttons" , buttons: [{ label: "Open" , value: "open" }] }],
},
ctx: {
cfg: {},
to: "channel:123456" ,
},
} as never);
if (!payload) {
throw new Error("expected Discord presentation payload" );
}
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload,
accountId: "default" ,
replyToId: "reply-1" ,
replyToMode: "all" ,
});
expect(
(
hoisted.sendDiscordComponentMessageMock.mock.calls[0 ]?.[2 ] as
| { replyTo?: unknown }
| undefined
)?.replyTo,
).toBe("reply-1" );
expect(
(hoisted.sendMessageDiscordMock.mock.calls[0 ]?.[2 ] as { replyTo?: unknown } | undefined)
?.replyTo,
).toBe("reply-1" );
});
it("preserves explicit component payload replies when replyToMode is off" , async () => {
const payload = await discordOutbound.renderPresentation?.({
payload: {
text: "hello" ,
mediaUrls: ["https://example.com/1.png ", "https://example.com/2.png"],
},
presentation: {
blocks: [{ type: "buttons" , buttons: [{ label: "Open" , value: "open" }] }],
},
ctx: {
cfg: {},
to: "channel:123456" ,
},
} as never);
if (!payload) {
throw new Error("expected Discord presentation payload" );
}
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload,
accountId: "default" ,
replyToId: "explicit-reply-1" ,
replyToMode: "off" ,
});
expect(
(
hoisted.sendDiscordComponentMessageMock.mock.calls[0 ]?.[2 ] as
| { replyTo?: unknown }
| undefined
)?.replyTo,
).toBe("explicit-reply-1" );
expect(
(hoisted.sendMessageDiscordMock.mock.calls[0 ]?.[2 ] as { replyTo?: unknown } | undefined)
?.replyTo,
).toBe("explicit-reply-1" );
});
it("uses explicit maxLinesPerMessage in its adapter chunker" , () => {
expect(
discordOutbound.chunker?.("line one\nline two\nline three" , 2000 , {
formatting: { maxLinesPerMessage: 1 },
}),
).toEqual(["line one" , "line two" , "line three" ]);
});
it("renders channelData Discord components on payload sends" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "native component text" ,
channelData: {
discord: {
components: {
blocks: [{ type: "text" , text: "Native component body" }],
},
},
},
},
accountId: "default" ,
});
expect(hoisted.sendDiscordComponentMessageMock).toHaveBeenCalledWith(
"channel:123456" ,
expect.objectContaining({
text: "native component text" ,
blocks: [{ type: "text" , text: "Native component body" }],
}),
expect.objectContaining({ accountId: "default" }),
);
expect(hoisted.sendMessageDiscordMock).not.toHaveBeenCalled();
});
it("neutralizes approval mentions only for approval payloads" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "Approval @everyone <@123> <#456>" ,
channelData: {
execApproval: {
approvalId: "req-1" ,
approvalSlug: "req-1" ,
},
},
},
accountId: "default" ,
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"Approval @\u200beveryone <@\u200b123> <#\u200b456>" ,
expect.objectContaining({
accountId: "default" ,
}),
);
});
it("uses a single implicit reply for chunked approval payload fallbacks" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "line one\nline two" ,
channelData: {
execApproval: {
approvalId: "req-1" ,
approvalSlug: "req-1" ,
},
},
},
accountId: "default" ,
replyToId: "reply-1" ,
replyToIdSource: "implicit" ,
replyToMode: "first" ,
formatting: { maxLinesPerMessage: 1 },
});
expect(
hoisted.sendMessageDiscordMock.mock.calls.map(
(call) => (call[2 ] as { replyTo?: unknown } | undefined)?.replyTo,
),
).toEqual(["reply-1" , undefined]);
});
it("leaves non-approval mentions unchanged" , async () => {
await discordOutbound.sendPayload?.({
cfg: {},
to: "channel:123456" ,
text: "" ,
payload: {
text: "Hello @everyone" ,
},
accountId: "default" ,
});
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledWith(
"channel:123456" ,
"Hello @everyone" ,
expect.objectContaining({
accountId: "default" ,
}),
);
});
});
Messung V0.5 in Prozent C=99 H=99 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland