mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 15:51:06 +00:00
407 lines
11 KiB
TypeScript
407 lines
11 KiB
TypeScript
import fs from "node:fs";
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
|
import { describe, expect, it } from "vitest";
|
|
import { validateJsonSchemaValue } from "../../../src/plugins/schema-validator.js";
|
|
import { qqbotPlugin } from "./channel.js";
|
|
import { qqbotSetupPlugin } from "./channel.setup.js";
|
|
import { QQBotConfigSchema } from "./config-schema.js";
|
|
import {
|
|
DEFAULT_ACCOUNT_ID,
|
|
resolveDefaultQQBotAccountId,
|
|
resolveQQBotAccount,
|
|
} from "./config.js";
|
|
|
|
describe("qqbot config", () => {
|
|
it("accepts top-level speech overrides in the manifest schema", () => {
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
|
|
) as { configSchema: Record<string, unknown> };
|
|
|
|
const result = validateJsonSchemaValue({
|
|
schema: manifest.configSchema,
|
|
cacheKey: "qqbot.manifest.speech-overrides",
|
|
value: {
|
|
tts: {
|
|
provider: "openai",
|
|
baseUrl: "https://example.com/v1",
|
|
apiKey: "tts-key",
|
|
model: "gpt-4o-mini-tts",
|
|
voice: "alloy",
|
|
authStyle: "api-key",
|
|
queryParams: {
|
|
format: "wav",
|
|
},
|
|
speed: 1.1,
|
|
},
|
|
stt: {
|
|
provider: "openai",
|
|
baseUrl: "https://example.com/v1",
|
|
apiKey: "stt-key",
|
|
model: "whisper-1",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts defaultAccount in the manifest schema", () => {
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
|
|
) as { configSchema: Record<string, unknown> };
|
|
|
|
const result = validateJsonSchemaValue({
|
|
schema: manifest.configSchema,
|
|
cacheKey: "qqbot.manifest.default-account",
|
|
value: {
|
|
defaultAccount: "bot2",
|
|
accounts: {
|
|
bot2: {
|
|
appId: "654321",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("honors configured defaultAccount when resolving the default QQ Bot account id", () => {
|
|
const cfg = {
|
|
channels: {
|
|
qqbot: {
|
|
defaultAccount: "bot2",
|
|
accounts: {
|
|
bot2: {
|
|
appId: "654321",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(resolveDefaultQQBotAccountId(cfg)).toBe("bot2");
|
|
});
|
|
|
|
it("accepts SecretRef-backed credentials in the runtime schema", () => {
|
|
const parsed = QQBotConfigSchema.safeParse({
|
|
defaultAccount: "bot2",
|
|
appId: "123456",
|
|
clientSecret: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "QQBOT_CLIENT_SECRET",
|
|
},
|
|
allowFrom: ["*"],
|
|
audioFormatPolicy: {
|
|
sttDirectFormats: [".wav"],
|
|
uploadDirectFormats: [".mp3"],
|
|
transcodeEnabled: false,
|
|
},
|
|
urlDirectUpload: false,
|
|
upgradeUrl: "https://docs.openclaw.ai/channels/qqbot",
|
|
upgradeMode: "doc",
|
|
accounts: {
|
|
bot2: {
|
|
appId: "654321",
|
|
clientSecret: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "QQBOT_CLIENT_SECRET_BOT2",
|
|
},
|
|
allowFrom: ["user-1"],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it("rejects account-level speech overrides that runtime does not consume", () => {
|
|
const parsed = QQBotConfigSchema.safeParse({
|
|
accounts: {
|
|
bot2: {
|
|
appId: "654321",
|
|
tts: {
|
|
provider: "openai",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("preserves top-level media and upgrade config on the default account", () => {
|
|
const cfg = {
|
|
channels: {
|
|
qqbot: {
|
|
appId: "123456",
|
|
clientSecret: "secret-value",
|
|
audioFormatPolicy: {
|
|
sttDirectFormats: [".wav"],
|
|
uploadDirectFormats: [".mp3"],
|
|
transcodeEnabled: false,
|
|
},
|
|
urlDirectUpload: false,
|
|
upgradeUrl: "https://docs.openclaw.ai/channels/qqbot",
|
|
upgradeMode: "hot-reload",
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
const resolved = resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID);
|
|
|
|
expect(resolved.clientSecret).toBe("secret-value");
|
|
expect(resolved.config.audioFormatPolicy).toEqual({
|
|
sttDirectFormats: [".wav"],
|
|
uploadDirectFormats: [".mp3"],
|
|
transcodeEnabled: false,
|
|
});
|
|
expect(resolved.config.urlDirectUpload).toBe(false);
|
|
expect(resolved.config.upgradeUrl).toBe("https://docs.openclaw.ai/channels/qqbot");
|
|
expect(resolved.config.upgradeMode).toBe("hot-reload");
|
|
});
|
|
|
|
it("uses configured defaultAccount when accountId is omitted", () => {
|
|
const cfg = {
|
|
channels: {
|
|
qqbot: {
|
|
defaultAccount: "bot2",
|
|
accounts: {
|
|
bot2: {
|
|
appId: "654321",
|
|
clientSecret: "secret-value",
|
|
name: "Bot Two",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
const resolved = resolveQQBotAccount(cfg);
|
|
|
|
expect(resolved.accountId).toBe("bot2");
|
|
expect(resolved.appId).toBe("654321");
|
|
expect(resolved.clientSecret).toBe("secret-value");
|
|
expect(resolved.name).toBe("Bot Two");
|
|
});
|
|
|
|
it("rejects unresolved SecretRefs on runtime resolution", () => {
|
|
const cfg = {
|
|
channels: {
|
|
qqbot: {
|
|
appId: "123456",
|
|
clientSecret: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "QQBOT_CLIENT_SECRET",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
expect(() => resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID)).toThrow(
|
|
'channels.qqbot.clientSecret: unresolved SecretRef "env:default:QQBOT_CLIENT_SECRET"',
|
|
);
|
|
});
|
|
|
|
it("allows unresolved SecretRefs for setup/status flows", () => {
|
|
const cfg = {
|
|
channels: {
|
|
qqbot: {
|
|
appId: "123456",
|
|
clientSecret: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "QQBOT_CLIENT_SECRET",
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
|
|
const resolved = resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID, {
|
|
allowUnresolvedSecretRef: true,
|
|
});
|
|
|
|
expect(resolved.clientSecret).toBe("");
|
|
expect(resolved.secretSource).toBe("config");
|
|
expect(qqbotSetupPlugin.config.isConfigured?.(resolved, cfg)).toBe(true);
|
|
expect(qqbotSetupPlugin.config.describeAccount?.(resolved, cfg)?.configured).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
inputAccountId: DEFAULT_ACCOUNT_ID,
|
|
expectedPath: ["channels", "qqbot"],
|
|
},
|
|
{
|
|
accountId: "bot2",
|
|
inputAccountId: "bot2",
|
|
expectedPath: ["channels", "qqbot", "accounts", "bot2"],
|
|
},
|
|
])("splits --token on the first colon for $accountId", ({ inputAccountId, expectedPath }) => {
|
|
const setup = qqbotSetupPlugin.setup;
|
|
expect(setup).toBeDefined();
|
|
|
|
const next = setup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: inputAccountId,
|
|
input: {
|
|
token: "102905186:Oi2Mg1Mh2Ni3:Pl7TpBXuHe1OmAYwKi7W",
|
|
},
|
|
}) as Record<string, unknown>;
|
|
|
|
const accountConfig = expectedPath.reduce<unknown>((value, key) => {
|
|
if (!value || typeof value !== "object") {
|
|
return undefined;
|
|
}
|
|
return (value as Record<string, unknown>)[key];
|
|
}, next) as Record<string, unknown> | undefined;
|
|
|
|
expect(accountConfig).toMatchObject({
|
|
enabled: true,
|
|
appId: "102905186",
|
|
clientSecret: "Oi2Mg1Mh2Ni3:Pl7TpBXuHe1OmAYwKi7W",
|
|
});
|
|
});
|
|
|
|
it("rejects malformed --token consistently across setup paths", () => {
|
|
const runtimeSetup = qqbotPlugin.setup;
|
|
const lightweightSetup = qqbotSetupPlugin.setup;
|
|
expect(runtimeSetup).toBeDefined();
|
|
expect(lightweightSetup).toBeDefined();
|
|
|
|
const input = { token: "broken", name: "Bad" };
|
|
|
|
expect(
|
|
runtimeSetup!.validateInput?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toBe("QQBot --token must be in appId:clientSecret format");
|
|
expect(
|
|
lightweightSetup!.validateInput?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toBe("QQBot --token must be in appId:clientSecret format");
|
|
expect(
|
|
runtimeSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toEqual({});
|
|
expect(
|
|
lightweightSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toEqual({});
|
|
});
|
|
|
|
it("preserves the --use-env add flow across setup paths", () => {
|
|
const runtimeSetup = qqbotPlugin.setup;
|
|
const lightweightSetup = qqbotSetupPlugin.setup;
|
|
expect(runtimeSetup).toBeDefined();
|
|
expect(lightweightSetup).toBeDefined();
|
|
|
|
const input = { useEnv: true, name: "Env Bot" };
|
|
|
|
expect(
|
|
runtimeSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toMatchObject({
|
|
channels: {
|
|
qqbot: {
|
|
enabled: true,
|
|
allowFrom: ["*"],
|
|
name: "Env Bot",
|
|
},
|
|
},
|
|
});
|
|
expect(
|
|
lightweightSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
input,
|
|
}),
|
|
).toMatchObject({
|
|
channels: {
|
|
qqbot: {
|
|
enabled: true,
|
|
allowFrom: ["*"],
|
|
name: "Env Bot",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("uses configured defaultAccount when runtime setup accountId is omitted", () => {
|
|
const runtimeSetup = qqbotPlugin.setup;
|
|
expect(runtimeSetup).toBeDefined();
|
|
|
|
expect(
|
|
runtimeSetup!.resolveAccountId?.({
|
|
cfg: {
|
|
channels: {
|
|
qqbot: {
|
|
defaultAccount: "bot2",
|
|
accounts: {
|
|
bot2: { appId: "123456" },
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig,
|
|
accountId: undefined,
|
|
} as never),
|
|
).toBe("bot2");
|
|
});
|
|
|
|
it("rejects --use-env for named accounts across setup paths", () => {
|
|
const runtimeSetup = qqbotPlugin.setup;
|
|
const lightweightSetup = qqbotSetupPlugin.setup;
|
|
expect(runtimeSetup).toBeDefined();
|
|
expect(lightweightSetup).toBeDefined();
|
|
|
|
const input = { useEnv: true, name: "Env Bot" };
|
|
|
|
expect(
|
|
runtimeSetup!.validateInput?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "bot2",
|
|
input,
|
|
}),
|
|
).toBe("QQBot --use-env only supports the default account");
|
|
expect(
|
|
lightweightSetup!.validateInput?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "bot2",
|
|
input,
|
|
}),
|
|
).toBe("QQBot --use-env only supports the default account");
|
|
expect(
|
|
runtimeSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "bot2",
|
|
input,
|
|
}),
|
|
).toEqual({});
|
|
expect(
|
|
lightweightSetup!.applyAccountConfig?.({
|
|
cfg: {} as OpenClawConfig,
|
|
accountId: "bot2",
|
|
input,
|
|
}),
|
|
).toEqual({});
|
|
});
|
|
});
|