Compare commits

...

7 Commits

Author SHA1 Message Date
Vincent Koc
e64fb1c4ea test(config): mock provider policy cold starts 2026-04-18 08:21:52 -07:00
Vincent Koc
d7ce4cfeb9 perf(config): fast-path core channel schemas 2026-04-18 08:21:45 -07:00
Vincent Koc
363bcea7c0 test(infra): avoid heavy runtime imports in system events 2026-04-18 08:13:17 -07:00
Vincent Koc
e3a5b4509b perf(infra): skip plugin lookup for shared poll params 2026-04-18 08:10:22 -07:00
Vincent Koc
57f7a648ac perf(config): skip bundled channel runtime lookups 2026-04-18 08:04:36 -07:00
Vincent Koc
7f2d38412d test(infra): reset cached presence test state 2026-04-18 07:35:24 -07:00
Vincent Koc
ddf63d7766 test(infra): reuse channel resolution module 2026-04-18 07:33:35 -07:00
14 changed files with 294 additions and 64 deletions

View File

@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
const { getBundledChannelPluginMock, getChannelPluginMock } = vi.hoisted(() => ({
getBundledChannelPluginMock: vi.fn(),
getChannelPluginMock: vi.fn(),
}));
vi.mock("./bundled.js", () => ({
getBundledChannelPlugin: getBundledChannelPluginMock,
}));
vi.mock("./registry.js", () => ({
getChannelPlugin: getChannelPluginMock,
}));
import { resolveSingleAccountKeysToMove } from "./setup-promotion-helpers.js";
describe("resolveSingleAccountKeysToMove", () => {
it("keeps bundled static promotion keys off the plugin runtime path", () => {
getBundledChannelPluginMock.mockImplementation(() => {
throw new Error("should not load bundled channel runtime");
});
getChannelPluginMock.mockImplementation(() => {
throw new Error("should not query channel registry");
});
const keys = resolveSingleAccountKeysToMove({
channelKey: "whatsapp",
channel: {
dmPolicy: "allowlist",
allowFrom: ["+15550001111"],
groupPolicy: "open",
groupAllowFrom: [],
accounts: {
work: {
enabled: true,
authDir: "/tmp/wa-work",
},
},
},
});
expect(keys).toEqual(["dmPolicy", "allowFrom", "groupPolicy", "groupAllowFrom"]);
expect(getChannelPluginMock).not.toHaveBeenCalled();
expect(getBundledChannelPluginMock).not.toHaveBeenCalled();
});
it("uses bundled named-account fallbacks without loading setup surfaces", () => {
getBundledChannelPluginMock.mockImplementation(() => {
throw new Error("should not load bundled channel runtime");
});
getChannelPluginMock.mockImplementation(() => {
throw new Error("should not query channel registry");
});
const keys = resolveSingleAccountKeysToMove({
channelKey: "telegram",
channel: {
botToken: "telegram-test-token",
tokenFile: "/tmp/telegram-token",
streaming: true,
accounts: {
work: {
botToken: "work-token",
},
},
},
});
expect(keys).toEqual(["botToken", "tokenFile"]);
expect(getChannelPluginMock).not.toHaveBeenCalled();
expect(getBundledChannelPluginMock).not.toHaveBeenCalled();
});
});

View File

@@ -118,9 +118,16 @@ export function resolveSingleAccountKeysToMove(params: {
return keysToMove;
}
const namedAccountPromotionKeys =
resolveSetupSurface()?.namedAccountPromotionKeys ??
const bundledNamedAccountPromotionKeys =
BUNDLED_NAMED_ACCOUNT_PROMOTION_FALLBACKS[params.channelKey];
const shouldLoadSetupSurfaceForNamedAccounts =
bundledNamedAccountPromotionKeys === undefined &&
keysToMove.some((key) => !isStaticSingleAccountPromotionKey(params.channelKey, key));
const namedAccountPromotionKeys =
bundledNamedAccountPromotionKeys ??
(shouldLoadSetupSurfaceForNamedAccounts
? resolveSetupSurface()?.namedAccountPromotionKeys
: undefined);
if (!namedAccountPromotionKeys) {
return keysToMove;
}

View File

@@ -1,8 +1,30 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
import { applyModelDefaults } from "./defaults.js";
import type { OpenClawConfig } from "./types.js";
vi.mock("../plugins/provider-public-artifacts.js", () => ({
resolveBundledProviderPolicySurface(providerId: string) {
if (providerId !== "anthropic") {
return null;
}
return {
normalizeConfig: ({
providerConfig,
}: {
providerConfig: OpenClawConfig["models"]["providers"][string];
}) => ({
...providerConfig,
api: providerConfig.api ?? "anthropic-messages",
models: providerConfig.models?.map((model) => ({
...model,
api: model.api ?? providerConfig.api ?? "anthropic-messages",
})),
}),
};
},
}));
describe("applyModelDefaults", () => {
function buildProxyProviderConfig(overrides?: { contextWindow?: number; maxTokens?: number }) {
return {

View File

@@ -58,6 +58,7 @@ const bundledChannelSchemaById = new Map<string, unknown>(
(entry) => [entry.channelId, entry.schema] as const,
),
);
const bundledChannelIds = new Set(bundledChannelSchemaById.keys());
function toIssueRecord(value: unknown): UnknownIssueRecord | null {
if (!value || typeof value !== "object") {
@@ -901,7 +902,12 @@ function validateConfigObjectWithPluginsBase(
};
};
const allowedChannels = new Set<string>(["defaults", "modelByChannel", ...CHANNEL_IDS]);
const allowedChannels = new Set<string>([
"defaults",
"modelByChannel",
...CHANNEL_IDS,
...bundledChannelIds,
]);
if (config.channels && isRecord(config.channels)) {
for (const key of Object.keys(config.channels)) {
@@ -925,7 +931,8 @@ function validateConfigObjectWithPluginsBase(
continue;
}
const channelSchema = ensureChannelSchemas().get(trimmed)?.schema;
const channelSchema =
bundledChannelSchemaById.get(trimmed) ?? ensureChannelSchemas().get(trimmed)?.schema;
if (!channelSchema) {
continue;
}
@@ -956,6 +963,9 @@ function validateConfigObjectWithPluginsBase(
for (const channelId of CHANNEL_IDS) {
heartbeatChannelIds.add(normalizeLowercaseStringOrEmpty(channelId));
}
for (const channelId of bundledChannelIds) {
heartbeatChannelIds.add(normalizeLowercaseStringOrEmpty(channelId));
}
const validateHeartbeatTarget = (target: string | undefined, path: string) => {
if (typeof target !== "string") {

View File

@@ -49,14 +49,33 @@ describe("ChannelsSchema bundled runtime loading", () => {
);
});
it("skips bundled channel runtime discovery for core channel config objects", async () => {
const runtime = await importFreshModule<typeof import("./zod-schema.providers.js")>(
import.meta.url,
"./zod-schema.providers.js?scope=channels-core-config",
);
const parsed = runtime.ChannelsSchema.parse({
telegram: {
botToken: "123:ABC",
},
});
expect(parsed?.telegram).toMatchObject({
botToken: "123:ABC",
});
expect(listBundledPluginMetadataMock).not.toHaveBeenCalled();
expect(collectBundledChannelConfigsMock).not.toHaveBeenCalled();
});
it("loads bundled channel runtime discovery only when plugin-owned channel config is present", async () => {
listBundledPluginMetadataMock.mockReturnValueOnce([
{
dirName: "discord",
dirName: "matrix",
manifest: {
channels: ["discord"],
channels: ["matrix"],
channelConfigs: {
discord: {
matrix: {
runtime: {
safeParse: (value: unknown) => ({ success: true, data: value }),
},
@@ -72,7 +91,7 @@ describe("ChannelsSchema bundled runtime loading", () => {
);
runtime.ChannelsSchema.parse({
discord: {},
matrix: {},
});
expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([
@@ -87,14 +106,14 @@ describe("ChannelsSchema bundled runtime loading", () => {
it("loads a single plugin-owned runtime surface when the manifest omits runtime metadata", async () => {
listBundledPluginMetadataMock.mockReturnValueOnce([
{
dirName: "discord",
dirName: "matrix",
manifest: {
channels: ["discord"],
channels: ["matrix"],
},
} as unknown as BundledPluginMetadata,
]);
collectBundledChannelConfigsMock.mockReturnValueOnce({
discord: {
matrix: {
schema: {},
runtime: {
safeParse: (value: unknown) => ({ success: true, data: value }),
@@ -108,7 +127,7 @@ describe("ChannelsSchema bundled runtime loading", () => {
);
runtime.ChannelsSchema.parse({
discord: {},
matrix: {},
});
expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([

View File

@@ -8,6 +8,18 @@ import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
import type { ChannelsConfig } from "./types.channels.js";
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
import { ContextVisibilityModeSchema, GroupPolicySchema } from "./zod-schema.core.js";
import {
BlueBubblesConfigSchema,
DiscordConfigSchema,
GoogleChatConfigSchema,
IMessageConfigSchema,
IrcConfigSchema,
MSTeamsConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
TelegramConfigSchema,
} from "./zod-schema.providers-core.js";
import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js";
export * from "./zod-schema.providers-core.js";
export * from "./zod-schema.providers-whatsapp.js";
@@ -18,6 +30,37 @@ const ChannelModelByChannelSchema = z
.optional();
let directChannelRuntimeSchemasCache: ReadonlyMap<string, ChannelConfigRuntimeSchema> | undefined;
function createZodRuntimeSchema(schema: z.ZodType): ChannelConfigRuntimeSchema {
return {
safeParse(value) {
const parsed = schema.safeParse(value);
if (parsed.success) {
return parsed;
}
return {
success: false,
issues: parsed.error.issues.map((issue) => ({
path: issue.path,
message: issue.message,
code: issue.code,
})),
};
},
};
}
const STATIC_CHANNEL_RUNTIME_SCHEMA_BY_ID = {
bluebubbles: BlueBubblesConfigSchema,
discord: DiscordConfigSchema,
googlechat: GoogleChatConfigSchema,
imessage: IMessageConfigSchema,
irc: IrcConfigSchema,
msteams: MSTeamsConfigSchema,
signal: SignalConfigSchema,
slack: SlackConfigSchema,
telegram: TelegramConfigSchema,
whatsapp: WhatsAppConfigSchema,
} as const satisfies Record<string, z.ZodType>;
const OPENCLAW_PACKAGE_ROOT =
resolveLoaderPackageRoot({
modulePath: fileURLToPath(import.meta.url),
@@ -25,6 +68,14 @@ const OPENCLAW_PACKAGE_ROOT =
}) ?? fileURLToPath(new URL("../..", import.meta.url));
function getDirectChannelRuntimeSchema(channelId: string): ChannelConfigRuntimeSchema | undefined {
const staticSchema =
STATIC_CHANNEL_RUNTIME_SCHEMA_BY_ID[
channelId as keyof typeof STATIC_CHANNEL_RUNTIME_SCHEMA_BY_ID
];
if (staticSchema) {
return createZodRuntimeSchema(staticSchema);
}
if (!directChannelRuntimeSchemasCache) {
directChannelRuntimeSchemasCache = new Map();
}

View File

@@ -1,6 +1,5 @@
import os from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../test/helpers/import-fresh.js";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const execFileMock = vi.hoisted(() => vi.fn());
@@ -19,12 +18,17 @@ vi.mock("node:child_process", async () => {
const originalVitest = process.env.VITEST;
const originalNodeEnv = process.env.NODE_ENV;
async function importMachineName(scope: string) {
return await importFreshModule<typeof import("./machine-name.js")>(
import.meta.url,
`./machine-name.js?scope=${scope}`,
);
}
type MachineNameModule = typeof import("./machine-name.js");
let machineName: MachineNameModule;
beforeAll(async () => {
machineName = await import("./machine-name.js");
});
beforeEach(() => {
machineName.resetMachineDisplayNameCacheForTest();
});
afterEach(() => {
execFileMock.mockReset();
@@ -61,7 +65,7 @@ describe("getMachineDisplayName", () => {
},
])("$name", async ({ scope, hostname, expected, expectedCalls, repeatLookup }) => {
const hostnameSpy = vi.spyOn(os, "hostname").mockReturnValue(hostname);
const machineName = await importMachineName(scope);
void scope;
await expect(machineName.getMachineDisplayName()).resolves.toBe(expected);
if (repeatLookup) {

View File

@@ -7,6 +7,10 @@ const execFileAsync = promisify(execFile);
let cachedPromise: Promise<string> | null = null;
export function resetMachineDisplayNameCacheForTest(): void {
cachedPromise = null;
}
async function tryScutil(key: "ComputerName" | "LocalHostName") {
try {
const { stdout } = await execFileAsync("/usr/sbin/scutil", ["--get", key], {

View File

@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn());
const resolveAgentWorkspaceDirMock = vi.hoisted(() => vi.fn());
@@ -43,14 +43,9 @@ vi.mock("../../utils/message-channel.js", () => ({
isDeliverableMessageChannel: (...args: unknown[]) => isDeliverableMessageChannelMock(...args),
}));
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
type ChannelResolutionModule = typeof import("./channel-resolution.js");
async function importChannelResolution(scope: string) {
return await importFreshModule<typeof import("./channel-resolution.js")>(
import.meta.url,
`./channel-resolution.js?scope=${scope}`,
);
}
let channelResolution: ChannelResolutionModule;
function expectBootstrapArgs() {
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
@@ -66,7 +61,11 @@ function expectBootstrapArgs() {
}
describe("outbound channel resolution", () => {
beforeEach(async () => {
beforeAll(async () => {
channelResolution = await import("./channel-resolution.js");
});
beforeEach(() => {
resolveDefaultAgentIdMock.mockReset();
resolveAgentWorkspaceDirMock.mockReset();
getLoadedChannelPluginMock.mockReset();
@@ -94,8 +93,6 @@ describe("outbound channel resolution", () => {
});
resolveDefaultAgentIdMock.mockReturnValue("main");
resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace");
const channelResolution = await importChannelResolution("reset");
channelResolution.resetOutboundChannelResolutionStateForTest();
});
@@ -103,15 +100,13 @@ describe("outbound channel resolution", () => {
{ input: " Telegram ", expected: "telegram" },
{ input: "unknown", expected: undefined },
{ input: null, expected: undefined },
])("normalizes deliverable outbound channel for %j", async ({ input, expected }) => {
const channelResolution = await importChannelResolution("normalize");
])("normalizes deliverable outbound channel for %j", ({ input, expected }) => {
expect(channelResolution.normalizeDeliverableOutboundChannel(input)).toBe(expected);
});
it("returns the already-registered plugin without bootstrapping", async () => {
it("returns the already-registered plugin without bootstrapping", () => {
const plugin = { id: "telegram" };
getLoadedChannelPluginMock.mockReturnValueOnce(plugin);
const channelResolution = await importChannelResolution("existing-plugin");
expect(
channelResolution.resolveOutboundChannelPlugin({
@@ -122,7 +117,7 @@ describe("outbound channel resolution", () => {
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
});
it("falls back to the active registry when getChannelPlugin misses", async () => {
it("falls back to the active registry when getChannelPlugin misses", () => {
const plugin = { id: "telegram" };
getChannelPluginMock.mockReturnValue(undefined);
getActivePluginRegistryMock.mockReturnValue({
@@ -131,7 +126,6 @@ describe("outbound channel resolution", () => {
getActivePluginChannelRegistryMock.mockReturnValue({
channels: [{ plugin }],
});
const channelResolution = await importChannelResolution("direct-registry");
expect(
channelResolution.resolveOutboundChannelPlugin({
@@ -141,10 +135,9 @@ describe("outbound channel resolution", () => {
).toBe(plugin);
});
it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => {
it("bootstraps plugins once per registry key and returns the newly loaded plugin", () => {
const plugin = { id: "telegram" };
getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
const channelResolution = await importChannelResolution("bootstrap-success");
expect(
channelResolution.resolveOutboundChannelPlugin({
@@ -163,7 +156,7 @@ describe("outbound channel resolution", () => {
expectBootstrapArgs();
});
it("bootstraps when the active registry has other channels but not the requested one", async () => {
it("bootstraps when the active registry has other channels but not the requested one", () => {
const plugin = { id: "telegram" };
getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
getActivePluginRegistryMock.mockReturnValue({
@@ -172,7 +165,6 @@ describe("outbound channel resolution", () => {
getActivePluginChannelRegistryMock.mockReturnValue({
channels: [{ plugin: { id: "discord" } }],
});
const channelResolution = await importChannelResolution("bootstrap-missing-target");
expect(
channelResolution.resolveOutboundChannelPlugin({
@@ -183,12 +175,11 @@ describe("outbound channel resolution", () => {
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
});
it("retries bootstrap after a transient load failure", async () => {
it("retries bootstrap after a transient load failure", () => {
getChannelPluginMock.mockReturnValue(undefined);
resolveRuntimePluginRegistryMock.mockImplementationOnce(() => {
throw new Error("transient");
});
const channelResolution = await importChannelResolution("bootstrap-retry");
expect(
channelResolution.resolveOutboundChannelPlugin({
@@ -204,9 +195,8 @@ describe("outbound channel resolution", () => {
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2);
});
it("retries bootstrap when the pinned channel registry version changes", async () => {
it("retries bootstrap when the pinned channel registry version changes", () => {
getChannelPluginMock.mockReturnValue(undefined);
const channelResolution = await importChannelResolution("channel-version-change");
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",

View File

@@ -1,6 +1,25 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
const STANDARD_MESSAGE_ACTION_PARAM_KEYS = new Set([
function toSnakeCaseKey(key: string): string {
return key
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase();
}
function createStandardMessageActionParamKeys(keys: readonly string[]): Set<string> {
const allKeys = new Set<string>();
for (const key of keys) {
allKeys.add(key);
const snakeKey = toSnakeCaseKey(key);
if (snakeKey !== key) {
allKeys.add(snakeKey);
}
}
return allKeys;
}
const STANDARD_MESSAGE_ACTION_PARAM_KEYS = createStandardMessageActionParamKeys([
"accountId",
"asDocument",
"base64",
@@ -28,6 +47,7 @@ const STANDARD_MESSAGE_ACTION_PARAM_KEYS = new Set([
"path",
"pollAnonymous",
"pollDurationHours",
"pollDurationSeconds",
"pollMulti",
"pollOption",
"pollPublic",
@@ -39,7 +59,7 @@ const STANDARD_MESSAGE_ACTION_PARAM_KEYS = new Set([
"text",
"threadId",
"to",
]);
] as const);
export function hasPotentialPluginActionParam(params: Record<string, unknown>): boolean {
return Object.entries(params).some(([key, value]) => {

View File

@@ -47,6 +47,27 @@ describe("message action media helpers", () => {
expect(resolveChannelMessageToolMediaSourceParamKeysMock).not.toHaveBeenCalled();
});
it("skips plugin media discovery for shared poll params and their snake_case aliases", () => {
expect(
resolveExtraActionMediaSourceParamKeys({
cfg,
action: "send",
channel: "slack",
args: {
channel: "slack",
target: "#C12345678",
message: "hi",
pollDurationSeconds: "60",
pollPublic: "true",
poll_question: "Ready?",
poll_option: ["Yes", "No"],
poll_public: "false",
},
}),
).toEqual([]);
expect(resolveChannelMessageToolMediaSourceParamKeysMock).not.toHaveBeenCalled();
});
it("discovers plugin media params when args include an extension-owned field", () => {
expect(
resolveExtraActionMediaSourceParamKeys({

View File

@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it } from "vitest";
import { drainFormattedSystemEvents } from "../auto-reply/reply/session-updates.js";
import { drainFormattedSystemEvents } from "../auto-reply/reply/session-system-events.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { isCronSystemEvent } from "./heartbeat-runner.js";
import { isCronSystemEvent } from "./heartbeat-events-filter.js";
import {
consumeSystemEventEntries,
drainSystemEventEntries,

View File

@@ -131,6 +131,11 @@ function touchSelfPresence() {
initSelfPresence();
export function resetSystemPresenceForTest(): void {
entries.clear();
initSelfPresence();
}
function parsePresence(text: string): SystemPresence {
const trimmed = text.trim();
const pattern =

View File

@@ -1,14 +1,21 @@
import os from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../test/helpers/import-fresh.js";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { VERSION as runtimeVersion } from "../version.js";
vi.unmock("../version.js");
type SystemPresenceModule = typeof import("./system-presence.js");
let systemPresenceModule: SystemPresenceModule;
beforeAll(async () => {
systemPresenceModule = await import("./system-presence.js");
});
async function withPresenceModule<T>(
env: Record<string, string | undefined>,
run: (module: typeof import("./system-presence.js")) => Promise<T> | T,
run: (module: SystemPresenceModule) => Promise<T> | T,
): Promise<T> {
return withEnvAsync(
{
@@ -18,11 +25,8 @@ async function withPresenceModule<T>(
...env,
},
async () => {
const module = await importFreshModule<typeof import("./system-presence.js")>(
import.meta.url,
`./system-presence.js?scope=${JSON.stringify(env)}`,
);
return await run(module);
systemPresenceModule.resetSystemPresenceForTest();
return await run(systemPresenceModule);
},
);
}
@@ -104,11 +108,10 @@ describe("system-presence version fallback", () => {
vi.spyOn(os, "networkInterfaces").mockImplementation(() => {
throw new Error("uv_interface_addresses failed");
});
const module = await importFreshModule<typeof import("./system-presence.js")>(
import.meta.url,
"./system-presence.js?scope=hostname-fallback",
);
const selfEntry = module.listSystemPresence().find((entry) => entry.reason === "self");
systemPresenceModule.resetSystemPresenceForTest();
const selfEntry = systemPresenceModule
.listSystemPresence()
.find((entry) => entry.reason === "self");
expect(selfEntry?.host).toBe("test-host");
expect(selfEntry?.ip).toBe("test-host");
});