mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
* Secrets: add inline allowlist review set * Secrets: narrow detect-secrets file exclusions * Secrets: exclude Docker fingerprint false positive * Secrets: allowlist test and docs false positives * Secrets: refresh baseline after allowlist updates * Secrets: fix gateway chat fixture pragma * Secrets: format pre-commit config * Android: keep talk mode fixture JSON valid * Feishu: rely on client timeout injection * Secrets: allowlist provider auth test fixtures * Secrets: allowlist onboard search fixtures * Secrets: allowlist onboard mode fixture * Secrets: allowlist gateway auth mode fixture * Secrets: allowlist APNS wake test key * Secrets: allowlist gateway reload fixtures * Secrets: allowlist moonshot video fixture * Secrets: allowlist auto audio fixture * Secrets: allowlist tiny audio fixture * Secrets: allowlist embeddings fixtures * Secrets: allowlist resolve fixtures * Secrets: allowlist target registry pattern fixtures * Secrets: allowlist gateway chat env fixture * Secrets: refresh baseline after fixture allowlists * Secrets: reapply gateway chat env allowlist * Secrets: reapply gateway chat env allowlist * Secrets: stabilize gateway chat env allowlist * Secrets: allowlist runtime snapshot save fixture * Secrets: allowlist oauth profile fixtures * Secrets: allowlist compaction identifier fixture * Secrets: allowlist model auth fixture * Secrets: allowlist model status fixtures * Secrets: allowlist custom onboarding fixture * Secrets: allowlist mattermost token summary fixtures * Secrets: allowlist gateway auth suite fixtures * Secrets: allowlist channel summary fixture * Secrets: allowlist provider usage auth fixtures * Secrets: allowlist media proxy fixture * Secrets: allowlist secrets audit fixtures * Secrets: refresh baseline after final fixture allowlists * Feishu: prefer explicit client timeout * Feishu: test direct timeout precedence
1948 lines
63 KiB
TypeScript
1948 lines
63 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
|
|
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
activateSecretsRuntimeSnapshot,
|
|
clearSecretsRuntimeSnapshot,
|
|
prepareSecretsRuntimeSnapshot,
|
|
} from "./runtime.js";
|
|
|
|
function asConfig(value: unknown): OpenClawConfig {
|
|
return value as OpenClawConfig;
|
|
}
|
|
|
|
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
|
|
|
|
function createOpenAiFileModelsConfig(): NonNullable<OpenClawConfig["models"]> {
|
|
return {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
|
models: [],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
|
return {
|
|
version: 1,
|
|
profiles,
|
|
};
|
|
}
|
|
|
|
describe("secrets runtime snapshot", () => {
|
|
afterEach(() => {
|
|
clearSecretsRuntimeSnapshot();
|
|
});
|
|
|
|
it("resolves env refs for config and auth profiles", async () => {
|
|
const config = asConfig({
|
|
agents: {
|
|
defaults: {
|
|
memorySearch: {
|
|
remote: {
|
|
apiKey: { source: "env", provider: "default", id: "MEMORY_REMOTE_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
skills: {
|
|
entries: {
|
|
"review-pr": {
|
|
enabled: true,
|
|
apiKey: { source: "env", provider: "default", id: "REVIEW_SKILL_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
talk: {
|
|
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
|
|
providers: {
|
|
elevenlabs: {
|
|
apiKey: { source: "env", provider: "default", id: "TALK_PROVIDER_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
gateway: {
|
|
mode: "remote",
|
|
remote: {
|
|
url: "wss://gateway.example",
|
|
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" },
|
|
password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" },
|
|
},
|
|
},
|
|
channels: {
|
|
telegram: {
|
|
botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN_REF" },
|
|
webhookUrl: "https://example.test/telegram-webhook",
|
|
webhookSecret: { source: "env", provider: "default", id: "TELEGRAM_WEBHOOK_SECRET_REF" },
|
|
accounts: {
|
|
work: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "TELEGRAM_WORK_BOT_TOKEN_REF",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
slack: {
|
|
mode: "http",
|
|
signingSecret: { source: "env", provider: "default", id: "SLACK_SIGNING_SECRET_REF" },
|
|
accounts: {
|
|
work: {
|
|
botToken: { source: "env", provider: "default", id: "SLACK_WORK_BOT_TOKEN_REF" },
|
|
appToken: { source: "env", provider: "default", id: "SLACK_WORK_APP_TOKEN_REF" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: {
|
|
OPENAI_API_KEY: "sk-env-openai", // pragma: allowlist secret
|
|
GITHUB_TOKEN: "ghp-env-token", // pragma: allowlist secret
|
|
REVIEW_SKILL_API_KEY: "sk-skill-ref", // pragma: allowlist secret
|
|
MEMORY_REMOTE_API_KEY: "mem-ref-key", // pragma: allowlist secret
|
|
TALK_API_KEY: "talk-ref-key", // pragma: allowlist secret
|
|
TALK_PROVIDER_API_KEY: "talk-provider-ref-key", // pragma: allowlist secret
|
|
REMOTE_GATEWAY_TOKEN: "remote-token-ref",
|
|
REMOTE_GATEWAY_PASSWORD: "remote-password-ref", // pragma: allowlist secret
|
|
TELEGRAM_BOT_TOKEN_REF: "telegram-bot-ref",
|
|
TELEGRAM_WEBHOOK_SECRET_REF: "telegram-webhook-ref", // pragma: allowlist secret
|
|
TELEGRAM_WORK_BOT_TOKEN_REF: "telegram-work-ref",
|
|
SLACK_SIGNING_SECRET_REF: "slack-signing-ref", // pragma: allowlist secret
|
|
SLACK_WORK_BOT_TOKEN_REF: "slack-work-bot-ref",
|
|
SLACK_WORK_APP_TOKEN_REF: "slack-work-app-ref",
|
|
WEB_SEARCH_API_KEY: "web-search-ref", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () =>
|
|
loadAuthStoreWithProfiles({
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
key: "old-openai",
|
|
keyRef: OPENAI_ENV_KEY_REF,
|
|
},
|
|
"github-copilot:default": {
|
|
type: "token",
|
|
provider: "github-copilot",
|
|
token: "old-gh",
|
|
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
|
},
|
|
"openai:inline": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
key: "${OPENAI_API_KEY}",
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-env-openai");
|
|
expect(snapshot.config.skills?.entries?.["review-pr"]?.apiKey).toBe("sk-skill-ref");
|
|
expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toBe("mem-ref-key");
|
|
expect(snapshot.config.talk?.apiKey).toBe("talk-ref-key");
|
|
expect(snapshot.config.talk?.providers?.elevenlabs?.apiKey).toBe("talk-provider-ref-key");
|
|
expect(snapshot.config.gateway?.remote?.token).toBe("remote-token-ref");
|
|
expect(snapshot.config.gateway?.remote?.password).toBe("remote-password-ref");
|
|
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "TELEGRAM_BOT_TOKEN_REF",
|
|
});
|
|
expect(snapshot.config.channels?.telegram?.webhookSecret).toBe("telegram-webhook-ref");
|
|
expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe("telegram-work-ref");
|
|
expect(snapshot.config.channels?.slack?.signingSecret).toBe("slack-signing-ref");
|
|
expect(snapshot.config.channels?.slack?.accounts?.work?.botToken).toBe("slack-work-bot-ref");
|
|
expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "SLACK_WORK_APP_TOKEN_REF",
|
|
});
|
|
expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref");
|
|
expect(snapshot.warnings).toHaveLength(4);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"channels.slack.accounts.work.appToken",
|
|
);
|
|
expect(snapshot.authStores[0]?.store.profiles["openai:default"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-env-openai",
|
|
});
|
|
expect(snapshot.authStores[0]?.store.profiles["github-copilot:default"]).toMatchObject({
|
|
type: "token",
|
|
token: "ghp-env-token",
|
|
});
|
|
expect(snapshot.authStores[0]?.store.profiles["openai:inline"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-env-openai",
|
|
});
|
|
// After normalization, inline SecretRef string should be promoted to keyRef
|
|
expect(
|
|
(snapshot.authStores[0].store.profiles["openai:inline"] as Record<string, unknown>).keyRef,
|
|
).toEqual({ source: "env", provider: "default", id: "OPENAI_API_KEY" });
|
|
});
|
|
|
|
it("normalizes inline SecretRef object on token to tokenRef", async () => {
|
|
const config: OpenClawConfig = { models: {}, secrets: {} };
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: { MY_TOKEN: "resolved-token-value" },
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () =>
|
|
loadAuthStoreWithProfiles({
|
|
"custom:inline-token": {
|
|
type: "token",
|
|
provider: "custom",
|
|
token: { source: "env", provider: "default", id: "MY_TOKEN" } as unknown as string,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const profile = snapshot.authStores[0]?.store.profiles["custom:inline-token"] as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
// tokenRef should be set from the inline SecretRef
|
|
expect(profile.tokenRef).toEqual({ source: "env", provider: "default", id: "MY_TOKEN" });
|
|
// token should be resolved to the actual value after activation
|
|
activateSecretsRuntimeSnapshot(snapshot);
|
|
expect(profile.token).toBe("resolved-token-value");
|
|
});
|
|
|
|
it("normalizes inline SecretRef object on key to keyRef", async () => {
|
|
const config: OpenClawConfig = { models: {}, secrets: {} };
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: { MY_KEY: "resolved-key-value" },
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () =>
|
|
loadAuthStoreWithProfiles({
|
|
"custom:inline-key": {
|
|
type: "api_key",
|
|
provider: "custom",
|
|
key: { source: "env", provider: "default", id: "MY_KEY" } as unknown as string,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const profile = snapshot.authStores[0]?.store.profiles["custom:inline-key"] as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
// keyRef should be set from the inline SecretRef
|
|
expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "MY_KEY" });
|
|
// key should be resolved to the actual value after activation
|
|
activateSecretsRuntimeSnapshot(snapshot);
|
|
expect(profile.key).toBe("resolved-key-value");
|
|
});
|
|
|
|
it("keeps explicit keyRef when inline key SecretRef is also present", async () => {
|
|
const config: OpenClawConfig = { models: {}, secrets: {} };
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: {
|
|
PRIMARY_KEY: "primary-key-value",
|
|
SHADOW_KEY: "shadow-key-value",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () =>
|
|
loadAuthStoreWithProfiles({
|
|
"custom:explicit-keyref": {
|
|
type: "api_key",
|
|
provider: "custom",
|
|
keyRef: { source: "env", provider: "default", id: "PRIMARY_KEY" },
|
|
key: { source: "env", provider: "default", id: "SHADOW_KEY" } as unknown as string,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const profile = snapshot.authStores[0]?.store.profiles["custom:explicit-keyref"] as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "PRIMARY_KEY" });
|
|
activateSecretsRuntimeSnapshot(snapshot);
|
|
expect(profile.key).toBe("primary-key-value");
|
|
});
|
|
|
|
it("treats non-selected web search provider refs as inactive", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
enabled: true,
|
|
provider: "brave",
|
|
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" },
|
|
grok: {
|
|
apiKey: { source: "env", provider: "default", id: "MISSING_GROK_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
WEB_SEARCH_API_KEY: "web-search-ref", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref");
|
|
expect(snapshot.config.tools?.web?.search?.grok?.apiKey).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_GROK_API_KEY",
|
|
});
|
|
expect(snapshot.warnings).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
|
path: "tools.web.search.grok.apiKey",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("resolves provider-specific refs in web search auto mode", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
enabled: true,
|
|
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" },
|
|
gemini: {
|
|
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
WEB_SEARCH_API_KEY: "web-search-ref", // pragma: allowlist secret
|
|
WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-ref", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref");
|
|
expect(snapshot.config.tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-ref");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"tools.web.search.gemini.apiKey",
|
|
);
|
|
});
|
|
|
|
it("resolves selected web search provider ref even when provider config is disabled", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
enabled: true,
|
|
provider: "gemini",
|
|
gemini: {
|
|
enabled: false,
|
|
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-ref", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-ref");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"tools.web.search.gemini.apiKey",
|
|
);
|
|
});
|
|
|
|
it("resolves file refs via configured file provider", async () => {
|
|
if (process.platform === "win32") {
|
|
return;
|
|
}
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-"));
|
|
const secretsPath = path.join(root, "secrets.json");
|
|
try {
|
|
await fs.writeFile(
|
|
secretsPath,
|
|
JSON.stringify(
|
|
{
|
|
providers: {
|
|
openai: {
|
|
apiKey: "sk-from-file-provider", // pragma: allowlist secret
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
await fs.chmod(secretsPath, 0o600);
|
|
|
|
const config = asConfig({
|
|
secrets: {
|
|
providers: {
|
|
default: {
|
|
source: "file",
|
|
path: secretsPath,
|
|
mode: "json",
|
|
},
|
|
},
|
|
defaults: {
|
|
file: "default",
|
|
},
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-from-file-provider");
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("fails when file provider payload is not a JSON object", async () => {
|
|
if (process.platform === "win32") {
|
|
return;
|
|
}
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-bad-"));
|
|
const secretsPath = path.join(root, "secrets.json");
|
|
try {
|
|
await fs.writeFile(secretsPath, JSON.stringify(["not-an-object"]), "utf8");
|
|
await fs.chmod(secretsPath, 0o600);
|
|
|
|
await expect(
|
|
prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
secrets: {
|
|
providers: {
|
|
default: {
|
|
source: "file",
|
|
path: secretsPath,
|
|
mode: "json",
|
|
},
|
|
},
|
|
},
|
|
models: {
|
|
...createOpenAiFileModelsConfig(),
|
|
},
|
|
}),
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
}),
|
|
).rejects.toThrow("payload is not a JSON object");
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
|
const prepared = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: { OPENAI_API_KEY: "sk-runtime" }, // pragma: allowlist secret
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () =>
|
|
loadAuthStoreWithProfiles({
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
keyRef: OPENAI_ENV_KEY_REF,
|
|
},
|
|
}),
|
|
});
|
|
|
|
activateSecretsRuntimeSnapshot(prepared);
|
|
|
|
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-runtime");
|
|
const store = ensureAuthProfileStore("/tmp/openclaw-agent-main");
|
|
expect(store.profiles["openai:default"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-runtime",
|
|
});
|
|
});
|
|
|
|
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" },
|
|
gemini: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISABLED_TELEGRAM_BASE_TOKEN",
|
|
});
|
|
expect(
|
|
snapshot.warnings.filter(
|
|
(warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
|
),
|
|
).toHaveLength(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",
|
|
"tools.web.search.apiKey",
|
|
"tools.web.search.gemini.apiKey",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("treats gateway.remote refs as inactive when local auth credentials are configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
mode: "password",
|
|
token: "local-token",
|
|
password: "local-password", // pragma: allowlist secret
|
|
},
|
|
remote: {
|
|
enabled: true,
|
|
token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" },
|
|
password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" },
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.remote?.token).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_REMOTE_TOKEN",
|
|
});
|
|
expect(snapshot.config.gateway?.remote?.password).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_REMOTE_PASSWORD",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
|
expect.arrayContaining(["gateway.remote.token", "gateway.remote.password"]),
|
|
);
|
|
});
|
|
|
|
it("treats gateway.auth.password ref as active when mode is unset and no token is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
auth: {
|
|
password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
GATEWAY_PASSWORD_REF: "resolved-gateway-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.auth?.password).toBe("resolved-gateway-password");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.auth.password");
|
|
});
|
|
|
|
it("treats gateway.auth.token ref as active when token mode is explicit", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
auth: {
|
|
mode: "token",
|
|
token: { source: "env", provider: "default", id: "GATEWAY_TOKEN_REF" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
GATEWAY_TOKEN_REF: "resolved-gateway-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.auth?.token).toBe("resolved-gateway-token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.auth.token");
|
|
});
|
|
|
|
it("treats gateway.auth.token ref as inactive when password mode is explicit", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
auth: {
|
|
mode: "password",
|
|
token: { source: "env", provider: "default", id: "GATEWAY_TOKEN_REF" },
|
|
password: "password-123", // pragma: allowlist secret
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
GATEWAY_TOKEN_REF: "resolved-gateway-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.auth?.token).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "GATEWAY_TOKEN_REF",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.token");
|
|
});
|
|
|
|
it("fails when gateway.auth.token ref is active and unresolved", async () => {
|
|
await expect(
|
|
prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
auth: {
|
|
mode: "token",
|
|
token: { source: "env", provider: "default", id: "MISSING_GATEWAY_TOKEN_REF" },
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
}),
|
|
).rejects.toThrow(/MISSING_GATEWAY_TOKEN_REF/i);
|
|
});
|
|
|
|
it("treats gateway.auth.password ref as inactive when auth mode is trusted-proxy", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
auth: {
|
|
mode: "trusted-proxy",
|
|
password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
GATEWAY_PASSWORD_REF: "resolved-gateway-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.auth?.password).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "GATEWAY_PASSWORD_REF",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.password");
|
|
});
|
|
|
|
it("treats gateway.auth.password ref as inactive when remote token is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" },
|
|
},
|
|
remote: {
|
|
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
REMOTE_GATEWAY_TOKEN: "remote-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.auth?.password).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "GATEWAY_PASSWORD_REF",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.password");
|
|
});
|
|
|
|
it.each(["none", "trusted-proxy"] as const)(
|
|
"treats gateway.remote refs as inactive in local mode when auth mode is %s",
|
|
async (mode) => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
mode,
|
|
},
|
|
remote: {
|
|
token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" },
|
|
password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" },
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.remote?.token).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_REMOTE_TOKEN",
|
|
});
|
|
expect(snapshot.config.gateway?.remote?.password).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_REMOTE_PASSWORD",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toEqual(
|
|
expect.arrayContaining(["gateway.remote.token", "gateway.remote.password"]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("treats gateway.remote.token ref as active in local mode when no local credentials are configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {},
|
|
remote: {
|
|
enabled: true,
|
|
token: { source: "env", provider: "default", id: "REMOTE_TOKEN" },
|
|
password: { source: "env", provider: "default", id: "REMOTE_PASSWORD" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
REMOTE_TOKEN: "resolved-remote-token",
|
|
REMOTE_PASSWORD: "resolved-remote-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.remote?.token).toBe("resolved-remote-token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.remote.token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.remote.password");
|
|
});
|
|
|
|
it("treats gateway.remote.password ref as active in local mode when password can win", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {},
|
|
remote: {
|
|
enabled: true,
|
|
password: { source: "env", provider: "default", id: "REMOTE_PASSWORD" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
REMOTE_PASSWORD: "resolved-remote-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.remote?.password).toBe("resolved-remote-password");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"gateway.remote.password",
|
|
);
|
|
});
|
|
|
|
it("treats top-level Zalo botToken refs as active even when tokenFile is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
zalo: {
|
|
botToken: { source: "env", provider: "default", id: "ZALO_BOT_TOKEN" },
|
|
tokenFile: "/tmp/missing-zalo-token-file",
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
ZALO_BOT_TOKEN: "resolved-zalo-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.zalo?.botToken).toBe("resolved-zalo-token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.zalo.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats account-level Zalo botToken refs as active even when tokenFile is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
zalo: {
|
|
accounts: {
|
|
work: {
|
|
botToken: { source: "env", provider: "default", id: "ZALO_WORK_BOT_TOKEN" },
|
|
tokenFile: "/tmp/missing-zalo-work-token-file",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
ZALO_WORK_BOT_TOKEN: "resolved-zalo-work-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.zalo?.accounts?.work?.botToken).toBe(
|
|
"resolved-zalo-work-token",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.zalo.accounts.work.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats top-level Zalo botToken refs as active for non-default accounts without overrides", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
zalo: {
|
|
botToken: { source: "env", provider: "default", id: "ZALO_TOP_LEVEL_TOKEN" },
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
ZALO_TOP_LEVEL_TOKEN: "resolved-zalo-top-level-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.zalo?.botToken).toBe("resolved-zalo-top-level-token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.zalo.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats channels.zalo.accounts.default.botToken refs as active", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
zalo: {
|
|
accounts: {
|
|
default: {
|
|
enabled: true,
|
|
botToken: { source: "env", provider: "default", id: "ZALO_DEFAULT_TOKEN" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
ZALO_DEFAULT_TOKEN: "resolved-zalo-default-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.zalo?.accounts?.default?.botToken).toBe(
|
|
"resolved-zalo-default-token",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.zalo.accounts.default.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats top-level Nextcloud Talk botSecret and apiPassword refs as active when file paths are configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
"nextcloud-talk": {
|
|
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_BOT_SECRET" },
|
|
botSecretFile: "/tmp/missing-nextcloud-bot-secret-file",
|
|
apiUser: "bot-user",
|
|
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_API_PASSWORD" },
|
|
apiPasswordFile: "/tmp/missing-nextcloud-api-password-file",
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
NEXTCLOUD_BOT_SECRET: "resolved-nextcloud-bot-secret", // pragma: allowlist secret
|
|
NEXTCLOUD_API_PASSWORD: "resolved-nextcloud-api-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.["nextcloud-talk"]?.botSecret).toBe(
|
|
"resolved-nextcloud-bot-secret",
|
|
);
|
|
expect(snapshot.config.channels?.["nextcloud-talk"]?.apiPassword).toBe(
|
|
"resolved-nextcloud-api-password",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.nextcloud-talk.botSecret",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.nextcloud-talk.apiPassword",
|
|
);
|
|
});
|
|
|
|
it("treats account-level Nextcloud Talk botSecret and apiPassword refs as active when file paths are configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
"nextcloud-talk": {
|
|
accounts: {
|
|
work: {
|
|
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_WORK_BOT_SECRET" },
|
|
botSecretFile: "/tmp/missing-nextcloud-work-bot-secret-file",
|
|
apiPassword: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "NEXTCLOUD_WORK_API_PASSWORD",
|
|
},
|
|
apiPasswordFile: "/tmp/missing-nextcloud-work-api-password-file",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
NEXTCLOUD_WORK_BOT_SECRET: "resolved-nextcloud-work-bot-secret", // pragma: allowlist secret
|
|
NEXTCLOUD_WORK_API_PASSWORD: "resolved-nextcloud-work-api-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.botSecret).toBe(
|
|
"resolved-nextcloud-work-bot-secret",
|
|
);
|
|
expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.apiPassword).toBe(
|
|
"resolved-nextcloud-work-api-password",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.nextcloud-talk.accounts.work.botSecret",
|
|
);
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.nextcloud-talk.accounts.work.apiPassword",
|
|
);
|
|
});
|
|
|
|
it("treats gateway.remote refs as active when tailscale serve is enabled", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
gateway: {
|
|
mode: "local",
|
|
tailscale: { mode: "serve" },
|
|
remote: {
|
|
enabled: true,
|
|
token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" },
|
|
password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" },
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
REMOTE_GATEWAY_TOKEN: "tailscale-remote-token",
|
|
REMOTE_GATEWAY_PASSWORD: "tailscale-remote-password", // pragma: allowlist secret
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.gateway?.remote?.token).toBe("tailscale-remote-token");
|
|
expect(snapshot.config.gateway?.remote?.password).toBe("tailscale-remote-password");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.remote.token");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"gateway.remote.password",
|
|
);
|
|
});
|
|
|
|
it("treats defaults memorySearch ref as inactive when all enabled agents disable memorySearch", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
agents: {
|
|
defaults: {
|
|
memorySearch: {
|
|
remote: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DEFAULT_MEMORY_REMOTE_API_KEY",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
list: [
|
|
{
|
|
enabled: true,
|
|
memorySearch: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DEFAULT_MEMORY_REMOTE_API_KEY",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"agents.defaults.memorySearch.remote.apiKey",
|
|
);
|
|
});
|
|
|
|
it("fails when enabled channel surfaces contain unresolved refs", async () => {
|
|
await expect(
|
|
prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_ENABLED_TELEGRAM_TOKEN",
|
|
},
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
}),
|
|
).rejects.toThrow('Environment variable "MISSING_ENABLED_TELEGRAM_TOKEN" is missing or empty.');
|
|
});
|
|
|
|
it("fails when default Telegram account can inherit an unresolved top-level token ref", async () => {
|
|
await expect(
|
|
prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_ENABLED_TELEGRAM_TOKEN",
|
|
},
|
|
accounts: {
|
|
default: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
}),
|
|
).rejects.toThrow('Environment variable "MISSING_ENABLED_TELEGRAM_TOKEN" is missing or empty.');
|
|
});
|
|
|
|
it("treats top-level Telegram token as inactive when all enabled accounts override it", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "UNUSED_TELEGRAM_BASE_TOKEN",
|
|
},
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "TELEGRAM_WORK_TOKEN",
|
|
},
|
|
},
|
|
disabled: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
TELEGRAM_WORK_TOKEN: "telegram-work-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe(
|
|
"telegram-work-token",
|
|
);
|
|
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "UNUSED_TELEGRAM_BASE_TOKEN",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"channels.telegram.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats Telegram account overrides as enabled when account.enabled is omitted", async () => {
|
|
await expect(
|
|
prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
enabled: true,
|
|
accounts: {
|
|
inheritedEnabled: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_INHERITED_TELEGRAM_ACCOUNT_TOKEN",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
}),
|
|
).rejects.toThrow(
|
|
'Environment variable "MISSING_INHERITED_TELEGRAM_ACCOUNT_TOKEN" is missing or empty.',
|
|
);
|
|
});
|
|
|
|
it("treats Telegram webhookSecret refs as inactive when webhook mode is not configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
webhookSecret: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_WEBHOOK_SECRET",
|
|
},
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.webhookSecret).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_WEBHOOK_SECRET",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"channels.telegram.webhookSecret",
|
|
);
|
|
});
|
|
|
|
it("treats Telegram top-level botToken refs as inactive when tokenFile is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
tokenFile: "/tmp/telegram-bot-token",
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_BOT_TOKEN",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_BOT_TOKEN",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"channels.telegram.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats Telegram account botToken refs as inactive when account tokenFile is configured", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
tokenFile: "/tmp/telegram-work-bot-token",
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_WORK_BOT_TOKEN",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_TELEGRAM_WORK_BOT_TOKEN",
|
|
});
|
|
expect(snapshot.warnings.map((warning) => warning.path)).toContain(
|
|
"channels.telegram.accounts.work.botToken",
|
|
);
|
|
});
|
|
|
|
it("treats top-level Telegram botToken refs as active when account botToken is blank", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
telegram: {
|
|
botToken: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "TELEGRAM_BASE_TOKEN",
|
|
},
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
botToken: "",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
env: {
|
|
TELEGRAM_BASE_TOKEN: "telegram-base-token",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
|
});
|
|
|
|
expect(snapshot.config.channels?.telegram?.botToken).toBe("telegram-base-token");
|
|
expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe("");
|
|
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
|
|
"channels.telegram.botToken",
|
|
);
|
|
});
|
|
|
|
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: {
|
|
openai: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
accounts: {
|
|
work: {
|
|
enabled: true,
|
|
voice: {
|
|
enabled: false,
|
|
tts: {
|
|
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?.openai?.apiKey).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
|
});
|
|
expect(snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.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.openai.apiKey",
|
|
"channels.discord.accounts.work.voice.tts.openai.apiKey",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("handles Discord nested inheritance for enabled and disabled accounts", async () => {
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config: asConfig({
|
|
channels: {
|
|
discord: {
|
|
voice: {
|
|
tts: {
|
|
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: {
|
|
openai: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
disabledOverride: {
|
|
enabled: false,
|
|
voice: {
|
|
tts: {
|
|
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?.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?.openai?.apiKey,
|
|
).toBe("enabled-override-tts-openai");
|
|
expect(
|
|
snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.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.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: {
|
|
openai: {
|
|
apiKey: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "DISCORD_UNUSED_BASE_TTS_OPENAI",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
accounts: {
|
|
enabledOverride: {
|
|
enabled: true,
|
|
voice: {
|
|
tts: {
|
|
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?.openai?.apiKey,
|
|
).toBe("enabled-only-tts-openai");
|
|
expect(snapshot.config.channels?.discord?.voice?.tts?.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.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: {
|
|
openai: {
|
|
apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" },
|
|
},
|
|
},
|
|
},
|
|
accounts: {
|
|
enabledOverride: {
|
|
enabled: true,
|
|
voice: {
|
|
tts: {
|
|
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("does not write inherited auth stores during runtime secret activation", async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-runtime-"));
|
|
const stateDir = path.join(root, ".openclaw");
|
|
const mainAgentDir = path.join(stateDir, "agents", "main", "agent");
|
|
const workerStorePath = path.join(stateDir, "agents", "worker", "agent", "auth-profiles.json");
|
|
const prevStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
|
|
try {
|
|
await fs.mkdir(mainAgentDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(mainAgentDir, "auth-profiles.json"),
|
|
JSON.stringify({
|
|
...loadAuthStoreWithProfiles({
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
keyRef: OPENAI_ENV_KEY_REF,
|
|
},
|
|
}),
|
|
}),
|
|
"utf8",
|
|
);
|
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
|
|
await prepareSecretsRuntimeSnapshot({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "worker" }],
|
|
},
|
|
},
|
|
env: { OPENAI_API_KEY: "sk-runtime-worker" }, // pragma: allowlist secret
|
|
});
|
|
|
|
await expect(fs.access(workerStorePath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
if (prevStateDir === undefined) {
|
|
delete process.env.OPENCLAW_STATE_DIR;
|
|
} else {
|
|
process.env.OPENCLAW_STATE_DIR = prevStateDir;
|
|
}
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|