perf(slack): narrow runtime-setter + lazy-load 4 modules + narrow 2 SDK surfaces (#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
This commit is contained in:
Alex Knight
2026-04-22 16:42:43 +10:00
committed by GitHub
parent fd2c883673
commit 201385548c
11 changed files with 574 additions and 17 deletions

View File

@@ -14,7 +14,7 @@ export default defineBundledChannelEntry({
exportName: "channelSecrets",
},
runtime: {
specifier: "./runtime-api.js",
specifier: "./runtime-setter-api.js",
exportName: "setSlackRuntime",
},
});

View File

@@ -0,0 +1,4 @@
// Narrow entry point for registerSlackPluginHttpRoutes — avoids pulling in
// the full runtime-api barrel (~284KB, 13 chunks) during plugin register().
// Mirrors the runtime-setter-api.ts split.
export { registerSlackPluginHttpRoutes } from "./src/http/plugin-routes.js";

View File

@@ -6,7 +6,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contra
function registerSlackPluginHttpRoutes(api: OpenClawPluginApi): void {
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
specifier: "./runtime-api.js",
specifier: "./http-routes-api.js",
exportName: "registerSlackPluginHttpRoutes",
});
register(api);
@@ -26,7 +26,7 @@ export default defineBundledChannelEntry({
exportName: "channelSecrets",
},
runtime: {
specifier: "./runtime-api.js",
specifier: "./runtime-setter-api.js",
exportName: "setSlackRuntime",
},
accountInspect: {

View File

@@ -0,0 +1,3 @@
// Narrow entry point for setSlackRuntime — avoids pulling in the full
// runtime-api barrel (284KB, 29 chunks) during plugin register().
export { setSlackRuntime } from "./src/runtime.js";

View File

@@ -0,0 +1,318 @@
// Regression tests for the lazy-loading boundaries introduced for Slack
// startup-perf work (see PR #69317). Each test asserts both:
// - that the lazy module is reached (call mocks fire), and
// - that the inputs forwarded into the lazy module are correct, and
// - that the lazy module's return value is propagated back through the
// plugin surface unchanged.
//
// Together these guard against:
// - dynamic-import path/specifier drift on cold paths,
// - silent contract drift between the channel and its lazy modules,
// - and accidental loss of the perf intent (re-introducing eager imports
// without updating the seam).
import { beforeEach, describe, expect, it, vi } from "vitest";
import { slackPlugin } from "./channel.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { setSlackRuntime } from "./runtime.js";
// --- Hoisted mocks for lazy seams ------------------------------------------------
const collectAuditFindingsMock = vi.hoisted(() => vi.fn());
const fetchSlackScopesMock = vi.hoisted(() => vi.fn());
const resolveTargetsWithOptionalTokenMock = vi.hoisted(() => vi.fn());
const buildPassiveProbedChannelStatusSummaryMock = vi.hoisted(() => vi.fn());
vi.mock("./security-audit.js", () => ({
collectSlackSecurityAuditFindings: collectAuditFindingsMock,
}));
vi.mock("./scopes.js", () => ({
fetchSlackScopes: fetchSlackScopesMock,
}));
vi.mock("openclaw/plugin-sdk/target-resolver-runtime", async (orig) => {
// Preserve any sibling exports so importers that touch unrelated helpers
// do not break; only override the function the channel actually calls.
const original = (await orig()) as Record<string, unknown>;
return {
...original,
resolveTargetsWithOptionalToken: resolveTargetsWithOptionalTokenMock,
};
});
vi.mock("openclaw/plugin-sdk/extension-shared", async (orig) => {
const original = (await orig()) as Record<string, unknown>;
return {
...original,
buildPassiveProbedChannelStatusSummary: buildPassiveProbedChannelStatusSummaryMock,
};
});
// --- Test setup -----------------------------------------------------------------
beforeEach(() => {
collectAuditFindingsMock.mockReset();
fetchSlackScopesMock.mockReset();
resolveTargetsWithOptionalTokenMock.mockReset();
buildPassiveProbedChannelStatusSummaryMock.mockReset();
setSlackRuntime({ channel: { slack: {} } } as never);
});
function makeMinimalSlackConfig(
opts: { botToken?: string; userToken?: string } = {},
): OpenClawConfig {
const slack: Record<string, unknown> = {};
if (opts.botToken !== undefined) {
slack.botToken = opts.botToken;
}
if (opts.userToken !== undefined) {
slack.userToken = opts.userToken;
}
return { channels: { slack } } as OpenClawConfig;
}
// --- Status: buildChannelSummary -------------------------------------------------
describe("slackPlugin.status.buildChannelSummary lazy SDK forwarding", () => {
it("calls the lazy extension-shared SDK helper with the snapshot and token sources, and returns its output unchanged", async () => {
const buildChannelSummary = slackPlugin.status?.buildChannelSummary;
if (!buildChannelSummary) {
throw new Error("slackPlugin.status.buildChannelSummary should be exposed");
}
const sentinelSummary = { sentinel: "passive-summary" };
buildPassiveProbedChannelStatusSummaryMock.mockReturnValue(sentinelSummary);
const snapshot = {
accountId: "default",
configured: true,
enabled: true,
botTokenSource: "config" as const,
appTokenSource: "config" as const,
extra: { custom: 1 },
};
const result = await buildChannelSummary({
account: { accountId: "default" } as never,
snapshot,
cfg: makeMinimalSlackConfig({ botToken: "xoxb-test" }),
runtime: undefined,
} as never);
expect(buildPassiveProbedChannelStatusSummaryMock).toHaveBeenCalledTimes(1);
const [forwardedSnapshot, forwardedExtras] =
buildPassiveProbedChannelStatusSummaryMock.mock.calls[0] ?? [];
// Snapshot must be forwarded by reference / structurally intact.
expect(forwardedSnapshot).toBe(snapshot);
// The channel must forward the (possibly fallback'd) token sources.
expect(forwardedExtras).toEqual({ botTokenSource: "config", appTokenSource: "config" });
// The SDK return value must be propagated through unchanged.
expect(result).toBe(sentinelSummary);
});
it("falls back to 'none' for missing token sources before forwarding to the SDK helper", async () => {
const buildChannelSummary = slackPlugin.status?.buildChannelSummary;
if (!buildChannelSummary) {
throw new Error("slackPlugin.status.buildChannelSummary should be exposed");
}
buildPassiveProbedChannelStatusSummaryMock.mockReturnValue({ sentinel: true });
await buildChannelSummary({
account: { accountId: "default" } as never,
snapshot: { accountId: "default", configured: false, enabled: true } as never,
cfg: makeMinimalSlackConfig(),
runtime: undefined,
} as never);
const [, forwardedExtras] = buildPassiveProbedChannelStatusSummaryMock.mock.calls[0] ?? [];
expect(forwardedExtras).toEqual({ botTokenSource: "none", appTokenSource: "none" });
});
});
// --- Status: buildCapabilitiesDiagnostics ---------------------------------------
describe("slackPlugin.status.buildCapabilitiesDiagnostics lazy scopes loader", () => {
it("invokes fetchSlackScopes once when only a bot token is present", async () => {
const buildDiagnostics = slackPlugin.status?.buildCapabilitiesDiagnostics;
if (!buildDiagnostics) {
throw new Error("slackPlugin.status.buildCapabilitiesDiagnostics should be exposed");
}
fetchSlackScopesMock.mockResolvedValue({ ok: true, scopes: ["chat:write"] });
const cfg = makeMinimalSlackConfig({ botToken: "xoxb-bot" });
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await buildDiagnostics({ account, timeoutMs: 1234, cfg } as never);
expect(fetchSlackScopesMock).toHaveBeenCalledTimes(1);
expect(fetchSlackScopesMock).toHaveBeenCalledWith("xoxb-bot", 1234);
expect(result?.details).toMatchObject({
botScopes: { ok: true, scopes: ["chat:write"] },
});
expect(result?.lines?.length ?? 0).toBeGreaterThan(0);
});
it("invokes fetchSlackScopes twice (bot and user) when both tokens are present", async () => {
const buildDiagnostics = slackPlugin.status?.buildCapabilitiesDiagnostics;
if (!buildDiagnostics) {
throw new Error("slackPlugin.status.buildCapabilitiesDiagnostics should be exposed");
}
fetchSlackScopesMock
.mockResolvedValueOnce({ ok: true, scopes: ["chat:write"] })
.mockResolvedValueOnce({ ok: true, scopes: ["users:read"] });
const cfg = makeMinimalSlackConfig({ botToken: "xoxb-bot", userToken: "xoxp-user" });
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await buildDiagnostics({ account, timeoutMs: 5000, cfg } as never);
expect(fetchSlackScopesMock).toHaveBeenCalledTimes(2);
expect(fetchSlackScopesMock.mock.calls[0]).toEqual(["xoxb-bot", 5000]);
expect(fetchSlackScopesMock.mock.calls[1]).toEqual(["xoxp-user", 5000]);
expect(result?.details).toMatchObject({
botScopes: { ok: true, scopes: ["chat:write"] },
userScopes: { ok: true, scopes: ["users:read"] },
});
});
it("does not invoke fetchSlackScopes when no bot token is present and reports a missing-token diagnostic", async () => {
const buildDiagnostics = slackPlugin.status?.buildCapabilitiesDiagnostics;
if (!buildDiagnostics) {
throw new Error("slackPlugin.status.buildCapabilitiesDiagnostics should be exposed");
}
const cfg = makeMinimalSlackConfig();
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await buildDiagnostics({ account, timeoutMs: 1000, cfg } as never);
expect(fetchSlackScopesMock).not.toHaveBeenCalled();
expect(result?.details).toMatchObject({
botScopes: { ok: false, error: "Slack bot token missing." },
});
});
});
// --- Security: collectAuditFindings ---------------------------------------------
describe("slackPlugin.security.collectAuditFindings lazy module forwarding", () => {
it("delegates to the lazy security-audit module with the original params and returns its output", async () => {
const collectAuditFindings = slackPlugin.security?.collectAuditFindings;
if (!collectAuditFindings) {
throw new Error("slackPlugin.security.collectAuditFindings should be exposed");
}
const sentinel = [
{
checkId: "test-check",
severity: "info" as const,
title: "t",
detail: "d",
},
];
collectAuditFindingsMock.mockResolvedValue(sentinel);
const cfg = makeMinimalSlackConfig({ botToken: "xoxb-bot" });
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await collectAuditFindings({ cfg, accountId: "default", account } as never);
expect(collectAuditFindingsMock).toHaveBeenCalledTimes(1);
expect(collectAuditFindingsMock.mock.calls[0]?.[0]).toEqual({
cfg,
accountId: "default",
account,
});
expect(result).toBe(sentinel);
});
it("propagates an empty findings array unchanged", async () => {
const collectAuditFindings = slackPlugin.security?.collectAuditFindings;
if (!collectAuditFindings) {
throw new Error("slackPlugin.security.collectAuditFindings should be exposed");
}
collectAuditFindingsMock.mockResolvedValue([]);
const cfg = makeMinimalSlackConfig();
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await collectAuditFindings({ cfg, account } as never);
expect(result).toEqual([]);
});
});
// --- Resolver: resolveTargets ---------------------------------------------------
describe("slackPlugin.resolver.resolveTargets lazy SDK forwarding", () => {
it("forwards user inputs and the configured token to the lazy SDK helper and returns its output", async () => {
const resolveTargets = slackPlugin.resolver?.resolveTargets;
if (!resolveTargets) {
throw new Error("slackPlugin.resolver.resolveTargets should be exposed");
}
const sentinelOutput = [{ input: "U123", resolved: true, id: "U123", note: undefined }];
resolveTargetsWithOptionalTokenMock.mockResolvedValue(sentinelOutput);
const cfg = makeMinimalSlackConfig({ botToken: "xoxb-bot" });
const result = await resolveTargets({
cfg,
accountId: "default",
inputs: ["U123"],
kind: "user",
} as never);
expect(resolveTargetsWithOptionalTokenMock).toHaveBeenCalledTimes(1);
const [params] = resolveTargetsWithOptionalTokenMock.mock.calls[0] ?? [];
expect(params).toMatchObject({
token: "xoxb-bot",
inputs: ["U123"],
missingTokenNote: "missing Slack token",
});
expect(typeof params.resolveWithToken).toBe("function");
expect(typeof params.mapResolved).toBe("function");
expect(result).toBe(sentinelOutput);
});
it("prefers the user token over the bot token when both are configured", async () => {
const resolveTargets = slackPlugin.resolver?.resolveTargets;
if (!resolveTargets) {
throw new Error("slackPlugin.resolver.resolveTargets should be exposed");
}
resolveTargetsWithOptionalTokenMock.mockResolvedValue([]);
await resolveTargets({
cfg: makeMinimalSlackConfig({ botToken: "xoxb-bot", userToken: "xoxp-user" }),
accountId: "default",
inputs: ["U1"],
kind: "user",
} as never);
const [params] = resolveTargetsWithOptionalTokenMock.mock.calls[0] ?? [];
expect(params).toMatchObject({ token: "xoxp-user" });
});
it("uses the same lazy SDK helper for kind='group'", async () => {
const resolveTargets = slackPlugin.resolver?.resolveTargets;
if (!resolveTargets) {
throw new Error("slackPlugin.resolver.resolveTargets should be exposed");
}
resolveTargetsWithOptionalTokenMock.mockResolvedValue([]);
await resolveTargets({
cfg: makeMinimalSlackConfig({ botToken: "xoxb-bot" }),
accountId: "default",
inputs: ["C1"],
kind: "group",
} as never);
expect(resolveTargetsWithOptionalTokenMock).toHaveBeenCalledTimes(1);
const [params] = resolveTargetsWithOptionalTokenMock.mock.calls[0] ?? [];
expect(params).toMatchObject({ token: "xoxb-bot", inputs: ["C1"] });
});
});
// Setup-wizard proxy delegation is unit-tested directly in
// setup-core.lazy-proxy.test.ts so it can be type-safe against the wider
// ChannelSetupWizard contract returned by createSlackSetupWizardProxy.

View File

@@ -13,7 +13,6 @@ import {
createChannelDirectoryAdapter,
createRuntimeDirectoryLiveAdapter,
} from "openclaw/plugin-sdk/directory-runtime";
import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime";
import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing";
@@ -21,7 +20,6 @@ import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/target-resolver-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
resolveDefaultSlackAccountId,
@@ -51,14 +49,11 @@ import {
isSlackInteractiveRepliesEnabled,
} from "./interactive-replies.js";
import { SLACK_TEXT_LIMIT } from "./limits.js";
import { slackOutbound } from "./outbound-adapter.js";
import type { SlackProbe } from "./probe.js";
import { resolveSlackReplyBlocks } from "./reply-blocks.js";
import { getOptionalSlackRuntime } from "./runtime.js";
import { fetchSlackScopes } from "./scopes.js";
import { slackSecurityAdapter } from "./security.js";
import { slackSetupAdapter } from "./setup-core.js";
import { slackSetupWizard } from "./setup-surface.js";
import { createSlackSetupWizardProxy, slackSetupAdapter } from "./setup-core.js";
import {
createSlackPluginBase,
isSlackPluginAccountConfigured,
@@ -68,6 +63,70 @@ import {
import { parseSlackTarget } from "./target-parsing.js";
import { buildSlackThreadingToolContext } from "./threading-tool-context.js";
// Lazy SDK loaders. The dynamic import is hidden behind a string-literal
// module id and typed by a hand-written structural alias so TypeScript does
// not have to crawl the SDK module's type graph just to type the loader.
//
// `openclaw/plugin-sdk/channel-policy` is intentionally NOT lazy here —
// `./group-policy.js` already imports it eagerly, so deferring it from
// `channel.ts` would not change the load graph.
type ExtensionSharedSurface = {
buildPassiveProbedChannelStatusSummary: <TExtra extends object>(
snapshot: {
configured?: boolean;
running?: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
probe?: unknown;
lastProbeAt?: number | null;
},
extra?: TExtra,
) => {
configured: boolean;
running: boolean;
lastStartAt: number | null;
lastStopAt: number | null;
lastError: string | null;
probe: unknown;
lastProbeAt: number | null;
} & TExtra;
};
type TargetResolverRuntimeSurface = {
resolveTargetsWithOptionalToken: <TResult>(params: {
token?: string | null;
inputs: string[];
missingTokenNote: string;
resolveWithToken: (params: { token: string; inputs: string[] }) => Promise<TResult[]>;
mapResolved: (entry: TResult) => {
input: string;
resolved: boolean;
id?: string;
name?: string;
note?: string;
};
}) => Promise<
Array<{ input: string; resolved: boolean; id?: string; name?: string; note?: string }>
>;
};
const EXTENSION_SHARED_MODULE_ID = "openclaw/plugin-sdk/extension-shared";
const TARGET_RESOLVER_RUNTIME_MODULE_ID = "openclaw/plugin-sdk/target-resolver-runtime";
const loadExtensionSharedSdk = createLazyRuntimeModule(
() => import(EXTENSION_SHARED_MODULE_ID) as Promise<ExtensionSharedSurface>,
);
const loadTargetResolverRuntimeSdk = createLazyRuntimeModule(
() => import(TARGET_RESOLVER_RUNTIME_MODULE_ID) as Promise<TargetResolverRuntimeSurface>,
);
const loadSlackSetupSurfaceModule = createLazyRuntimeModule(() => import("./setup-surface.js"));
const loadSlackScopesModule = createLazyRuntimeModule(() => import("./scopes.js"));
const loadSlackOutboundAdapterModule = createLazyRuntimeModule(
() => import("./outbound-adapter.js"),
);
async function resolveSlackHandleAction() {
return (
getOptionalSlackRuntime()?.channel?.slack?.handleSlackAction ??
@@ -258,9 +317,18 @@ async function resolveSlackOutboundSessionRoute(params: {
});
}
// Mirrors `SlackScopesResult` in ./scopes.ts so the type does not pull the
// scopes module back in at module-load time. Keep the two in sync.
type SlackScopesResultShape = {
ok: boolean;
scopes?: string[];
source?: string;
error?: string;
};
function formatSlackScopeDiagnostic(params: {
tokenType: "bot" | "user";
result: Awaited<ReturnType<typeof fetchSlackScopes>>;
result: SlackScopesResultShape;
}) {
const source = params.result.source ? ` (${params.result.source})` : "";
const label = params.tokenType === "user" ? "User scopes" : "Bot scopes";
@@ -293,7 +361,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
>({
base: {
...createSlackPluginBase({
setupWizard: slackSetupWizard,
setupWizard: createSlackSetupWizardProxy(loadSlackSetupSurfaceModule),
setup: slackSetupAdapter,
}),
allowlist: {
@@ -379,6 +447,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
note,
});
const account = resolveSlackAccount({ cfg, accountId });
const { resolveTargetsWithOptionalToken } = await loadTargetResolverRuntimeSdk();
if (kind === "group") {
return resolveTargetsWithOptionalToken({
token:
@@ -418,11 +487,13 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
}),
status: createComputedAccountStatusAdapter<ResolvedSlackAccount, SlackProbe>({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
buildChannelSummary: ({ snapshot }) =>
buildPassiveProbedChannelStatusSummary(snapshot, {
buildChannelSummary: async ({ snapshot }) => {
const { buildPassiveProbedChannelStatusSummary } = await loadExtensionSharedSdk();
return buildPassiveProbedChannelStatusSummary(snapshot, {
botTokenSource: snapshot.botTokenSource ?? "none",
appTokenSource: snapshot.appTokenSource ?? "none",
}),
});
},
probeAccount: async ({ account, timeoutMs }) => {
const token = account.botToken?.trim();
if (!token) {
@@ -447,7 +518,8 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
const details: Record<string, unknown> = {};
const botToken = account.botToken?.trim();
const userToken = account.config.userToken?.trim();
const botScopes = botToken
const { fetchSlackScopes } = await loadSlackScopesModule();
const botScopes: SlackScopesResultShape = botToken
? await fetchSlackScopes(botToken, timeoutMs)
: { ok: false, error: "Slack bot token missing." };
lines.push(formatSlackScopeDiagnostic({ tokenType: "bot", result: botScopes }));
@@ -567,6 +639,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
replyToId: ctx.replyToId,
threadId: ctx.threadId,
});
const { slackOutbound } = await loadSlackOutboundAdapterModule();
return await slackOutbound.sendPayload!({
...ctx,
deps: {

View File

@@ -1,8 +1,8 @@
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type { ResolvedSlackAccount } from "./accounts.js";
import type { ChannelPlugin } from "./channel-api.js";
import { collectSlackSecurityAuditFindings } from "./security-audit.js";
const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount>({
channelKey: "slack",
@@ -36,8 +36,13 @@ const collectSlackSecurityWarnings =
},
});
const loadSlackSecurityAuditModule = createLazyRuntimeModule(() => import("./security-audit.js"));
export const slackSecurityAdapter = {
resolveDmPolicy: resolveSlackDmPolicy,
collectWarnings: collectSlackSecurityWarnings,
collectAuditFindings: collectSlackSecurityAuditFindings,
collectAuditFindings: async (params) => {
const { collectSlackSecurityAuditFindings } = await loadSlackSecurityAuditModule();
return await collectSlackSecurityAuditFindings(params);
},
} satisfies NonNullable<ChannelPlugin<ResolvedSlackAccount>["security"]>;

View File

@@ -0,0 +1,125 @@
// Unit tests for createSlackSetupWizardProxy. The Slack channel plugin
// installs this proxy as setupWizard so the heavy ./setup-surface module is
// only imported when wizard methods that actually need it are invoked.
//
// These tests use a fake loader so the proxy can be tested type-safely
// against the wider ChannelSetupWizard contract, without going through the
// (narrower) ChannelPluginSetupWizard surface exposed on slackPlugin.
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup-runtime";
import { describe, expect, it, vi } from "vitest";
import { createSlackSetupWizardProxy } from "./setup-core.js";
function makeFakeWizard(overrides: Partial<ChannelSetupWizard> = {}): ChannelSetupWizard {
return {
channel: "slack",
status: {
resolveConfigured: vi.fn(async () => ({ configured: false })),
},
credentials: [],
...overrides,
} as ChannelSetupWizard;
}
describe("createSlackSetupWizardProxy", () => {
it("does not load the wizard module just by constructing the proxy", () => {
const loader = vi.fn(async () => ({ slackSetupWizard: makeFakeWizard() }));
const proxy = createSlackSetupWizardProxy(loader);
expect(proxy).toBeDefined();
expect(loader).not.toHaveBeenCalled();
});
it("forwards allowFrom.resolveEntries to the lazily loaded wizard and propagates its result", async () => {
const sentinel = [{ input: "U123", resolved: true, id: "U123" }];
const resolveEntries = vi.fn(async () => sentinel);
// The full ChannelSetupWizardAllowFrom type carries many UI-only fields
// (placeholder, parseId, etc.) that are irrelevant to the proxy's
// delegation contract. Build a minimal stub and cast through unknown so
// the assertion stays focused on resolveEntries forwarding.
const fakeWizard = makeFakeWizard({
allowFrom: {
resolveEntries,
} as unknown as ChannelSetupWizard["allowFrom"],
});
const loader = vi.fn(async () => ({ slackSetupWizard: fakeWizard }));
const proxy = createSlackSetupWizardProxy(loader);
const cfg = { channels: { slack: {} } } as never;
const result = await proxy.allowFrom!.resolveEntries({
cfg,
accountId: "default",
credentialValues: { botToken: "xoxb-bot" },
entries: ["U123"],
});
expect(loader).toHaveBeenCalledTimes(1);
expect(resolveEntries).toHaveBeenCalledTimes(1);
expect(resolveEntries).toHaveBeenCalledWith({
cfg,
accountId: "default",
credentialValues: { botToken: "xoxb-bot" },
entries: ["U123"],
});
expect(result).toBe(sentinel);
});
it("returns unresolved entries without invoking the lazy wizard when allowFrom is absent on the loaded wizard", async () => {
const fakeWizard = makeFakeWizard();
const loader = vi.fn(async () => ({ slackSetupWizard: fakeWizard }));
const proxy = createSlackSetupWizardProxy(loader);
const result = await proxy.allowFrom!.resolveEntries({
cfg: { channels: { slack: {} } } as never,
accountId: "default",
credentialValues: {},
entries: ["U1", "U2"],
});
// The proxy still loads the wizard once to inspect its allowFrom shape,
// then falls back to a "resolved: false" projection of the inputs.
expect(loader).toHaveBeenCalledTimes(1);
expect(result).toEqual([
{ input: "U1", resolved: false, id: null },
{ input: "U2", resolved: false, id: null },
]);
});
it("forwards groupAccess.resolveAllowlist when present and uses the configured fallback otherwise", async () => {
// First: with groupAccess present, the lazy wizard handles resolution.
const groupResolved = ["G1-resolved"];
const resolveAllowlist = vi.fn(async () => groupResolved);
const fakeWithGroupAccess = makeFakeWizard({
groupAccess: {
resolveAllowlist,
} as unknown as ChannelSetupWizard["groupAccess"],
});
const loaderA = vi.fn(async () => ({ slackSetupWizard: fakeWithGroupAccess }));
const proxyA = createSlackSetupWizardProxy(loaderA);
const cfg = { channels: { slack: {} } } as never;
const a = await proxyA.groupAccess!.resolveAllowlist!({
cfg,
accountId: "default",
credentialValues: {},
entries: ["G1"],
prompter: undefined as never,
});
expect(resolveAllowlist).toHaveBeenCalledTimes(1);
expect(a).toBe(groupResolved);
// Second: without groupAccess, the fallback (entries -> entries) is used.
const fakeNoGroupAccess = makeFakeWizard();
const loaderB = vi.fn(async () => ({ slackSetupWizard: fakeNoGroupAccess }));
const proxyB = createSlackSetupWizardProxy(loaderB);
const b = await proxyB.groupAccess!.resolveAllowlist!({
cfg,
accountId: "default",
credentialValues: {},
entries: ["G1", "G2"],
prompter: undefined as never,
});
// createSlackSetupWizardProxy passes (entries) => entries as the fallback.
expect(b).toEqual(["G1", "G2"]);
});
});

View File

@@ -28,6 +28,7 @@
"dist/extensions/qqbot/runtime-api.js",
"dist/extensions/signal/runtime-api.js",
"dist/extensions/slack/runtime-api.js",
"dist/extensions/slack/runtime-setter-api.js",
"dist/extensions/telegram/runtime-api.js",
"dist/extensions/telegram/runtime-setter-api.js",
"dist/extensions/tlon/runtime-api.js",

View File

@@ -165,6 +165,17 @@ describe("bundled plugin metadata", () => {
});
});
it("keeps Slack's narrow runtime-setter sidecar on the bundled public surface", () => {
// Regression for #69317: the bundled channel entry now points its
// runtime.specifier at runtime-setter-api.js to avoid loading the full
// runtime-api barrel during register(). The setter file must therefore
// be discoverable as part of Slack's public surface.
const slack = listRepoBundledPluginMetadata().find((entry) => entry.dirName === "slack");
expectArtifactPresence(slack?.publicSurfaceArtifacts, {
contains: ["runtime-setter-api.js"],
});
});
it("keeps Telegram's narrow runtime setter on the bundled runtime sidecar surface", () => {
const telegram = listRepoBundledPluginMetadata().find((entry) => entry.dirName === "telegram");
expectArtifactPresence(telegram?.publicSurfaceArtifacts, {

View File

@@ -239,6 +239,23 @@ describe("runtime api guardrails", () => {
}
});
it("keeps Slack's narrow runtime-setter entrypoint pinned to a single export", () => {
// Regression for #69317. The bundled channel entry's runtime.specifier
// now points at runtime-setter-api.ts. The whole point of that file is
// to expose ONLY setSlackRuntime so that register() does not pay the
// cost of importing the full runtime-api barrel. If a future change
// re-broadens this file, this test fails so the perf regression is
// surfaced explicitly rather than silently re-introduced.
const setterFile = bundledPluginFile({
rootDir: ROOT_DIR,
pluginId: "slack",
relativePath: "runtime-setter-api.ts",
});
expect(readExportStatements(setterFile)).toEqual([
'export { setSlackRuntime } from "./src/runtime.js";',
]);
});
it("keeps Matrix's narrow runtime-setter entrypoint pinned to a single export", () => {
const setterFile = bundledPluginFile({
rootDir: ROOT_DIR,