mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-16 03:31:10 +00:00
perf(test): split secrets runtime coverage
This commit is contained in:
259
src/secrets/runtime-channel-inactive-variants.test.ts
Normal file
259
src/secrets/runtime-channel-inactive-variants.test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
vi.mock("../channels/plugins/bootstrap-registry.js", async () => {
|
||||
const [ircSecrets, slackSecrets, googleChatSecrets] = await Promise.all([
|
||||
import("../../extensions/irc/src/secret-contract.ts"),
|
||||
import("../../extensions/slack/src/secret-contract.ts"),
|
||||
import("../../extensions/googlechat/src/secret-contract.ts"),
|
||||
]);
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) => {
|
||||
if (id === "irc") {
|
||||
return {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: ircSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (id === "slack") {
|
||||
return {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: slackSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (id === "googlechat") {
|
||||
return {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: googleChatSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
|
||||
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot channel inactive variants", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("treats IRC account nickserv password refs as inactive when nickserv is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
irc: {
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
nickserv: {
|
||||
enabled: false,
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_WORK_NICKSERV_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.irc?.accounts?.work?.nickserv?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_WORK_NICKSERV_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.irc.accounts.work.nickserv.password",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats top-level IRC nickserv password refs as inactive when nickserv is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
irc: {
|
||||
nickserv: {
|
||||
enabled: false,
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.irc?.nickserv?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.irc.nickserv.password",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Slack signingSecret refs as inactive when mode is socket", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "socket",
|
||||
signingSecret: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_SIGNING_SECRET",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
mode: "socket",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.slack?.signingSecret).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_SIGNING_SECRET",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.slack.signingSecret",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Slack appToken refs as inactive when mode is http", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "http",
|
||||
appToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_APP_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
appToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_WORK_APP_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.slack?.appToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_APP_TOKEN",
|
||||
});
|
||||
expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_WORK_APP_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining(["channels.slack.appToken", "channels.slack.accounts.work.appToken"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats top-level Google Chat serviceAccount as inactive when enabled accounts use serviceAccountRef", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
googlechat: {
|
||||
serviceAccount: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
serviceAccountRef: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "GOOGLECHAT_WORK_SERVICE_ACCOUNT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
GOOGLECHAT_WORK_SERVICE_ACCOUNT: "work-service-account-json",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.googlechat?.serviceAccount).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT",
|
||||
});
|
||||
expect(snapshot.config.channels?.googlechat?.accounts?.work?.serviceAccount).toBe(
|
||||
"work-service-account-json",
|
||||
);
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.googlechat.serviceAccount",
|
||||
);
|
||||
});
|
||||
});
|
||||
418
src/secrets/runtime-discord-surface.test.ts
Normal file
418
src/secrets/runtime-discord-surface.test.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
vi.mock("../channels/plugins/bootstrap-registry.js", async () => {
|
||||
const discordSecrets = await import("../../extensions/discord/src/secret-config-contract.ts");
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) =>
|
||||
id === "discord"
|
||||
? {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: discordSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
|
||||
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot discord surface", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("fails when non-default Discord account inherits an unresolved top-level token ref", async () => {
|
||||
await expect(
|
||||
prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_BASE_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
}),
|
||||
).rejects.toThrow('Environment variable "MISSING_DISCORD_BASE_TOKEN" is missing or empty.');
|
||||
});
|
||||
|
||||
it("treats top-level Discord token refs as inactive when account token is explicitly blank", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_DEFAULT_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
enabled: true,
|
||||
token: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.token).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_DEFAULT_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain("channels.discord.token");
|
||||
});
|
||||
|
||||
it("treats Discord PluralKit token refs as inactive when PluralKit is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
pluralkit: {
|
||||
enabled: false,
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_PLURALKIT_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.pluralkit?.token).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_PLURALKIT_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.discord.pluralkit.token",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Discord voice TTS refs as inactive when voice is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
});
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.providers?.openai?.apiKey,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.discord.voice.tts.providers.openai.apiKey",
|
||||
"channels.discord.accounts.work.voice.tts.providers.openai.apiKey",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles Discord nested inheritance for enabled and disabled accounts", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OPENAI" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluralkit: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BASE_PK_TOKEN" },
|
||||
},
|
||||
accounts: {
|
||||
enabledInherited: {
|
||||
enabled: true,
|
||||
},
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disabledOverride: {
|
||||
enabled: false,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluralkit: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_BASE_TTS_OPENAI: "base-tts-openai",
|
||||
DISCORD_BASE_PK_TOKEN: "base-pk-token",
|
||||
DISCORD_ENABLED_OVERRIDE_TTS_OPENAI: "enabled-override-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toBe(
|
||||
"base-tts-openai",
|
||||
);
|
||||
expect(snapshot.config.channels?.discord?.pluralkit?.token).toBe("base-pk-token");
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toBe("enabled-override-tts-openai");
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.config.channels?.discord?.accounts?.disabledOverride?.pluralkit?.token).toEqual(
|
||||
{
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN",
|
||||
},
|
||||
);
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.discord.accounts.disabledOverride.voice.tts.providers.openai.apiKey",
|
||||
"channels.discord.accounts.disabledOverride.pluralkit.token",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips top-level Discord voice refs when all enabled accounts override nested voice config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_UNUSED_BASE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_ONLY_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disabledInherited: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_ENABLED_ONLY_TTS_OPENAI: "enabled-only-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toBe("enabled-only-tts-openai");
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_UNUSED_BASE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.discord.voice.tts.providers.openai.apiKey",
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when an enabled Discord account override has an unresolved nested ref", async () => {
|
||||
await expect(
|
||||
prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_OVERRIDE_TTS_MISSING",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_BASE_TTS_OK: "base-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Environment variable "DISCORD_ENABLED_OVERRIDE_TTS_MISSING" is missing or empty.',
|
||||
);
|
||||
});
|
||||
});
|
||||
60
src/secrets/runtime-inactive-core-surfaces.test.ts
Normal file
60
src/secrets/runtime-inactive-core-surfaces.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
|
||||
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
describe("secrets runtime snapshot inactive core surfaces", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("skips inactive core refs and emits diagnostics", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
enabled: false,
|
||||
remote: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISABLED_MEMORY_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
password: { source: "env", provider: "default", id: "DISABLED_GATEWAY_PASSWORD" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: new Map(),
|
||||
});
|
||||
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"agents.defaults.memorySearch.remote.apiKey",
|
||||
"gateway.auth.password",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
78
src/secrets/runtime-inactive-telegram-surfaces.test.ts
Normal file
78
src/secrets/runtime-inactive-telegram-surfaces.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
vi.mock("../channels/plugins/bootstrap-registry.js", async () => {
|
||||
const telegramSecrets = await import("../../extensions/telegram/src/secret-contract.ts");
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) =>
|
||||
id === "telegram"
|
||||
? {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: telegramSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
|
||||
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
describe("secrets runtime snapshot inactive telegram surfaces", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("skips inactive Telegram refs and emits diagnostics", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "env", provider: "default", id: "DISABLED_TELEGRAM_BASE_TOKEN" },
|
||||
accounts: {
|
||||
disabled: {
|
||||
enabled: false,
|
||||
botToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_TELEGRAM_ACCOUNT_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: new Map(),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_TELEGRAM_BASE_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.telegram.botToken",
|
||||
"channels.telegram.accounts.disabled.botToken",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
|
||||
type WebProviderUnderTest = "brave" | "gemini";
|
||||
type WebProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl";
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
@@ -60,7 +59,7 @@ function createTestProvider(params: {
|
||||
getCredentialValue: readSearchConfigKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const providerConfig =
|
||||
params.id === "brave"
|
||||
params.id === "brave" || params.id === "firecrawl"
|
||||
? searchConfigTarget
|
||||
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
|
||||
providerConfig.apiKey = value;
|
||||
@@ -76,6 +75,12 @@ function createTestProvider(params: {
|
||||
const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown };
|
||||
webSearch.apiKey = value;
|
||||
},
|
||||
resolveRuntimeMetadata:
|
||||
params.id === "perplexity"
|
||||
? () => ({
|
||||
perplexityTransport: "search_api" as const,
|
||||
})
|
||||
: undefined,
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
@@ -84,6 +89,10 @@ function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
return [
|
||||
createTestProvider({ id: "brave", pluginId: "brave", order: 10 }),
|
||||
createTestProvider({ id: "gemini", pluginId: "google", order: 20 }),
|
||||
createTestProvider({ id: "grok", pluginId: "xai", order: 30 }),
|
||||
createTestProvider({ id: "kimi", pluginId: "moonshot", order: 40 }),
|
||||
createTestProvider({ id: "perplexity", pluginId: "perplexity", order: 50 }),
|
||||
createTestProvider({ id: "firecrawl", pluginId: "firecrawl", order: 60 }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -91,15 +100,9 @@ let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
|
||||
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map();
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot inactive surfaces", () => {
|
||||
describe("secrets runtime snapshot legacy x_search", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
@@ -117,89 +120,90 @@ describe("secrets runtime snapshot inactive surfaces", () => {
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("skips inactive-surface refs and emits diagnostics", async () => {
|
||||
const config = asConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
enabled: false,
|
||||
remote: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISABLED_MEMORY_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
password: { source: "env", provider: "default", id: "DISABLED_GATEWAY_PASSWORD" },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: { source: "env", provider: "default", id: "DISABLED_TELEGRAM_BASE_TOKEN" },
|
||||
accounts: {
|
||||
disabled: {
|
||||
enabled: false,
|
||||
botToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_TELEGRAM_ACCOUNT_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
apiKey: { source: "env", provider: "default", id: "DISABLED_WEB_SEARCH_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("keeps legacy x_search SecretRefs in place until doctor repairs them", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_KEY_REF: "xai-runtime-key",
|
||||
},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_TELEGRAM_BASE_TOKEN",
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
apiKey: "xai-runtime-key",
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
const ignoredInactiveWarnings = snapshot.warnings.filter(
|
||||
(warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
);
|
||||
expect(ignoredInactiveWarnings.length).toBeGreaterThanOrEqual(6);
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"agents.defaults.memorySearch.remote.apiKey",
|
||||
"gateway.auth.password",
|
||||
"channels.telegram.botToken",
|
||||
"channels.telegram.accounts.disabled.botToken",
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
"plugins.entries.google.config.webSearch.apiKey",
|
||||
]),
|
||||
);
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still resolves legacy x_search auth in place even when unrelated legacy config is present", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
groupMentionsOnly: true,
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_KEY_REF: "xai-runtime-key-invalid-config",
|
||||
},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
apiKey: "xai-runtime-key-invalid-config",
|
||||
enabled: true,
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not force-enable xai at runtime for knob-only x_search config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
@@ -15,6 +14,20 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/bootstrap-registry.js", async () => {
|
||||
const matrixSecrets = await import("../../extensions/matrix/src/secret-contract.ts");
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) =>
|
||||
id === "matrix"
|
||||
? {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
@@ -88,13 +101,6 @@ let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntim
|
||||
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
|
||||
let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot matrix access token", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
@@ -135,9 +141,8 @@ describe("secrets runtime snapshot matrix access token", () => {
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN: "default-matrix-token",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: new Map(),
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accessToken).toBe("default-matrix-token");
|
||||
|
||||
@@ -792,6 +792,49 @@ describe("runtime web tools resolution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("emits inactive warnings for configured and lower-priority web-search providers when search is disabled", async () => {
|
||||
const { context } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
apiKey: { source: "env", provider: "default", id: "DISABLED_WEB_SEARCH_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(context.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "plugins.entries.google.config.webSearch.apiKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not auto-enable search when tools.web.search is absent", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({}),
|
||||
|
||||
@@ -382,7 +382,8 @@ export async function resolveRuntimeWebTools(params: {
|
||||
|
||||
const sourceTools = isRecord(params.sourceConfig.tools) ? params.sourceConfig.tools : undefined;
|
||||
const sourceWeb = isRecord(sourceTools?.web) ? sourceTools.web : undefined;
|
||||
if (!sourceWeb && !hasPluginWebToolConfig(params.sourceConfig)) {
|
||||
const hasPluginWebConfig = hasPluginWebToolConfig(params.sourceConfig);
|
||||
if (!sourceWeb && !hasPluginWebConfig) {
|
||||
return {
|
||||
search: {
|
||||
providerSource: "none",
|
||||
@@ -396,6 +397,20 @@ export async function resolveRuntimeWebTools(params: {
|
||||
};
|
||||
}
|
||||
const search = isRecord(sourceWeb?.search) ? sourceWeb.search : undefined;
|
||||
const fetch = isRecord(sourceWeb?.fetch) ? (sourceWeb.fetch as FetchConfig) : undefined;
|
||||
if (!search && !fetch && !hasPluginWebConfig) {
|
||||
return {
|
||||
search: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
const rawProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledPluginId = resolveManifestContractOwnerPluginId({
|
||||
@@ -703,7 +718,6 @@ export async function resolveRuntimeWebTools(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = isRecord(sourceWeb?.fetch) ? (sourceWeb.fetch as FetchConfig) : undefined;
|
||||
const rawFetchProvider =
|
||||
typeof fetch?.provider === "string" ? fetch.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledFetchPluginId = resolveManifestContractOwnerPluginId({
|
||||
|
||||
@@ -1185,565 +1185,6 @@ describe("secrets runtime snapshot", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("treats IRC account nickserv password refs as inactive when nickserv is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
irc: {
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
nickserv: {
|
||||
enabled: false,
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_WORK_NICKSERV_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.irc?.accounts?.work?.nickserv?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_WORK_NICKSERV_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.irc.accounts.work.nickserv.password",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats top-level IRC nickserv password refs as inactive when nickserv is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
irc: {
|
||||
nickserv: {
|
||||
enabled: false,
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.irc?.nickserv?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.irc.nickserv.password",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Slack signingSecret refs as inactive when mode is socket", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "socket",
|
||||
signingSecret: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_SIGNING_SECRET",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
mode: "socket",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.slack?.signingSecret).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_SIGNING_SECRET",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.slack.signingSecret",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Slack appToken refs as inactive when mode is http", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "http",
|
||||
appToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_APP_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
appToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_WORK_APP_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.slack?.appToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_APP_TOKEN",
|
||||
});
|
||||
expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_SLACK_WORK_APP_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining(["channels.slack.appToken", "channels.slack.accounts.work.appToken"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats top-level Google Chat serviceAccount as inactive when enabled accounts use serviceAccountRef", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
googlechat: {
|
||||
serviceAccount: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
serviceAccountRef: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "GOOGLECHAT_WORK_SERVICE_ACCOUNT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
GOOGLECHAT_WORK_SERVICE_ACCOUNT: "work-service-account-json",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.googlechat?.serviceAccount).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT",
|
||||
});
|
||||
expect(snapshot.config.channels?.googlechat?.accounts?.work?.serviceAccount).toBe(
|
||||
"work-service-account-json",
|
||||
);
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.googlechat.serviceAccount",
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when non-default Discord account inherits an unresolved top-level token ref", async () => {
|
||||
await expect(
|
||||
prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_BASE_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
}),
|
||||
).rejects.toThrow('Environment variable "MISSING_DISCORD_BASE_TOKEN" is missing or empty.');
|
||||
});
|
||||
|
||||
it("treats top-level Discord token refs as inactive when account token is explicitly blank", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_DEFAULT_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
enabled: true,
|
||||
token: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.token).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_DEFAULT_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain("channels.discord.token");
|
||||
});
|
||||
|
||||
it("treats Discord PluralKit token refs as inactive when PluralKit is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
pluralkit: {
|
||||
enabled: false,
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_PLURALKIT_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.pluralkit?.token).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_PLURALKIT_TOKEN",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.discord.pluralkit.token",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Discord voice TTS refs as inactive when voice is disabled", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
});
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.providers?.openai?.apiKey,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.discord.voice.tts.providers.openai.apiKey",
|
||||
"channels.discord.accounts.work.voice.tts.providers.openai.apiKey",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles Discord nested inheritance for enabled and disabled accounts", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OPENAI" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluralkit: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BASE_PK_TOKEN" },
|
||||
},
|
||||
accounts: {
|
||||
enabledInherited: {
|
||||
enabled: true,
|
||||
},
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disabledOverride: {
|
||||
enabled: false,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluralkit: {
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_BASE_TTS_OPENAI: "base-tts-openai",
|
||||
DISCORD_BASE_PK_TOKEN: "base-pk-token",
|
||||
DISCORD_ENABLED_OVERRIDE_TTS_OPENAI: "enabled-override-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toBe(
|
||||
"base-tts-openai",
|
||||
);
|
||||
expect(snapshot.config.channels?.discord?.pluralkit?.token).toBe("base-pk-token");
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toBe("enabled-override-tts-openai");
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.config.channels?.discord?.accounts?.disabledOverride?.pluralkit?.token).toEqual(
|
||||
{
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN",
|
||||
},
|
||||
);
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"channels.discord.accounts.disabledOverride.voice.tts.providers.openai.apiKey",
|
||||
"channels.discord.accounts.disabledOverride.pluralkit.token",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips top-level Discord voice refs when all enabled accounts override nested voice config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_UNUSED_BASE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_ONLY_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
disabledInherited: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_ENABLED_ONLY_TTS_OPENAI: "enabled-only-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(
|
||||
snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.providers?.openai
|
||||
?.apiKey,
|
||||
).toBe("enabled-only-tts-openai");
|
||||
expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_UNUSED_BASE_TTS_OPENAI",
|
||||
});
|
||||
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
||||
"channels.discord.voice.tts.providers.openai.apiKey",
|
||||
);
|
||||
});
|
||||
|
||||
it("fails when an enabled Discord account override has an unresolved nested ref", async () => {
|
||||
await expect(
|
||||
prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
discord: {
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
enabledOverride: {
|
||||
enabled: true,
|
||||
voice: {
|
||||
tts: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISCORD_ENABLED_OVERRIDE_TTS_MISSING",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
DISCORD_BASE_TTS_OK: "base-tts-openai",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Environment variable "DISCORD_ENABLED_OVERRIDE_TTS_MISSING" is missing or empty.',
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves SecretRef objects for active acpx MCP env vars", async () => {
|
||||
const config = asConfig({
|
||||
plugins: {
|
||||
@@ -1946,56 +1387,4 @@ describe("secrets runtime snapshot", () => {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves legacy x_search SecretRefs untouched so doctor owns the migration", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
X_SEARCH_KEY_REF: "xai-runtime-key",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" },
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not force-enable xai at runtime for knob-only x_search config", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
x_search: {
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect((snapshot.config.tools?.web as Record<string, unknown> | undefined)?.x_search).toEqual({
|
||||
enabled: true,
|
||||
model: "grok-4-1-fast",
|
||||
});
|
||||
expect(snapshot.config.plugins?.entries?.xai).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user