fix: resolve channel typing regressions

This commit is contained in:
Peter Steinberger
2026-04-06 17:42:03 +01:00
parent 1880b104ed
commit 6acb43f294
43 changed files with 418 additions and 169 deletions

View File

@@ -8,6 +8,10 @@ type BlueBubblesConfigPatch = {
};
type AccountEnabledMode = boolean | "preserve-or-true";
type BlueBubblesAccountEntry = {
enabled?: boolean;
[key: string]: unknown;
};
function normalizePatch(
patch: BlueBubblesConfigPatch,
@@ -51,7 +55,9 @@ export function applyBlueBubblesConnectionConfig(params: {
};
}
const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId];
const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId] as
| BlueBubblesAccountEntry
| undefined;
const enabled =
params.accountEnabled === "preserve-or-true"
? (currentAccount?.enabled ?? true)

View File

@@ -27,7 +27,13 @@ describe("bluebubbles doctor", () => {
expect(result.config.channels?.bluebubbles?.network).toEqual({
dangerouslyAllowPrivateNetwork: true,
});
expect(result.config.channels?.bluebubbles?.accounts?.default?.network).toEqual({
expect(
(
result.config.channels?.bluebubbles?.accounts?.default as {
network?: { dangerouslyAllowPrivateNetwork?: boolean };
}
)?.network,
).toEqual({
dangerouslyAllowPrivateNetwork: false,
});
});

View File

@@ -201,8 +201,8 @@ export async function sendBlueBubblesMedia(params: {
const maxBytes = resolveChannelMediaMaxBytes({
cfg,
resolveChannelLimitMb: ({ cfg, accountId }) =>
cfg.channels?.bluebubbles?.accounts?.[accountId]?.mediaMaxMb ??
cfg.channels?.bluebubbles?.mediaMaxMb,
(cfg.channels?.bluebubbles?.accounts?.[accountId] as { mediaMaxMb?: number } | undefined)
?.mediaMaxMb ?? cfg.channels?.bluebubbles?.mediaMaxMb,
accountId,
});
const mediaLocalRoots = resolveMediaLocalRoots({ cfg, accountId });

View File

@@ -21,8 +21,11 @@ export function setBlueBubblesDmPolicy(
const existingAllowFrom =
resolvedAccountId === "default"
? cfg.channels?.bluebubbles?.allowFrom
: (cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]?.allowFrom ??
cfg.channels?.bluebubbles?.allowFrom);
: ((
cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId] as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom ?? cfg.channels?.bluebubbles?.allowFrom);
return patchScopedAccountConfig({
cfg,
channelKey: channel,

View File

@@ -36,6 +36,9 @@ async function createBlueBubblesConfigureAdapter() {
docsPath: "/channels/bluebubbles",
blurb: "iMessage via BlueBubbles",
},
capabilities: {
chatTypes: ["direct", "group"],
},
config: {
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
@@ -213,8 +216,13 @@ describe("bluebubbles setup surface", () => {
});
const next = blueBubblesSetupWizard.dmPolicy?.setPolicy(cfg, "open");
const workAccount = next?.channels?.bluebubbles?.accounts?.work as
| {
dmPolicy?: string;
}
| undefined;
expect(next?.channels?.bluebubbles?.dmPolicy).toBe("disabled");
expect(next?.channels?.bluebubbles?.accounts?.work?.dmPolicy).toBe("open");
expect(workAccount?.dmPolicy).toBe("open");
});
it("uses configured defaultAccount when accountId is omitted in account resolution", async () => {
@@ -294,12 +302,15 @@ describe("bluebubbles setup surface", () => {
"work",
);
const workAccount = next?.channels?.bluebubbles?.accounts?.work as
| {
dmPolicy?: string;
allowFrom?: string[];
}
| undefined;
expect(next?.channels?.bluebubbles?.dmPolicy).toBeUndefined();
expect(next?.channels?.bluebubbles?.accounts?.work?.dmPolicy).toBe("open");
expect(next?.channels?.bluebubbles?.accounts?.work?.allowFrom).toEqual([
"user@example.com",
"*",
]);
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["user@example.com", "*"]);
});
});

View File

@@ -371,11 +371,17 @@ describe("feishu setup wizard status", () => {
});
const next = feishuSetupWizard.dmPolicy?.setPolicy?.(cfg as never, "open");
const workAccount = next?.channels?.feishu?.accounts?.work as
| {
dmPolicy?: string;
allowFrom?: string[];
}
| undefined;
expect(next?.channels?.feishu?.dmPolicy).toBeUndefined();
expect(next?.channels?.feishu?.allowFrom).toEqual(["ou_root"]);
expect(next?.channels?.feishu?.accounts?.work?.dmPolicy).toBe("open");
expect(next?.channels?.feishu?.accounts?.work?.allowFrom).toEqual(["ou_work", "*"]);
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["ou_work", "*"]);
});
it("treats env SecretRef appId as not configured when env var is missing", async () => {

View File

@@ -234,8 +234,13 @@ describe("line setup wizard", () => {
});
const next = lineSetupWizard.dmPolicy?.setPolicy(cfg, "open");
const workAccount = next?.channels?.line?.accounts?.work as
| {
dmPolicy?: string;
}
| undefined;
expect(next?.channels?.line?.dmPolicy).toBe("disabled");
expect(next?.channels?.line?.accounts?.work?.dmPolicy).toBe("open");
expect(workAccount?.dmPolicy).toBe("open");
});
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', async () => {
@@ -259,10 +264,16 @@ describe("line setup wizard", () => {
"work",
);
const workAccount = next?.channels?.line?.accounts?.work as
| {
dmPolicy?: string;
allowFrom?: string[];
}
| undefined;
expect(next?.channels?.line?.dmPolicy).toBeUndefined();
expect(next?.channels?.line?.allowFrom).toEqual(["Uroot"]);
expect(next?.channels?.line?.accounts?.work?.dmPolicy).toBe("open");
expect(next?.channels?.line?.accounts?.work?.allowFrom).toEqual(["Uroot", "*"]);
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["Uroot", "*"]);
});
it("uses configured defaultAccount for omitted setup configured state", async () => {

View File

@@ -157,10 +157,24 @@ describe("matrix doctor", () => {
} as never,
});
expect(result.config.channels?.matrix?.groups?.["!ops:example.org"]).toEqual({
const matrixConfig = result.config.channels?.matrix as
| {
groups?: Record<string, unknown>;
accounts?: Record<string, unknown>;
network?: { dangerouslyAllowPrivateNetwork?: boolean };
}
| undefined;
const workAccount = matrixConfig?.accounts?.work as
| {
rooms?: Record<string, unknown>;
network?: { dangerouslyAllowPrivateNetwork?: boolean };
}
| undefined;
expect(matrixConfig?.groups?.["!ops:example.org"]).toEqual({
enabled: true,
});
expect(result.config.channels?.matrix?.accounts?.work?.rooms?.["!legacy:example.org"]).toEqual({
expect(workAccount?.rooms?.["!legacy:example.org"]).toEqual({
enabled: false,
});
expect(result.changes).toEqual(
@@ -193,10 +207,22 @@ describe("matrix doctor", () => {
} as never,
});
expect(result.config.channels?.matrix?.network).toEqual({
const matrixConfig = result.config.channels?.matrix as
| {
accounts?: Record<string, unknown>;
network?: { dangerouslyAllowPrivateNetwork?: boolean };
}
| undefined;
const workAccount = matrixConfig?.accounts?.work as
| {
network?: { dangerouslyAllowPrivateNetwork?: boolean };
}
| undefined;
expect(matrixConfig?.network).toEqual({
dangerouslyAllowPrivateNetwork: true,
});
expect(result.config.channels?.matrix?.accounts?.work?.network).toEqual({
expect(workAccount?.network).toEqual({
dangerouslyAllowPrivateNetwork: false,
});
expect(result.changes).toEqual(

View File

@@ -18,10 +18,9 @@ export function resolveMatrixAckReactionConfig(params: {
channel: "matrix",
accountId: params.accountId ?? undefined,
}).trim();
const ackReactionScope =
accountConfig.ackReactionScope ??
const ackReactionScope = (accountConfig.ackReactionScope ??
matrixConfig?.ackReactionScope ??
params.cfg.messages?.ackReactionScope ??
"group-mentions";
"group-mentions") as MatrixAckReactionScope;
return { ackReaction, ackReactionScope };
}

View File

@@ -62,12 +62,19 @@ describe("matrix onboarding", () => {
expect(result).not.toBe("skip");
if (result !== "skip") {
const opsAccount = result.cfg.channels?.["matrix"]?.accounts?.ops as
| {
enabled?: boolean;
homeserver?: string;
accessToken?: string;
}
| undefined;
expect(result.accountId).toBe("ops");
expect(result.cfg.channels?.["matrix"]?.accounts?.ops).toMatchObject({
expect(opsAccount).toMatchObject({
enabled: true,
});
expect(result.cfg.channels?.["matrix"]?.accounts?.ops?.homeserver).toBeUndefined();
expect(result.cfg.channels?.["matrix"]?.accounts?.ops?.accessToken).toBeUndefined();
expect(opsAccount?.homeserver).toBeUndefined();
expect(opsAccount?.accessToken).toBeUndefined();
}
expect(
confirmMessages.some((message) =>

View File

@@ -441,7 +441,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
resolveGatewayAuthBypassPaths: ({ cfg }) => {
const base = cfg.channels?.mattermost;
const callbackPaths = new Set(
collectMattermostSlashCallbackPaths(base?.commands).filter(
collectMattermostSlashCallbackPaths(
base?.commands as Partial<MattermostSlashCommandConfig> | undefined,
).filter(
(path) =>
path === "/api/channels/mattermost/command" ||
path.startsWith("/api/channels/mattermost/"),

View File

@@ -27,7 +27,13 @@ describe("mattermost doctor", () => {
expect(result.config.channels?.mattermost?.network).toEqual({
dangerouslyAllowPrivateNetwork: true,
});
expect(result.config.channels?.mattermost?.accounts?.work?.network).toEqual({
expect(
(
result.config.channels?.mattermost?.accounts?.work as
| { network?: Record<string, unknown> }
| undefined
)?.network,
).toEqual({
dangerouslyAllowPrivateNetwork: false,
});
});

View File

@@ -13,10 +13,15 @@ import {
function resolveRequireMentionForTest(params: MattermostRequireMentionResolverInput): boolean {
const root = params.cfg.channels?.mattermost;
const accountGroups = root?.accounts?.[params.accountId]?.groups;
const accountGroups = (
root?.accounts?.[params.accountId] as
| { groups?: Record<string, { requireMention?: boolean }> }
| undefined
)?.groups;
const groups = accountGroups ?? root?.groups;
const groupConfig = params.groupId ? groups?.[params.groupId] : undefined;
const defaultGroupConfig = groups?.["*"];
const typedGroups = groups as Record<string, { requireMention?: boolean }> | undefined;
const groupConfig = params.groupId ? typedGroups?.[params.groupId] : undefined;
const defaultGroupConfig = typedGroups?.["*"];
const configMention =
typeof groupConfig?.requireMention === "boolean"
? groupConfig.requireMention

View File

@@ -240,7 +240,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
changed = true;
}
const accountCleanup = clearAccountEntryFields({
accounts: nextSection.accounts,
accounts: nextSection.accounts as Record<string, object> | undefined,
accountId,
fields: ["botSecret"],
});
@@ -250,7 +250,7 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
cleared = true;
}
if (accountCleanup.nextAccounts) {
nextSection.accounts = accountCleanup.nextAccounts;
nextSection.accounts = accountCleanup.nextAccounts as Record<string, unknown>;
} else {
delete nextSection.accounts;
}

View File

@@ -27,7 +27,13 @@ describe("nextcloud-talk doctor", () => {
expect(result.config.channels?.["nextcloud-talk"]?.network).toEqual({
dangerouslyAllowPrivateNetwork: true,
});
expect(result.config.channels?.["nextcloud-talk"]?.accounts?.work?.network).toEqual({
expect(
(
result.config.channels?.["nextcloud-talk"]?.accounts?.work as
| { network?: Record<string, unknown> }
| undefined
)?.network,
).toEqual({
dangerouslyAllowPrivateNetwork: false,
});
});

View File

@@ -155,7 +155,10 @@ describe("nextcloud talk setup", () => {
const next = nextcloudTalkDmPolicy.setPolicy(base, "open");
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBe("disabled");
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
});
it('writes open DM policy to the named account and preserves inherited allowFrom with "*"', () => {
@@ -178,8 +181,11 @@ describe("nextcloud talk setup", () => {
);
expect(next.channels?.["nextcloud-talk"]?.dmPolicy).toBeUndefined();
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.dmPolicy).toBe("open");
expect(next.channels?.["nextcloud-talk"]?.accounts?.work?.allowFrom).toEqual(["alice", "*"]);
const workAccount = next.channels?.["nextcloud-talk"]?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["alice", "*"]);
});
it("validates env/default-account constraints and applies config patches", () => {

View File

@@ -4,7 +4,7 @@ import {
hasConfiguredSecretInput,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
import type { ChannelSetupDmPolicy, ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
import type { ChannelSetupDmPolicy, ChannelSetupWizard, DmPolicy } from "openclaw/plugin-sdk/setup";
import {
createStandardChannelSetupStatus,
createTopLevelChannelDmPolicy,
@@ -89,7 +89,7 @@ const nostrDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
channel,
policyKey: "channels.nostr.dmPolicy",
allowFromKey: "channels.nostr.allowFrom",
getCurrent: (cfg) => cfg.channels?.nostr?.dmPolicy ?? "pairing",
getCurrent: (cfg) => (cfg.channels?.nostr?.dmPolicy as DmPolicy | undefined) ?? "pairing",
promptAllowFrom: promptNostrAllowFrom,
});
@@ -234,8 +234,8 @@ export const nostrSetupWizard: ChannelSetupWizard = {
helpLines: ["Use ws:// or wss:// relay URLs.", "Leave blank to keep the default relay set."],
currentValue: ({ cfg, accountId }) => {
const account = resolveNostrAccount({ cfg, accountId });
const relays =
cfg.channels?.nostr?.relays && cfg.channels.nostr.relays.length > 0 ? account.relays : [];
const configuredRelays = cfg.channels?.nostr?.relays as string[] | undefined;
const relays = configuredRelays && configuredRelays.length > 0 ? account.relays : [];
return relays.join(", ");
},
keepPrompt: (value) => `Relay URLs set (${value}). Keep them?`,

View File

@@ -18,7 +18,7 @@ import type {
/** Extract the channel config from the full OpenClaw config object. */
function getChannelConfig(cfg: OpenClawConfig): SynologyChatChannelConfig | undefined {
return cfg?.channels?.["synology-chat"];
return cfg?.channels?.["synology-chat"] as SynologyChatChannelConfig | undefined;
}
function resolveImplicitAccountId(channelCfg: SynologyChatChannelConfig): string | undefined {

View File

@@ -27,7 +27,13 @@ describe("tlon doctor", () => {
expect(result.config.channels?.tlon?.network).toEqual({
dangerouslyAllowPrivateNetwork: true,
});
expect(result.config.channels?.tlon?.accounts?.alt?.network).toEqual({
expect(
(
result.config.channels?.tlon?.accounts?.alt as
| { network?: Record<string, unknown> }
| undefined
)?.network,
).toEqual({
dangerouslyAllowPrivateNetwork: false,
});
});

View File

@@ -198,8 +198,11 @@ describe("setup surface helpers", () => {
// Should return config with username and clientId
expect(result).not.toBeNull();
expect(result?.cfg.channels?.twitch?.accounts?.default?.username).toBe("testbot");
expect(result?.cfg.channels?.twitch?.accounts?.default?.clientId).toBe("test-client-id");
const defaultAccount = result?.cfg.channels?.twitch?.accounts?.default as
| { username?: string; clientId?: string }
| undefined;
expect(defaultAccount?.username).toBe("testbot");
expect(defaultAccount?.clientId).toBe("test-client-id");
});
it("writes env-token setup to the configured default account", async () => {
@@ -226,8 +229,11 @@ describe("setup surface helpers", () => {
{} as Parameters<typeof configureWithEnvToken>[5],
);
expect(result?.cfg.channels?.twitch?.accounts?.secondary?.username).toBe("secondary-bot");
expect(result?.cfg.channels?.twitch?.accounts?.secondary?.clientId).toBe("secondary-client");
const secondaryAccount = result?.cfg.channels?.twitch?.accounts?.secondary as
| { username?: string; clientId?: string }
| undefined;
expect(secondaryAccount?.username).toBe("secondary-bot");
expect(secondaryAccount?.clientId).toBe("secondary-client");
expect(result?.cfg.channels?.twitch?.accounts?.default).toBeUndefined();
});
});

View File

@@ -12,6 +12,12 @@ import { resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
const channel = "zalo" as const;
type ZaloAccountSetupConfig = {
enabled?: boolean;
dmPolicy?: string;
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
};
export const zaloSetupAdapter = createPatchedAccountSetupAdapter({
channelKey: channel,
validateInput: createSetupInputPresenceValidator({
@@ -78,6 +84,9 @@ export const zaloDmPolicy: ChannelSetupDmPolicy = {
},
};
}
const currentAccount = cfg.channels?.zalo?.accounts?.[resolvedAccountId] as
| ZaloAccountSetupConfig
| undefined;
return {
...cfg,
channels: {
@@ -88,8 +97,8 @@ export const zaloDmPolicy: ChannelSetupDmPolicy = {
accounts: {
...cfg.channels?.zalo?.accounts,
[resolvedAccountId]: {
...cfg.channels?.zalo?.accounts?.[resolvedAccountId],
enabled: cfg.channels?.zalo?.accounts?.[resolvedAccountId]?.enabled ?? true,
...currentAccount,
enabled: currentAccount?.enabled ?? true,
dmPolicy: policy,
...(policy === "open"
? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) }

View File

@@ -96,7 +96,10 @@ describe("zalo setup wizard", () => {
const next = zaloDmPolicy.setPolicy(cfg, "open");
expect(next.channels?.zalo?.dmPolicy).toBe("disabled");
expect(next.channels?.zalo?.accounts?.work?.dmPolicy).toBe("open");
const workAccount = next.channels?.zalo?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
});
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
@@ -118,8 +121,11 @@ describe("zalo setup wizard", () => {
);
expect(next.channels?.zalo?.dmPolicy).toBeUndefined();
expect(next.channels?.zalo?.accounts?.work?.dmPolicy).toBe("open");
expect(next.channels?.zalo?.accounts?.work?.allowFrom).toEqual(["123456789", "*"]);
const workAccount = next.channels?.zalo?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["123456789", "*"]);
});
it("uses configured defaultAccount for omitted setup configured state", async () => {

View File

@@ -19,6 +19,10 @@ const channel = "zalo" as const;
type UpdateMode = "polling" | "webhook";
type ZaloAccountSetupConfig = {
enabled?: boolean;
};
function setZaloUpdateMode(
cfg: OpenClawConfig,
accountId: string,
@@ -150,6 +154,9 @@ async function promptZaloAllowFrom(params: {
} as OpenClawConfig;
}
const currentAccount = cfg.channels?.zalo?.accounts?.[accountId] as
| ZaloAccountSetupConfig
| undefined;
return {
...cfg,
channels: {
@@ -160,8 +167,8 @@ async function promptZaloAllowFrom(params: {
accounts: {
...cfg.channels?.zalo?.accounts,
[accountId]: {
...cfg.channels?.zalo?.accounts?.[accountId],
enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true,
...currentAccount,
enabled: currentAccount?.enabled ?? true,
dmPolicy: "allowlist",
allowFrom: unique,
},
@@ -261,7 +268,9 @@ export const zaloSetupWizard: ChannelSetupWizard = {
accounts: {
...currentCfg.channels?.zalo?.accounts,
[accountId]: {
...currentCfg.channels?.zalo?.accounts?.[accountId],
...(currentCfg.channels?.zalo?.accounts?.[accountId] as
| Record<string, unknown>
| undefined),
enabled: true,
botToken: value,
},

View File

@@ -35,7 +35,13 @@ describe("zalouser doctor", () => {
expect(result.config.channels?.zalouser?.groups?.["group:trusted"]).toEqual({
enabled: true,
});
expect(result.config.channels?.zalouser?.accounts?.work?.groups?.["group:legacy"]).toEqual({
expect(
(
result.config.channels?.zalouser?.accounts?.work as
| { groups?: Record<string, unknown> }
| undefined
)?.groups?.["group:legacy"],
).toEqual({
enabled: false,
});
expect(result.changes).toEqual(

View File

@@ -136,8 +136,9 @@ function installRuntime(params: {
resolveRequireMention: vi.fn((input) => {
const cfg = input.cfg as OpenClawConfig;
const groupCfg = cfg.channels?.zalouser?.groups ?? {};
const groupEntry = input.groupId ? groupCfg[input.groupId] : undefined;
const defaultEntry = groupCfg["*"];
const typedGroupCfg = groupCfg as Record<string, { requireMention?: boolean }>;
const groupEntry = input.groupId ? typedGroupCfg[input.groupId] : undefined;
const defaultEntry = typedGroupCfg["*"];
if (typeof groupEntry?.requireMention === "boolean") {
return groupEntry.requireMention;
}

View File

@@ -295,7 +295,10 @@ describe("zalouser setup wizard", () => {
const next = zalouserSetupWizard.dmPolicy?.setPolicy(cfg, "open");
expect(next?.channels?.zalouser?.dmPolicy).toBe("disabled");
expect(next?.channels?.zalouser?.accounts?.work?.dmPolicy).toBe("open");
const workAccount = next?.channels?.zalouser?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
});
it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
@@ -317,8 +320,11 @@ describe("zalouser setup wizard", () => {
);
expect(next?.channels?.zalouser?.dmPolicy).toBeUndefined();
expect(next?.channels?.zalouser?.accounts?.work?.dmPolicy).toBe("open");
expect(next?.channels?.zalouser?.accounts?.work?.allowFrom).toEqual(["123456789", "*"]);
const workAccount = next?.channels?.zalouser?.accounts?.work as
| { dmPolicy?: string; allowFrom?: Array<string | number> }
| undefined;
expect(workAccount?.dmPolicy).toBe("open");
expect(workAccount?.allowFrom).toEqual(["123456789", "*"]);
});
it("shows the account-scoped current DM policy in quickstart notes", async () => {

View File

@@ -406,7 +406,7 @@ describe("group policy warning builders", () => {
cfg: {
channels?: {
defaults?: { groupPolicy?: "open" | "allowlist" | "disabled" };
example?: unknown;
example?: Record<string, unknown>;
};
};
channelLabel: string;
@@ -505,7 +505,7 @@ describe("group policy warning builders", () => {
it("builds config-aware open-provider collectors", () => {
const collectWarnings = createOpenProviderGroupPolicyWarningCollector<{
cfg: { channels?: { example?: unknown } };
cfg: { channels?: { example?: Record<string, unknown> } };
configuredGroupPolicy?: "open" | "allowlist" | "disabled";
}>({
providerConfigPresent: (cfg) => cfg.channels?.example !== undefined,

View File

@@ -57,8 +57,8 @@ export function createScopedChannelMediaMaxBytesResolver(channel: string) {
cfg: params.cfg,
accountId: params.accountId,
resolveChannelLimitMb: ({ cfg, accountId }) =>
cfg.channels?.[channel]?.accounts?.[accountId]?.mediaMaxMb ??
cfg.channels?.[channel]?.mediaMaxMb,
(cfg.channels?.[channel]?.accounts?.[accountId] as { mediaMaxMb?: number } | undefined)
?.mediaMaxMb ?? cfg.channels?.[channel]?.mediaMaxMb,
});
}

View File

@@ -692,8 +692,13 @@ describe("promptParsedAllowFromForAccount", () => {
placeholder: "placeholder",
parseEntries: (raw) =>
parseSetupEntriesWithParser(raw, (entry) => ({ value: entry.toLowerCase() })),
getExistingAllowFrom: ({ cfg, accountId }) =>
cfg.channels?.bluebubbles?.accounts?.[accountId]?.allowFrom ?? [],
getExistingAllowFrom: ({ cfg, accountId }) => [
...((
cfg.channels?.bluebubbles?.accounts?.[accountId] as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom ?? []),
],
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
patchChannelConfigForAccount({
cfg,
@@ -703,7 +708,13 @@ describe("promptParsedAllowFromForAccount", () => {
}),
});
expect(next.channels?.bluebubbles?.accounts?.alt?.allowFrom).toEqual(["alice"]);
expect(
(
next.channels?.bluebubbles?.accounts?.alt as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom,
).toEqual(["alice"]);
expect(prompter.note).toHaveBeenCalledWith("line", "BlueBubbles allowlist");
});
@@ -723,7 +734,7 @@ describe("promptParsedAllowFromForAccount", () => {
message: "msg",
placeholder: "placeholder",
parseEntries: (raw) => ({ entries: [raw.trim()] }),
getExistingAllowFrom: ({ cfg }) => cfg.channels?.nostr?.allowFrom ?? [],
getExistingAllowFrom: ({ cfg }) => [...(cfg.channels?.nostr?.allowFrom ?? [])],
mergeEntries: ({ existing, parsed }) => [...existing.map(String), ...parsed],
applyAllowFrom: ({ cfg, allowFrom }) =>
patchTopLevelChannelConfigSection({
@@ -744,8 +755,13 @@ describe("createPromptParsedAllowFromForAccount", () => {
message: "msg",
placeholder: "placeholder",
parseEntries: (raw) => ({ entries: [raw.trim().toLowerCase()] }),
getExistingAllowFrom: ({ cfg, accountId }) =>
cfg.channels?.bluebubbles?.accounts?.[accountId]?.allowFrom ?? [],
getExistingAllowFrom: ({ cfg, accountId }) => [
...((
cfg.channels?.bluebubbles?.accounts?.[accountId] as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom ?? []),
],
applyAllowFrom: ({ cfg, accountId, allowFrom }) =>
patchChannelConfigForAccount({
cfg,
@@ -771,7 +787,13 @@ describe("createPromptParsedAllowFromForAccount", () => {
prompter: prompter as any,
});
expect(next.channels?.bluebubbles?.accounts?.work?.allowFrom).toEqual(["alice"]);
expect(
(
next.channels?.bluebubbles?.accounts?.work as
| { allowFrom?: ReadonlyArray<string | number> }
| undefined
)?.allowFrom,
).toEqual(["alice"]);
expect(prompter.note).not.toHaveBeenCalled();
});
});
@@ -1267,7 +1289,7 @@ describe("setTopLevelChannelDmPolicyWithAllowFrom", () => {
channel: "nextcloud-talk",
dmPolicy: "open",
getAllowFrom: (inputCfg) =>
normalizeAllowFromEntries(inputCfg.channels?.["nextcloud-talk"]?.allowFrom ?? []),
normalizeAllowFromEntries([...(inputCfg.channels?.["nextcloud-talk"]?.allowFrom ?? [])]),
});
expect(next.channels?.["nextcloud-talk"]?.allowFrom).toEqual(["alice", "*"]);
});
@@ -1339,7 +1361,7 @@ describe("patchNestedChannelConfigSection", () => {
section: "dm",
clearFields: ["allowFrom"],
enabled: true,
patch: { policy: "disabled" },
patch: { policy: "disabled" as const },
});
expect(next.channels?.matrix?.enabled).toBe(true);
@@ -1355,7 +1377,13 @@ describe("createTopLevelChannelDmPolicy", () => {
channel: "line",
policyKey: "channels.line.dmPolicy",
allowFromKey: "channels.line.allowFrom",
getCurrent: (cfg) => cfg.channels?.line?.dmPolicy ?? "pairing",
getCurrent: (cfg) =>
(cfg.channels?.line?.dmPolicy as
| "open"
| "pairing"
| "allowlist"
| "disabled"
| undefined) ?? "pairing",
});
const next = dmPolicy.setPolicy(
@@ -1445,7 +1473,12 @@ describe("createNestedChannelDmPolicy", () => {
section: "dm",
policyKey: "channels.matrix.dm.policy",
allowFromKey: "channels.matrix.dm.allowFrom",
getCurrent: (cfg) => cfg.channels?.matrix?.dm?.policy ?? "pairing",
getCurrent: (cfg) =>
(
cfg.channels?.matrix?.dm as
| { policy?: "open" | "pairing" | "allowlist" | "disabled" }
| undefined
)?.policy ?? "pairing",
enabled: true,
});

View File

@@ -61,7 +61,7 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa
return normalized;
};
export function addWildcardAllowFrom(allowFrom?: Array<string | number> | null): string[] {
export function addWildcardAllowFrom(allowFrom?: ReadonlyArray<string | number> | null): string[] {
const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
if (!next.includes("*")) {
next.push("*");

View File

@@ -10,6 +10,7 @@ import {
splitSetupEntries,
} from "./setup-wizard-helpers.js";
import type {
ChannelSetupPlugin,
ChannelSetupWizardAdapter,
ChannelSetupConfigureContext,
ChannelSetupDmPolicy,
@@ -17,7 +18,6 @@ import type {
ChannelSetupStatusContext,
} from "./setup-wizard-types.js";
import type { ChannelSetupInput } from "./types.core.js";
import type { ChannelPlugin } from "./types.js";
export type ChannelSetupWizardStatus = {
configuredLabel: string;
@@ -282,7 +282,7 @@ export type ChannelSetupWizard = {
onAccountRecorded?: ChannelSetupWizardAdapter["onAccountRecorded"];
};
type ChannelSetupWizardPlugin = Pick<ChannelPlugin, "id" | "meta" | "config" | "setup">;
type ChannelSetupWizardPlugin = ChannelSetupPlugin;
async function buildStatus(
plugin: ChannelSetupWizardPlugin,

View File

@@ -115,11 +115,15 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
enabled: boolean;
}) => OpenClawConfig;
deleteAccount?: (params: { cfg: OpenClawConfig; accountId: string }) => OpenClawConfig;
isEnabled?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean;
disabledReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
isConfigured?: (account: ResolvedAccount, cfg: OpenClawConfig) => boolean | Promise<boolean>;
unconfiguredReason?: (account: ResolvedAccount, cfg: OpenClawConfig) => string;
describeAccount?: (account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot;
isEnabled?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => boolean>;
disabledReason?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => string>;
isConfigured?: BivariantCallback<
(account: ResolvedAccount, cfg: OpenClawConfig) => boolean | Promise<boolean>
>;
unconfiguredReason?: BivariantCallback<(account: ResolvedAccount, cfg: OpenClawConfig) => string>;
describeAccount?: BivariantCallback<
(account: ResolvedAccount, cfg: OpenClawConfig) => ChannelAccountSnapshot
>;
resolveAllowFrom?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
@@ -261,26 +265,28 @@ export type ChannelOutboundAdapter = {
export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unknown> = {
defaultRuntime?: ChannelAccountSnapshot;
skipStaleSocketHealthCheck?: boolean;
buildChannelSummary?: (params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
defaultAccountId: string;
snapshot: ChannelAccountSnapshot;
}) => Record<string, unknown> | Promise<Record<string, unknown>>;
probeAccount?: (params: {
account: ResolvedAccount;
timeoutMs: number;
cfg: OpenClawConfig;
}) => Promise<Probe>;
buildChannelSummary?: BivariantCallback<
(params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
defaultAccountId: string;
snapshot: ChannelAccountSnapshot;
}) => Record<string, unknown> | Promise<Record<string, unknown>>
>;
probeAccount?: BivariantCallback<
(params: { account: ResolvedAccount; timeoutMs: number; cfg: OpenClawConfig }) => Promise<Probe>
>;
formatCapabilitiesProbe?: BivariantCallback<
(params: { probe: Probe }) => ChannelCapabilitiesDisplayLine[]
>;
auditAccount?: (params: {
account: ResolvedAccount;
timeoutMs: number;
cfg: OpenClawConfig;
probe?: Probe;
}) => Promise<Audit>;
auditAccount?: BivariantCallback<
(params: {
account: ResolvedAccount;
timeoutMs: number;
cfg: OpenClawConfig;
probe?: Probe;
}) => Promise<Audit>
>;
buildCapabilitiesDiagnostics?: BivariantCallback<
(params: {
account: ResolvedAccount;
@@ -291,25 +297,31 @@ export type ChannelStatusAdapter<ResolvedAccount, Probe = unknown, Audit = unkno
target?: string;
}) => Promise<ChannelCapabilitiesDiagnostics | undefined>
>;
buildAccountSnapshot?: (params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
runtime?: ChannelAccountSnapshot;
probe?: Probe;
audit?: Audit;
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
logSelfId?: (params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
runtime: RuntimeEnv;
includeChannelPrefix?: boolean;
}) => void;
resolveAccountState?: (params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
configured: boolean;
enabled: boolean;
}) => ChannelAccountState;
buildAccountSnapshot?: BivariantCallback<
(params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
runtime?: ChannelAccountSnapshot;
probe?: Probe;
audit?: Audit;
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>
>;
logSelfId?: BivariantCallback<
(params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
runtime: RuntimeEnv;
includeChannelPrefix?: boolean;
}) => void
>;
resolveAccountState?: BivariantCallback<
(params: {
account: ResolvedAccount;
cfg: OpenClawConfig;
configured: boolean;
enabled: boolean;
}) => ChannelAccountState
>;
collectStatusIssues?: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
};
@@ -935,31 +947,35 @@ export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
}) => ChannelDoctorConfigMutation | Promise<ChannelDoctorConfigMutation>;
resolveDmPolicy?: (
ctx: ChannelSecurityContext<ResolvedAccount>,
) => ChannelSecurityDmPolicy | null;
collectWarnings?: (ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[];
collectAuditFindings?: (
ctx: ChannelSecurityContext<ResolvedAccount> & {
sourceConfig: OpenClawConfig;
orderedAccountIds: string[];
hasExplicitAccountPath: boolean;
},
) =>
| Promise<
Array<{
resolveDmPolicy?: BivariantCallback<
(ctx: ChannelSecurityContext<ResolvedAccount>) => ChannelSecurityDmPolicy | null
>;
collectWarnings?: BivariantCallback<
(ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[]
>;
collectAuditFindings?: BivariantCallback<
(
ctx: ChannelSecurityContext<ResolvedAccount> & {
sourceConfig: OpenClawConfig;
orderedAccountIds: string[];
hasExplicitAccountPath: boolean;
},
) =>
| Promise<
Array<{
checkId: string;
severity: "info" | "warn" | "critical";
title: string;
detail: string;
remediation?: string;
}>
>
| Array<{
checkId: string;
severity: "info" | "warn" | "critical";
title: string;
detail: string;
remediation?: string;
}>
>
| Array<{
checkId: string;
severity: "info" | "warn" | "critical";
title: string;
detail: string;
remediation?: string;
}>;
>;
};

View File

@@ -79,6 +79,10 @@ export type ChannelConfigSchema = {
/** Full capability contract for a native channel plugin. */
type ChannelPluginSetupWizard = ChannelSetupWizard | ChannelSetupWizardAdapter;
// oxlint-disable-next-line typescript/no-explicit-any
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
// Omitted generic means "plugin with some account shape", not "plugin whose
// account is literally Record<string, unknown>".
// oxlint-disable-next-line typescript/no-explicit-any
export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
id: ChannelId;

View File

@@ -1,6 +1,7 @@
import { vi } from "vitest";
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { ChannelsConfig } from "../config/types.channels.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
export function createMockChannelSetupPluginInstallModule(
@@ -71,7 +72,7 @@ export function createMSTeamsDeletePlugin(): ChannelPlugin {
delete nextChannels.msteams;
return {
...cfg,
channels: nextChannels,
channels: nextChannels as ChannelsConfig,
};
}),
},

View File

@@ -603,13 +603,25 @@ describe("legacy migrate nested channel enabled aliases", () => {
expect(res.config?.channels?.matrix?.groups?.["!ops:example.org"]).toEqual({
enabled: false,
});
expect(res.config?.channels?.matrix?.accounts?.work?.rooms?.["!legacy:example.org"]).toEqual({
expect(
(
res.config?.channels?.matrix?.accounts?.work as
| { rooms?: Record<string, unknown> }
| undefined
)?.rooms?.["!legacy:example.org"],
).toEqual({
enabled: true,
});
expect(res.config?.channels?.zalouser?.groups?.["group:trusted"]).toEqual({
enabled: false,
});
expect(res.config?.channels?.zalouser?.accounts?.work?.groups?.["group:legacy"]).toEqual({
expect(
(
res.config?.channels?.zalouser?.accounts?.work as
| { groups?: Record<string, unknown> }
| undefined
)?.groups?.["group:legacy"],
).toEqual({
enabled: true,
});
});

View File

@@ -26,21 +26,42 @@ export type ChannelDefaultsConfig = {
export type ChannelModelByChannelConfig = Record<string, Record<string, string>>;
export type ExtensionNestedPolicyConfig = {
policy?: string;
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
[key: string]: unknown;
};
/**
* Base type for extension channel config sections.
* Extensions can use this as a starting point for their channel config.
*/
export type ExtensionChannelConfig = {
enabled?: boolean;
allowFrom?: string | string[];
allowFrom?: Array<string | number> | ReadonlyArray<string | number>;
/** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */
defaultTo?: string;
defaultTo?: string | number;
/** Optional default account id when multiple accounts are configured. */
defaultAccount?: string;
dmPolicy?: string;
groupPolicy?: GroupPolicy;
contextVisibility?: ContextVisibilityMode;
healthMonitor?: ChannelHealthMonitorConfig;
dm?: ExtensionNestedPolicyConfig;
network?: Record<string, unknown>;
groups?: Record<string, unknown>;
rooms?: Record<string, unknown>;
mediaMaxMb?: number;
callbackBaseUrl?: string;
interactions?: { callbackBaseUrl?: string; [key: string]: unknown };
execApprovals?: Record<string, unknown>;
threadBindings?: {
enabled?: boolean;
spawnAcpSessions?: boolean;
spawnSubagentSessions?: boolean;
};
spawnSubagentSessions?: boolean;
dangerouslyAllowPrivateNetwork?: boolean;
accounts?: Record<string, unknown>;
[key: string]: unknown;
};

View File

@@ -122,5 +122,5 @@ export const ChannelsSchema: z.ZodType<ChannelsConfig | undefined> = z
.superRefine((value, ctx) => {
addLegacyChannelAcpBindingIssues(value, ctx);
})
.transform((value, ctx) => normalizeBundledChannelConfigs(value, ctx))
.transform((value, ctx) => normalizeBundledChannelConfigs(value as ChannelsConfig, ctx))
.optional() as z.ZodType<ChannelsConfig | undefined>;

View File

@@ -42,7 +42,11 @@ function isDiscordExecApprovalClientEnabledForTest(params: {
const rootConfig = params.cfg.channels?.discord?.execApprovals;
const accountConfig =
accountId && accountId !== "default"
? params.cfg.channels?.discordAccounts?.[accountId]?.execApprovals
? (
params.cfg.channels?.discordAccounts?.[accountId] as
| { execApprovals?: { enabled?: boolean; approvers?: unknown[] } }
| undefined
)?.execApprovals
: undefined;
const config = accountConfig ?? rootConfig;
return Boolean(config?.enabled && (config.approvers?.length ?? 0) > 0);
@@ -56,7 +60,11 @@ function isTelegramExecApprovalClientEnabledForTest(params: {
const rootConfig = params.cfg.channels?.telegram?.execApprovals;
const accountConfig =
accountId && accountId !== "default"
? params.cfg.channels?.telegramAccounts?.[accountId]?.execApprovals
? (
params.cfg.channels?.telegramAccounts?.[accountId] as
| { execApprovals?: { enabled?: boolean; approvers?: unknown[] } }
| undefined
)?.execApprovals
: undefined;
const config = accountConfig ?? rootConfig;
return Boolean(config?.enabled && (config.approvers?.length ?? 0) > 0);

View File

@@ -157,7 +157,10 @@ describe("secrets runtime snapshot matrix shadowing", () => {
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
expect(snapshot.config.channels?.matrix?.accounts?.ops?.password).toEqual({
expect(
(snapshot.config.channels?.matrix?.accounts?.ops as { password?: unknown } | undefined)
?.password,
).toEqual({
source: "env",
provider: "default",
id: "MATRIX_OPS_PASSWORD",
@@ -331,7 +334,10 @@ describe("secrets runtime snapshot matrix shadowing", () => {
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
expect(snapshot.config.channels?.matrix?.accounts?.default?.password).toEqual({
expect(
(snapshot.config.channels?.matrix?.accounts?.default as { password?: unknown } | undefined)
?.password,
).toEqual({
source: "env",
provider: "default",
id: "MATRIX_DEFAULT_PASSWORD",

View File

@@ -116,12 +116,11 @@ describe("secrets runtime snapshot nextcloud talk file precedence", () => {
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.botSecret).toBe(
"resolved-nextcloud-work-bot-secret",
);
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.apiPassword).toBe(
"resolved-nextcloud-work-api-password",
);
const workAccount = snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work as
| { botSecret?: unknown; apiPassword?: unknown }
| undefined;
expect(workAccount?.botSecret).toBe("resolved-nextcloud-work-bot-secret");
expect(workAccount?.apiPassword).toBe("resolved-nextcloud-work-api-password");
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
"channels.nextcloud-talk.accounts.work.botSecret",
);

View File

@@ -97,9 +97,10 @@ describe("secrets runtime snapshot zalo token activity", () => {
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
expect(snapshot.config.channels?.zalo?.accounts?.work?.botToken).toBe(
"resolved-zalo-work-token",
);
expect(
(snapshot.config.channels?.zalo?.accounts?.work as { botToken?: unknown } | undefined)
?.botToken,
).toBe("resolved-zalo-work-token");
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
"channels.zalo.accounts.work.botToken",
);
@@ -153,9 +154,10 @@ describe("secrets runtime snapshot zalo token activity", () => {
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
expect(snapshot.config.channels?.zalo?.accounts?.default?.botToken).toBe(
"resolved-zalo-default-token",
);
expect(
(snapshot.config.channels?.zalo?.accounts?.default as { botToken?: unknown } | undefined)
?.botToken,
).toBe("resolved-zalo-default-token");
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
"channels.zalo.accounts.default.botToken",
);

View File

@@ -53,9 +53,7 @@ function createSynologyChatAccount(params: {
}): ResolvedSynologyChatAccount {
const channel = params.cfg.channels?.["synology-chat"] ?? {};
const accountConfig =
params.accountId === "default"
? channel
: ((channel.accounts as Record<string, unknown> | undefined)?.[params.accountId] ?? {});
params.accountId === "default" ? channel : (channel.accounts?.[params.accountId] ?? {});
return {
accountId: params.accountId,
dangerouslyAllowNameMatching:

View File

@@ -1,6 +1,6 @@
// Centralized Vitest mock type for harness modules under `src/`.
// Using an explicit named type avoids exporting inferred `vi.fn()` types that can trip TS2742.
// Test harnesses need a permissive default callable shape so vi.fn() can stand in for many signatures.
// Keep the callable bound permissive so explicit callback signatures remain assignable.
// oxlint-disable-next-line typescript/no-explicit-any
export type MockFn<T extends (...args: any[]) => unknown = (...args: any[]) => unknown> =
export type MockFn<T extends (...args: any[]) => any = (...args: any[]) => any> =
import("vitest").Mock<T>;