fix: break mattermost runtime cycle

This commit is contained in:
Peter Steinberger
2026-03-29 00:43:41 +00:00
parent fcc9fd1623
commit 9e1b524a00
23 changed files with 124 additions and 96 deletions

View File

@@ -1,2 +1,3 @@
export { mattermostPlugin } from "./src/channel.js";
// Keep this barrel helper-only so plugin-sdk facades do not pull the full
// channel plugin (and its runtime state) into tests or other shared surfaces.
export { isMattermostSenderAllowed } from "./src/mattermost/monitor-auth.js";

View File

@@ -1,6 +1,6 @@
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
import { createAccountListHelpers, type OpenClawConfig } from "../runtime-api.js";
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "../secret-input.js";
import type {
MattermostAccountConfig,
@@ -9,6 +9,7 @@ import type {
MattermostReplyToMode,
} from "../types.js";
import { normalizeMattermostBaseUrl } from "./client.js";
import type { OpenClawConfig } from "./runtime-api.js";
export type MattermostTokenSource = "env" | "config" | "none";
export type MattermostBaseUrlSource = "env" | "config" | "none";
@@ -30,11 +31,15 @@ export type ResolvedMattermostAccount = {
blockStreamingCoalesce?: MattermostAccountConfig["blockStreamingCoalesce"];
};
const {
listAccountIds: listMattermostAccountIds,
resolveDefaultAccountId: resolveDefaultMattermostAccountId,
} = createAccountListHelpers("mattermost");
export { listMattermostAccountIds, resolveDefaultMattermostAccountId };
const mattermostAccountHelpers = createAccountListHelpers("mattermost");
export function listMattermostAccountIds(cfg: OpenClawConfig): string[] {
return mattermostAccountHelpers.listAccountIds(cfg);
}
export function resolveDefaultMattermostAccountId(cfg: OpenClawConfig): string {
return mattermostAccountHelpers.resolveDefaultAccountId(cfg);
}
function mergeMattermostAccountConfig(
cfg: OpenClawConfig,

View File

@@ -1,4 +1,3 @@
import type { ChannelDirectoryEntry, OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
import { listMattermostAccountIds, resolveMattermostAccount } from "./accounts.js";
import {
createMattermostClient,
@@ -7,6 +6,7 @@ import {
type MattermostClient,
type MattermostUser,
} from "./client.js";
import type { ChannelDirectoryEntry, OpenClawConfig, RuntimeEnv } from "./runtime-api.js";
export type MattermostDirectoryParams = {
cfg: OpenClawConfig;

View File

@@ -1,8 +1,8 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "./runtime-api.js";
const INTERACTION_MAX_BODY_BYTES = 64 * 1024;
const INTERACTION_BODY_TIMEOUT_MS = 10_000;

View File

@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import type { MattermostInteractiveButtonInput } from "./interactions.js";
import {
loadSessionStore,
normalizeProviderId,
@@ -6,8 +7,7 @@ import {
resolveStoredModelOverride,
type ModelsProviderData,
type OpenClawConfig,
} from "../runtime-api.js";
import type { MattermostInteractiveButtonInput } from "./interactions.js";
} from "./runtime-api.js";
const MATTERMOST_MODEL_PICKER_CONTEXT_KEY = "oc_model_picker";
const MODELS_PAGE_SIZE = 8;

View File

@@ -6,13 +6,17 @@ const resolveAllowlistMatchSimple = vi.hoisted(() => vi.fn());
const resolveControlCommandGate = vi.hoisted(() => vi.fn());
const resolveEffectiveAllowFromLists = vi.hoisted(() => vi.fn());
vi.mock("../runtime-api.js", () => ({
evaluateSenderGroupAccessForPolicy,
isDangerousNameMatchingEnabled,
resolveAllowlistMatchSimple,
resolveControlCommandGate,
resolveEffectiveAllowFromLists,
}));
vi.mock("./runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./runtime-api.js")>();
return {
...actual,
evaluateSenderGroupAccessForPolicy,
isDangerousNameMatchingEnabled,
resolveAllowlistMatchSimple,
resolveControlCommandGate,
resolveEffectiveAllowFromLists,
};
});
describe("mattermost monitor auth", () => {
beforeEach(() => {

View File

@@ -1,13 +1,13 @@
import type { OpenClawConfig } from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import type { MattermostChannel } from "./client.js";
import type { OpenClawConfig } from "./runtime-api.js";
import {
evaluateSenderGroupAccessForPolicy,
isDangerousNameMatchingEnabled,
resolveAllowlistMatchSimple,
resolveControlCommandGate,
resolveEffectiveAllowFromLists,
} from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import type { MattermostChannel } from "./client.js";
} from "./runtime-api.js";
export function normalizeMattermostAllowEntry(entry: string): string {
const trimmed = entry.trim();

View File

@@ -1,4 +1,4 @@
import type { ChatType, OpenClawConfig } from "../runtime-api.js";
import type { ChatType, OpenClawConfig } from "./runtime-api.js";
export function mapMattermostChannelTypeToChatType(channelType?: string | null): ChatType {
if (!channelType) {

View File

@@ -1,9 +1,12 @@
import {
createDedupeCache,
formatInboundFromLabel as formatInboundFromLabelShared,
rawDataToString,
resolveThreadSessionKeys as resolveThreadSessionKeysShared,
type OpenClawConfig,
} from "../runtime-api.js";
export { createDedupeCache, rawDataToString } from "../runtime-api.js";
} from "./runtime-api.js";
export { createDedupeCache, rawDataToString };
export type ResponsePrefixContext = {
model?: string;

View File

@@ -1,15 +1,15 @@
import {
listSkillCommandsForAgents,
parseStrictPositiveInteger,
type OpenClawConfig,
type RuntimeEnv,
} from "../runtime-api.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import {
fetchMattermostUserTeams,
normalizeMattermostBaseUrl,
type MattermostClient,
} from "./client.js";
import {
listSkillCommandsForAgents,
parseStrictPositiveInteger,
type OpenClawConfig,
type RuntimeEnv,
} from "./runtime-api.js";
import {
DEFAULT_COMMAND_SPECS,
isSlashCommandsEnabled,

View File

@@ -1,9 +1,9 @@
import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared";
import { z } from "openclaw/plugin-sdk/zod";
import WebSocket from "ws";
import type { ChannelAccountSnapshot, RuntimeEnv } from "../runtime-api.js";
import { MattermostPostSchema, type MattermostPost } from "./client.js";
import { rawDataToString } from "./monitor-helpers.js";
import type { ChannelAccountSnapshot, RuntimeEnv } from "./runtime-api.js";
export type MattermostEventPayload = {
event?: string;

View File

@@ -1,33 +1,3 @@
import type {
ChannelAccountSnapshot,
ChatType,
OpenClawConfig,
ReplyPayload,
RuntimeEnv,
} from "../runtime-api.js";
import {
buildAgentMediaPayload,
buildModelsProviderData,
DM_GROUP_ACCESS_REASON,
createChannelPairingController,
createChannelReplyPipeline,
logInboundDrop,
logTypingFailure,
buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntryIfEnabled,
isDangerousNameMatchingEnabled,
registerPluginHttpRoute,
resolveControlCommandGate,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveChannelMediaMaxBytes,
warnMissingProviderGroupPolicyFallbackOnce,
type HistoryEntry,
} from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
import { resolveMattermostAccount, resolveMattermostReplyToMode } from "./accounts.js";
import {
@@ -82,6 +52,36 @@ import {
} from "./monitor-websocket.js";
import { runWithReconnect } from "./reconnect.js";
import { deliverMattermostReplyPayload } from "./reply-delivery.js";
import type {
ChannelAccountSnapshot,
ChatType,
OpenClawConfig,
ReplyPayload,
RuntimeEnv,
} from "./runtime-api.js";
import {
buildAgentMediaPayload,
buildModelsProviderData,
DM_GROUP_ACCESS_REASON,
createChannelPairingController,
createChannelReplyPipeline,
logInboundDrop,
logTypingFailure,
buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled,
DEFAULT_GROUP_HISTORY_LIMIT,
recordPendingHistoryEntryIfEnabled,
isDangerousNameMatchingEnabled,
registerPluginHttpRoute,
resolveControlCommandGate,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveChannelMediaMaxBytes,
warnMissingProviderGroupPolicyFallbackOnce,
type HistoryEntry,
} from "./runtime-api.js";
import { sendMessageMattermost } from "./send.js";
import { cleanupSlashCommands } from "./slash-commands.js";
import { deactivateSlashCommands, getSlashCommandState } from "./slash-state.js";

View File

@@ -1,5 +1,5 @@
import type { BaseProbeResult } from "../runtime-api.js";
import { normalizeMattermostBaseUrl, readMattermostError, type MattermostUser } from "./client.js";
import type { BaseProbeResult } from "./runtime-api.js";
export type MattermostProbe = BaseProbeResult & {
status?: number | null;

View File

@@ -1,4 +1,3 @@
import type { OpenClawConfig } from "../runtime-api.js";
import { resolveMattermostAccount } from "./accounts.js";
import {
createMattermostClient,
@@ -6,6 +5,7 @@ import {
type MattermostClient,
type MattermostFetch,
} from "./client.js";
import type { OpenClawConfig } from "./runtime-api.js";
type Result = { ok: true } | { ok: false; error: string };
type ReactionParams = {

View File

@@ -2,8 +2,12 @@ import {
deliverTextOrMediaReply,
resolveSendableOutboundReplyParts,
} from "openclaw/plugin-sdk/reply-payload";
import type { OpenClawConfig, PluginRuntime, ReplyPayload } from "../runtime-api.js";
import { getAgentScopedMediaLocalRoots } from "../runtime-api.js";
import {
getAgentScopedMediaLocalRoots,
type OpenClawConfig,
type PluginRuntime,
type ReplyPayload,
} from "./runtime-api.js";
type MarkdownTableMode = Parameters<PluginRuntime["channel"]["text"]["convertMarkdownTables"]>[1];

View File

@@ -1,6 +1,5 @@
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
import { loadOutboundMediaFromUrl, type OpenClawConfig } from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
import { resolveMattermostAccount } from "./accounts.js";
import {
@@ -22,6 +21,7 @@ import {
setInteractionSecret,
type MattermostInteractiveButtonInput,
} from "./interactions.js";
import { loadOutboundMediaFromUrl, type OpenClawConfig } from "./runtime-api.js";
import { isMattermostId, resolveMattermostOpaqueTarget } from "./target-resolution.js";
export type MattermostSendOpts = {

View File

@@ -39,14 +39,18 @@ const mockState = vi.hoisted(() => ({
normalizeMattermostAllowList: vi.fn((value: unknown) => value),
}));
vi.mock("openclaw/plugin-sdk/mattermost", () => ({
buildModelsProviderData: mockState.buildModelsProviderData,
createReplyPrefixOptions: vi.fn(() => ({})),
createTypingCallbacks: vi.fn(() => ({ onReplyStart: vi.fn() })),
isRequestBodyLimitError: vi.fn(() => false),
logTypingFailure: vi.fn(),
readRequestBodyWithLimit: mockState.readRequestBodyWithLimit,
}));
vi.mock("./runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./runtime-api.js")>();
return {
...actual,
buildModelsProviderData: mockState.buildModelsProviderData,
createReplyPrefixOptions: vi.fn(() => ({})),
createTypingCallbacks: vi.fn(() => ({ onReplyStart: vi.fn() })),
isRequestBodyLimitError: vi.fn(() => false),
logTypingFailure: vi.fn(),
readRequestBodyWithLimit: mockState.readRequestBodyWithLimit,
};
});
vi.mock("../runtime.js", () => ({
getMattermostRuntime: () => ({

View File

@@ -7,16 +7,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import {
buildModelsProviderData,
createChannelReplyPipeline,
isRequestBodyLimitError,
logTypingFailure,
readRequestBodyWithLimit,
type OpenClawConfig,
type ReplyPayload,
type RuntimeEnv,
} from "../runtime-api.js";
import { getMattermostRuntime } from "../runtime.js";
import {
createMattermostClient,
@@ -37,6 +27,16 @@ import {
normalizeMattermostAllowList,
} from "./monitor-auth.js";
import { deliverMattermostReplyPayload } from "./reply-delivery.js";
import {
buildModelsProviderData,
createChannelReplyPipeline,
isRequestBodyLimitError,
logTypingFailure,
readRequestBodyWithLimit,
type OpenClawConfig,
type ReplyPayload,
type RuntimeEnv,
} from "./runtime-api.js";
import { sendMessageMattermost } from "./send.js";
import {
parseSlashCommandPayload,

View File

@@ -10,9 +10,9 @@
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawPluginApi } from "../runtime-api.js";
import type { MattermostConfig } from "../types.js";
import type { ResolvedMattermostAccount } from "./accounts.js";
import type { OpenClawPluginApi } from "./runtime-api.js";
import { resolveSlashCommandConfig, type MattermostRegisteredCommand } from "./slash-commands.js";
import { createSlashCommandHttpHandler } from "./slash-http.js";
@@ -87,8 +87,8 @@ export function activateSlashCommands(params: {
registeredCommands: MattermostRegisteredCommand[];
triggerMap?: Map<string, string>;
api: {
cfg: import("../runtime-api.js").OpenClawConfig;
runtime: import("../runtime-api.js").RuntimeEnv;
cfg: import("./runtime-api.js").OpenClawConfig;
runtime: import("./runtime-api.js").RuntimeEnv;
};
log?: (msg: string) => void;
}) {

View File

@@ -1,10 +1,10 @@
import type { OpenClawConfig } from "../runtime-api.js";
import { resolveMattermostAccount } from "./accounts.js";
import {
createMattermostClient,
fetchMattermostUser,
normalizeMattermostBaseUrl,
} from "./client.js";
import type { OpenClawConfig } from "./runtime-api.js";
export type MattermostOpaqueTargetResolution = {
kind: "user" | "channel";

View File

@@ -19,9 +19,13 @@ async function loadExecApprovalSurfaceModule() {
normalizeMessageChannelMock.mockImplementation((value?: string | null) =>
typeof value === "string" ? value.trim().toLowerCase() : undefined,
);
vi.doMock("../config/config.js", () => ({
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
}));
vi.doMock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
};
});
vi.doMock("../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args),

View File

@@ -2,9 +2,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.hoisted(() => vi.fn());
vi.mock("../config/config.js", () => ({
loadConfig: () => loadConfigMock(),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => loadConfigMock(),
};
});
const originalArgv = process.argv;

View File

@@ -96,4 +96,3 @@ export { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
export { createChannelPairingController } from "./channel-pairing.js";
export { isRequestBodyLimitError, readRequestBodyWithLimit } from "../infra/http-body.js";
export { isMattermostSenderAllowed } from "./mattermost-policy.js";