fix: gate setup-only plugin side effects

This commit is contained in:
Peter Steinberger 2026-03-16 01:05:06 +00:00
parent ae6ee73097
commit 59bcac472e
14 changed files with 106 additions and 9 deletions

View File

@ -12,6 +12,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setDiscordRuntime(api.runtime);
api.registerChannel({ plugin: discordPlugin });
if (api.registrationMode !== "full") {
return;
}
registerDiscordSubagentHooks(api);
},
};

View File

@ -46,9 +46,11 @@ const {
resolveDiscordAllowlistConfigMock,
resolveNativeCommandsEnabledMock,
resolveNativeSkillsEnabledMock,
shouldLogVerboseMock,
voiceRuntimeModuleLoadedMock,
} = vi.hoisted(() => {
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
const shouldLogVerboseMock = vi.fn(() => false);
return {
clientHandleDeployRequestMock: vi.fn(async () => undefined),
clientConstructorOptionsMock: vi.fn(),
@ -110,6 +112,7 @@ const {
})),
resolveNativeCommandsEnabledMock: vi.fn(() => true),
resolveNativeSkillsEnabledMock: vi.fn(() => false),
shouldLogVerboseMock,
voiceRuntimeModuleLoadedMock: vi.fn(),
};
});
@ -211,7 +214,7 @@ vi.mock("../../../../src/config/config.js", () => ({
vi.mock("../../../../src/globals.js", () => ({
danger: (v: string) => v,
logVerbose: vi.fn(),
shouldLogVerbose: () => false,
shouldLogVerbose: shouldLogVerboseMock,
warn: (v: string) => v,
}));
@ -435,6 +438,7 @@ describe("monitorDiscordProvider", () => {
});
resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true);
resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false);
shouldLogVerboseMock.mockClear().mockReturnValue(false);
voiceRuntimeModuleLoadedMock.mockClear();
});
@ -842,6 +846,7 @@ describe("monitorDiscordProvider", () => {
emitter.emit("debug", "WebSocket connection opened");
return { id: "bot-1", username: "Molty" };
});
shouldLogVerboseMock.mockReturnValue(true);
await monitorDiscordProvider({
config: baseConfig(),
@ -861,4 +866,17 @@ describe("monitorDiscordProvider", () => {
),
).toBe(true);
});
it("keeps Discord startup chatter quiet by default", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
const runtime = baseRuntime();
await monitorDiscordProvider({
config: baseConfig(),
runtime,
});
const messages = vi.mocked(runtime.log).mock.calls.map((call) => String(call[0]));
expect(messages.some((msg) => msg.includes("discord startup ["))).toBe(false);
});
});

View File

@ -273,14 +273,18 @@ async function deployDiscordCommands(params: {
body === undefined
? undefined
: Buffer.byteLength(typeof body === "string" ? body : JSON.stringify(body), "utf8");
params.runtime.log?.(
`discord startup [${accountId}] deploy-rest:put:start ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path}${typeof commandCount === "number" ? ` commands=${commandCount}` : ""}${typeof bodyBytes === "number" ? ` bytes=${bodyBytes}` : ""}`,
);
if (shouldLogVerbose()) {
params.runtime.log?.(
`discord startup [${accountId}] deploy-rest:put:start ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path}${typeof commandCount === "number" ? ` commands=${commandCount}` : ""}${typeof bodyBytes === "number" ? ` bytes=${bodyBytes}` : ""}`,
);
}
try {
const result = await originalPut(path, data, query);
params.runtime.log?.(
`discord startup [${accountId}] deploy-rest:put:done ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}`,
);
if (shouldLogVerbose()) {
params.runtime.log?.(
`discord startup [${accountId}] deploy-rest:put:done ${Math.max(0, Date.now() - startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}`,
);
}
return result;
} catch (err) {
params.runtime.error?.(
@ -359,6 +363,9 @@ function logDiscordStartupPhase(params: {
gateway?: GatewayPlugin;
details?: string;
}) {
if (!shouldLogVerbose()) {
return;
}
const elapsedMs = Math.max(0, Date.now() - params.startAt);
const suffix = [params.details, formatDiscordStartupGatewayState(params.gateway)]
.filter((value): value is string => Boolean(value))
@ -768,6 +775,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const lifecycleGateway = client.getPlugin<GatewayPlugin>("gateway");
earlyGatewayEmitter = getDiscordGatewayEmitter(lifecycleGateway);
onEarlyGatewayDebug = (msg: unknown) => {
if (!shouldLogVerbose()) {
return;
}
runtime.log?.(
`discord startup [${account.accountId}] gateway-debug ${Math.max(0, Date.now() - startupStartedAt)}ms ${String(msg)}`,
);

View File

@ -54,6 +54,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setFeishuRuntime(api.runtime);
api.registerChannel({ plugin: feishuPlugin });
if (api.registrationMode !== "full") {
return;
}
registerFeishuSubagentHooks(api);
registerFeishuDocTools(api);
registerFeishuChatTools(api);

View File

@ -12,6 +12,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setLineRuntime(api.runtime);
api.registerChannel({ plugin: linePlugin });
if (api.registrationMode !== "full") {
return;
}
registerLineCardCommand(api);
},
};

View File

@ -32,6 +32,7 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
id: "lobster",
name: "lobster",
source: "test",
registrationMode: "full",
config: {},
pluginConfig: {},
// oxlint-disable-next-line typescript/no-explicit-any

View File

@ -0,0 +1,43 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost";
import { describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../test-utils/plugin-api.js";
import plugin from "./index.js";
function createApi(
registrationMode: OpenClawPluginApi["registrationMode"],
registerHttpRoute = vi.fn(),
): OpenClawPluginApi {
return createTestPluginApi({
id: "mattermost",
name: "Mattermost",
source: "test",
config: {},
runtime: {} as OpenClawPluginApi["runtime"],
registrationMode,
registerHttpRoute,
});
}
describe("mattermost plugin register", () => {
it("skips slash callback registration in setup-only mode", () => {
const registerHttpRoute = vi.fn();
plugin.register(createApi("setup-only", registerHttpRoute));
expect(registerHttpRoute).not.toHaveBeenCalled();
});
it("registers slash callback routes in full mode", () => {
const registerHttpRoute = vi.fn();
plugin.register(createApi("full", registerHttpRoute));
expect(registerHttpRoute).toHaveBeenCalledTimes(1);
expect(registerHttpRoute).toHaveBeenCalledWith(
expect.objectContaining({
path: "/api/channels/mattermost/command",
auth: "plugin",
}),
);
});
});

View File

@ -12,6 +12,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setMattermostRuntime(api.runtime);
api.registerChannel({ plugin: mattermostPlugin });
if (api.registrationMode !== "full") {
return;
}
// Register the HTTP route for slash command callbacks.
// The actual command registration with MM happens in the monitor

View File

@ -14,6 +14,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setNostrRuntime(api.runtime);
api.registerChannel({ plugin: nostrPlugin });
if (api.registrationMode !== "full") {
return;
}
// Register HTTP handler for profile management
const httpHandler = createNostrProfileHttpHandler({

View File

@ -5,6 +5,7 @@ type TestPluginApiInput = Partial<OpenClawPluginApi> &
export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi {
return {
registrationMode: "full",
logger: { info() {}, warn() {}, error() {}, debug() {} },
registerTool() {},
registerHook() {},

View File

@ -138,6 +138,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setTlonRuntime(api.runtime);
api.registerChannel({ plugin: tlonPlugin });
if (api.registrationMode !== "full") {
return;
}
api.logger.debug?.("[tlon] Registering tlon tool");
api.registerTool({

View File

@ -12,6 +12,9 @@ const plugin = {
register(api: OpenClawPluginApi) {
setZalouserRuntime(api.runtime);
api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
if (api.registrationMode !== "full") {
return;
}
api.registerTool({
name: "zalouser",

View File

@ -43,6 +43,7 @@ import type {
PluginLogger,
PluginOrigin,
PluginKind,
PluginRegistrationMode,
PluginHookName,
PluginHookHandlerMap,
PluginHookRegistration as TypedPluginHookRegistration,
@ -186,8 +187,6 @@ type PluginTypedHookPolicy = {
allowPromptInjection?: boolean;
};
type PluginRegistrationMode = "full" | "setup-only";
const constrainLegacyPromptInjectionHook = (
handler: PluginHookHandlerMap["before_agent_start"],
): PluginHookHandlerMap["before_agent_start"] => {
@ -734,6 +733,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
description: record.description,
source: record.source,
rootDir: record.rootDir,
registrationMode,
config: params.config,
pluginConfig: params.pluginConfig,
runtime: registryParams.runtime,

View File

@ -839,6 +839,8 @@ export type OpenClawPluginModule =
| OpenClawPluginDefinition
| ((api: OpenClawPluginApi) => void | Promise<void>);
export type PluginRegistrationMode = "full" | "setup-only";
export type OpenClawPluginApi = {
id: string;
name: string;
@ -846,6 +848,7 @@ export type OpenClawPluginApi = {
description?: string;
source: string;
rootDir?: string;
registrationMode: PluginRegistrationMode;
config: OpenClawConfig;
pluginConfig?: Record<string, unknown>;
runtime: PluginRuntime;