mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-25 16:53:02 +08:00
Compare commits
7 Commits
v2026.6.8
...
core-runti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64fb1c4ea | ||
|
|
d7ce4cfeb9 | ||
|
|
363bcea7c0 | ||
|
|
e3a5b4509b | ||
|
|
57f7a648ac | ||
|
|
7f2d38412d | ||
|
|
ddf63d7766 |
74
src/channels/plugins/setup-promotion-helpers.test.ts
Normal file
74
src/channels/plugins/setup-promotion-helpers.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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], {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user