refactor: make OutboundSendDeps dynamic with channel-ID keys (#45517)

* refactor: make OutboundSendDeps dynamic with channel-ID keys

Replace hardcoded per-channel send fields (sendTelegram, sendDiscord,
etc.) with a dynamic index-signature type keyed by channel ID. This
unblocks moving channel implementations to extensions without breaking
the outbound dispatch contract.

- OutboundSendDeps and CliDeps are now { [channelId: string]: unknown }
- Each outbound adapter resolves its send fn via bracket access with cast
- Lazy-loading preserved via createLazySender with module cache
- Delete 6 deps-send-*.runtime.ts one-liner re-export files
- Harden guardrail scan against deleted-but-tracked files


* fix: preserve outbound send-deps compatibility

* style: fix formatting issues (import order, extra bracket, trailing whitespace)



* fix: resolve type errors from dynamic OutboundSendDeps in tests and extension

* fix: remove unused OutboundSendDeps import from deliver.test-helpers
This commit is contained in:
scoootscooob
2026-03-14 02:42:21 -07:00
committed by GitHub
parent 0c926a2c5e
commit 7764f717e9
41 changed files with 403 additions and 282 deletions

View File

@@ -8,6 +8,7 @@ import {
sendPollDiscord,
sendWebhookMessageDiscord,
} from "../../../discord/send.js";
import { resolveOutboundSendDep } from "../../../infra/outbound/deliver.js";
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
import type { ChannelOutboundAdapter } from "../types.js";
@@ -100,7 +101,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
return { channel: "discord", ...webhookResult };
}
}
const send = deps?.sendDiscord ?? sendMessageDiscord;
const send =
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
const target = resolveDiscordOutboundTarget({ to, threadId });
const result = await send(target, text, {
verbose: false,
@@ -123,7 +125,8 @@ export const discordOutbound: ChannelOutboundAdapter = {
threadId,
silent,
}) => {
const send = deps?.sendDiscord ?? sendMessageDiscord;
const send =
resolveOutboundSendDep<typeof sendMessageDiscord>(deps, "discord") ?? sendMessageDiscord;
const target = resolveDiscordOutboundTarget({ to, threadId });
const result = await send(target, text, {
verbose: false,

View File

@@ -22,7 +22,7 @@ describe("imessageOutbound", () => {
text: "hello",
accountId: "default",
replyToId: "msg-123",
deps: { sendIMessage },
deps: { imessage: sendIMessage },
});
expect(sendIMessage).toHaveBeenCalledWith(
@@ -50,7 +50,7 @@ describe("imessageOutbound", () => {
mediaLocalRoots: ["/tmp"],
accountId: "acct-1",
replyToId: "msg-456",
deps: { sendIMessage },
deps: { imessage: sendIMessage },
});
expect(sendIMessage).toHaveBeenCalledWith(

View File

@@ -1,12 +1,14 @@
import { sendMessageIMessage } from "../../../imessage/send.js";
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import {
createScopedChannelMediaMaxBytesResolver,
createDirectTextMediaOutbound,
} from "./direct-text-media.js";
function resolveIMessageSender(deps: OutboundSendDeps | undefined) {
return deps?.sendIMessage ?? sendMessageIMessage;
return (
resolveOutboundSendDep<typeof sendMessageIMessage>(deps, "imessage") ?? sendMessageIMessage
);
}
export const imessageOutbound = createDirectTextMediaOutbound({

View File

@@ -26,7 +26,7 @@ describe("signalOutbound", () => {
to: "+15555550123",
text: "hello",
accountId: "work",
deps: { sendSignal },
deps: { signal: sendSignal },
});
expect(sendSignal).toHaveBeenCalledWith(
@@ -52,7 +52,7 @@ describe("signalOutbound", () => {
mediaUrl: "https://example.com/file.jpg",
mediaLocalRoots: ["/tmp/media"],
accountId: "default",
deps: { sendSignal },
deps: { signal: sendSignal },
});
expect(sendSignal).toHaveBeenCalledWith(

View File

@@ -1,4 +1,4 @@
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import { sendMessageSignal } from "../../../signal/send.js";
import {
createScopedChannelMediaMaxBytesResolver,
@@ -6,7 +6,7 @@ import {
} from "./direct-text-media.js";
function resolveSignalSender(deps: OutboundSendDeps | undefined) {
return deps?.sendSignal ?? sendMessageSignal;
return resolveOutboundSendDep<typeof sendMessageSignal>(deps, "signal") ?? sendMessageSignal;
}
export const signalOutbound = createDirectTextMediaOutbound({

View File

@@ -1,3 +1,4 @@
import { resolveOutboundSendDep } from "../../../infra/outbound/deliver.js";
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { parseSlackBlocksInput } from "../../../slack/blocks-input.js";
@@ -56,12 +57,13 @@ async function sendSlackOutboundMessage(params: {
mediaLocalRoots?: readonly string[];
blocks?: NonNullable<Parameters<typeof sendMessageSlack>[2]>["blocks"];
accountId?: string | null;
deps?: { sendSlack?: typeof sendMessageSlack } | null;
deps?: { [channelId: string]: unknown } | null;
replyToId?: string | null;
threadId?: string | number | null;
identity?: OutboundIdentity;
}) {
const send = params.deps?.sendSlack ?? sendMessageSlack;
const send =
resolveOutboundSendDep<typeof sendMessageSlack>(params.deps, "slack") ?? sendMessageSlack;
// Use threadId fallback so routed tool notifications stay in the Slack thread.
const threadTs =
params.replyToId ?? (params.threadId != null ? String(params.threadId) : undefined);

View File

@@ -15,7 +15,7 @@ describe("telegramOutbound", () => {
accountId: "work",
replyToId: "44",
threadId: "55",
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledWith(
@@ -43,7 +43,7 @@ describe("telegramOutbound", () => {
text: "<b>hello</b>",
accountId: "work",
threadId: "12345:99",
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledWith(
@@ -70,7 +70,7 @@ describe("telegramOutbound", () => {
mediaUrl: "https://example.com/a.jpg",
mediaLocalRoots: ["/tmp/media"],
accountId: "default",
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledWith(
@@ -112,7 +112,7 @@ describe("telegramOutbound", () => {
payload,
mediaLocalRoots: ["/tmp/media"],
accountId: "default",
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledTimes(2);

View File

@@ -1,5 +1,5 @@
import type { ReplyPayload } from "../../../auto-reply/types.js";
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import { resolveOutboundSendDep, type OutboundSendDeps } from "../../../infra/outbound/deliver.js";
import type { TelegramInlineButtons } from "../../../telegram/button-types.js";
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
import {
@@ -30,7 +30,9 @@ function resolveTelegramSendContext(params: {
accountId?: string;
};
} {
const send = params.deps?.sendTelegram ?? sendMessageTelegram;
const send =
resolveOutboundSendDep<typeof sendMessageTelegram>(params.deps, "telegram") ??
sendMessageTelegram;
return {
send,
baseOpts: {

View File

@@ -87,7 +87,7 @@ describe("telegramOutbound.sendPayload", () => {
},
},
},
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledTimes(1);
@@ -121,7 +121,7 @@ describe("telegramOutbound.sendPayload", () => {
},
},
},
deps: { sendTelegram },
deps: { telegram: sendTelegram },
});
expect(sendTelegram).toHaveBeenCalledTimes(2);

View File

@@ -1,3 +1,4 @@
import { resolveOutboundSendDep } from "../../infra/outbound/deliver.js";
import type { PluginRuntimeChannel } from "../../plugins/runtime/types-channel.js";
import { escapeRegExp } from "../../utils.js";
import { resolveWhatsAppOutboundTarget } from "../../whatsapp/resolve-outbound-target.js";
@@ -66,7 +67,8 @@ export function createWhatsAppOutboundBase({
if (skipEmptyText && !normalizedText) {
return { channel: "whatsapp", messageId: "" };
}
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
const send =
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
const result = await send(to, normalizedText, {
verbose: false,
cfg,
@@ -85,7 +87,8 @@ export function createWhatsAppOutboundBase({
deps,
gifPlayback,
}) => {
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
const send =
resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp") ?? sendMessageWhatsApp;
const result = await send(to, normalizeText(text), {
verbose: false,
cfg,