mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 12:00:43 +00:00
* config(browser): allow profile attachOnly field * config(schema): accept profile attachOnly * browser(config): resolve per-profile attachOnly * browser(runtime): honor profile attachOnly checks * browser(routes): expose profile attachOnly in status * config(labels): add browser profile attachOnly label * config(help): document browser profile attachOnly * test(config): cover profile attachOnly resolution * test(browser): cover profile attachOnly runtime path * test(config): include profile attachOnly help target * changelog: note profile attachOnly override * browser(runtime): prioritize attachOnly over loopback ownership error * test(browser): cover attachOnly ws-failure ownership path
797 lines
26 KiB
TypeScript
797 lines
26 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { FIELD_HELP } from "./schema.help.js";
|
|
import { FIELD_LABELS } from "./schema.labels.js";
|
|
|
|
const ROOT_SECTIONS = [
|
|
"meta",
|
|
"env",
|
|
"wizard",
|
|
"diagnostics",
|
|
"logging",
|
|
"update",
|
|
"browser",
|
|
"ui",
|
|
"auth",
|
|
"models",
|
|
"nodeHost",
|
|
"agents",
|
|
"tools",
|
|
"bindings",
|
|
"broadcast",
|
|
"audio",
|
|
"media",
|
|
"messages",
|
|
"commands",
|
|
"approvals",
|
|
"session",
|
|
"cron",
|
|
"hooks",
|
|
"web",
|
|
"channels",
|
|
"discovery",
|
|
"canvasHost",
|
|
"talk",
|
|
"gateway",
|
|
"memory",
|
|
"plugins",
|
|
] as const;
|
|
|
|
const TARGET_KEYS = [
|
|
"memory.citations",
|
|
"memory.backend",
|
|
"memory.qmd.searchMode",
|
|
"memory.qmd.scope",
|
|
"memory.qmd.includeDefaultMemory",
|
|
"memory.qmd.mcporter.enabled",
|
|
"memory.qmd.mcporter.serverName",
|
|
"memory.qmd.command",
|
|
"memory.qmd.mcporter",
|
|
"memory.qmd.mcporter.startDaemon",
|
|
"memory.qmd.paths",
|
|
"memory.qmd.paths.path",
|
|
"memory.qmd.paths.pattern",
|
|
"memory.qmd.paths.name",
|
|
"memory.qmd.sessions.enabled",
|
|
"memory.qmd.sessions.exportDir",
|
|
"memory.qmd.sessions.retentionDays",
|
|
"memory.qmd.update.interval",
|
|
"memory.qmd.update.debounceMs",
|
|
"memory.qmd.update.onBoot",
|
|
"memory.qmd.update.waitForBootSync",
|
|
"memory.qmd.update.embedInterval",
|
|
"memory.qmd.update.commandTimeoutMs",
|
|
"memory.qmd.update.updateTimeoutMs",
|
|
"memory.qmd.update.embedTimeoutMs",
|
|
"memory.qmd.limits.maxResults",
|
|
"memory.qmd.limits.maxSnippetChars",
|
|
"memory.qmd.limits.maxInjectedChars",
|
|
"memory.qmd.limits.timeoutMs",
|
|
"agents.defaults.memorySearch.provider",
|
|
"agents.defaults.memorySearch.fallback",
|
|
"agents.defaults.memorySearch.sources",
|
|
"agents.defaults.memorySearch.extraPaths",
|
|
"agents.defaults.memorySearch.experimental.sessionMemory",
|
|
"agents.defaults.memorySearch.remote.baseUrl",
|
|
"agents.defaults.memorySearch.remote.apiKey",
|
|
"agents.defaults.memorySearch.remote.headers",
|
|
"agents.defaults.memorySearch.remote.batch.enabled",
|
|
"agents.defaults.memorySearch.remote.batch.wait",
|
|
"agents.defaults.memorySearch.remote.batch.concurrency",
|
|
"agents.defaults.memorySearch.remote.batch.pollIntervalMs",
|
|
"agents.defaults.memorySearch.remote.batch.timeoutMinutes",
|
|
"agents.defaults.memorySearch.local.modelPath",
|
|
"agents.defaults.memorySearch.store.path",
|
|
"agents.defaults.memorySearch.store.vector.enabled",
|
|
"agents.defaults.memorySearch.store.vector.extensionPath",
|
|
"agents.defaults.memorySearch.query.hybrid.enabled",
|
|
"agents.defaults.memorySearch.query.hybrid.vectorWeight",
|
|
"agents.defaults.memorySearch.query.hybrid.textWeight",
|
|
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier",
|
|
"agents.defaults.memorySearch.query.hybrid.mmr.enabled",
|
|
"agents.defaults.memorySearch.query.hybrid.mmr.lambda",
|
|
"agents.defaults.memorySearch.query.hybrid.temporalDecay.enabled",
|
|
"agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays",
|
|
"agents.defaults.memorySearch.cache.enabled",
|
|
"agents.defaults.memorySearch.cache.maxEntries",
|
|
"agents.defaults.memorySearch.sync.onSearch",
|
|
"agents.defaults.memorySearch.sync.watch",
|
|
"agents.defaults.memorySearch.sync.sessions.deltaBytes",
|
|
"agents.defaults.memorySearch.sync.sessions.deltaMessages",
|
|
"models.mode",
|
|
"models.providers.*.auth",
|
|
"models.providers.*.authHeader",
|
|
"gateway.reload.mode",
|
|
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback",
|
|
"gateway.controlUi.allowInsecureAuth",
|
|
"gateway.controlUi.dangerouslyDisableDeviceAuth",
|
|
"cron",
|
|
"cron.enabled",
|
|
"cron.store",
|
|
"cron.maxConcurrentRuns",
|
|
"cron.retry",
|
|
"cron.retry.maxAttempts",
|
|
"cron.retry.backoffMs",
|
|
"cron.retry.retryOn",
|
|
"cron.webhook",
|
|
"cron.webhookToken",
|
|
"cron.sessionRetention",
|
|
"cron.runLog",
|
|
"cron.runLog.maxBytes",
|
|
"cron.runLog.keepLines",
|
|
"session",
|
|
"session.scope",
|
|
"session.dmScope",
|
|
"session.identityLinks",
|
|
"session.resetTriggers",
|
|
"session.idleMinutes",
|
|
"session.reset",
|
|
"session.reset.mode",
|
|
"session.reset.atHour",
|
|
"session.reset.idleMinutes",
|
|
"session.resetByType",
|
|
"session.resetByType.direct",
|
|
"session.resetByType.dm",
|
|
"session.resetByType.group",
|
|
"session.resetByType.thread",
|
|
"session.resetByChannel",
|
|
"session.store",
|
|
"session.typingIntervalSeconds",
|
|
"session.typingMode",
|
|
"session.mainKey",
|
|
"session.sendPolicy",
|
|
"session.sendPolicy.default",
|
|
"session.sendPolicy.rules",
|
|
"session.sendPolicy.rules[].action",
|
|
"session.sendPolicy.rules[].match",
|
|
"session.sendPolicy.rules[].match.channel",
|
|
"session.sendPolicy.rules[].match.chatType",
|
|
"session.sendPolicy.rules[].match.keyPrefix",
|
|
"session.sendPolicy.rules[].match.rawKeyPrefix",
|
|
"session.agentToAgent",
|
|
"session.agentToAgent.maxPingPongTurns",
|
|
"session.threadBindings",
|
|
"session.threadBindings.enabled",
|
|
"session.threadBindings.idleHours",
|
|
"session.threadBindings.maxAgeHours",
|
|
"session.maintenance",
|
|
"session.maintenance.mode",
|
|
"session.maintenance.pruneAfter",
|
|
"session.maintenance.pruneDays",
|
|
"session.maintenance.maxEntries",
|
|
"session.maintenance.rotateBytes",
|
|
"session.maintenance.resetArchiveRetention",
|
|
"session.maintenance.maxDiskBytes",
|
|
"session.maintenance.highWaterBytes",
|
|
"approvals",
|
|
"approvals.exec",
|
|
"approvals.exec.enabled",
|
|
"approvals.exec.mode",
|
|
"approvals.exec.agentFilter",
|
|
"approvals.exec.sessionFilter",
|
|
"approvals.exec.targets",
|
|
"approvals.exec.targets[].channel",
|
|
"approvals.exec.targets[].to",
|
|
"approvals.exec.targets[].accountId",
|
|
"approvals.exec.targets[].threadId",
|
|
"nodeHost",
|
|
"nodeHost.browserProxy",
|
|
"nodeHost.browserProxy.enabled",
|
|
"nodeHost.browserProxy.allowProfiles",
|
|
"media",
|
|
"media.preserveFilenames",
|
|
"audio",
|
|
"audio.transcription",
|
|
"audio.transcription.command",
|
|
"audio.transcription.timeoutSeconds",
|
|
"bindings",
|
|
"bindings[].agentId",
|
|
"bindings[].match",
|
|
"bindings[].match.channel",
|
|
"bindings[].match.accountId",
|
|
"bindings[].match.peer",
|
|
"bindings[].match.peer.kind",
|
|
"bindings[].match.peer.id",
|
|
"bindings[].match.guildId",
|
|
"bindings[].match.teamId",
|
|
"bindings[].match.roles",
|
|
"broadcast",
|
|
"broadcast.strategy",
|
|
"broadcast.*",
|
|
"commands",
|
|
"commands.allowFrom",
|
|
"hooks",
|
|
"hooks.enabled",
|
|
"hooks.path",
|
|
"hooks.token",
|
|
"hooks.defaultSessionKey",
|
|
"hooks.allowRequestSessionKey",
|
|
"hooks.allowedSessionKeyPrefixes",
|
|
"hooks.allowedAgentIds",
|
|
"hooks.maxBodyBytes",
|
|
"hooks.transformsDir",
|
|
"hooks.mappings",
|
|
"hooks.mappings[].action",
|
|
"hooks.mappings[].wakeMode",
|
|
"hooks.mappings[].channel",
|
|
"hooks.mappings[].transform.module",
|
|
"hooks.gmail",
|
|
"hooks.gmail.pushToken",
|
|
"hooks.gmail.tailscale.mode",
|
|
"hooks.gmail.thinking",
|
|
"hooks.internal",
|
|
"hooks.internal.handlers",
|
|
"hooks.internal.handlers[].event",
|
|
"hooks.internal.handlers[].module",
|
|
"hooks.internal.load.extraDirs",
|
|
"messages",
|
|
"messages.messagePrefix",
|
|
"messages.responsePrefix",
|
|
"messages.groupChat",
|
|
"messages.groupChat.mentionPatterns",
|
|
"messages.groupChat.historyLimit",
|
|
"messages.queue",
|
|
"messages.queue.mode",
|
|
"messages.queue.byChannel",
|
|
"messages.queue.debounceMs",
|
|
"messages.queue.debounceMsByChannel",
|
|
"messages.queue.cap",
|
|
"messages.queue.drop",
|
|
"messages.inbound",
|
|
"messages.inbound.byChannel",
|
|
"messages.removeAckAfterReply",
|
|
"messages.tts",
|
|
"channels",
|
|
"channels.defaults",
|
|
"channels.defaults.groupPolicy",
|
|
"channels.defaults.heartbeat",
|
|
"channels.defaults.heartbeat.showOk",
|
|
"channels.defaults.heartbeat.showAlerts",
|
|
"channels.defaults.heartbeat.useIndicator",
|
|
"gateway",
|
|
"gateway.mode",
|
|
"gateway.bind",
|
|
"gateway.auth.mode",
|
|
"gateway.tailscale.mode",
|
|
"gateway.tools.allow",
|
|
"gateway.tools.deny",
|
|
"gateway.tls.enabled",
|
|
"gateway.tls.autoGenerate",
|
|
"gateway.http",
|
|
"gateway.http.endpoints",
|
|
"browser",
|
|
"browser.enabled",
|
|
"browser.cdpUrl",
|
|
"browser.headless",
|
|
"browser.noSandbox",
|
|
"browser.profiles",
|
|
"browser.profiles.*.driver",
|
|
"browser.profiles.*.attachOnly",
|
|
"tools",
|
|
"tools.allow",
|
|
"tools.deny",
|
|
"tools.exec",
|
|
"tools.exec.host",
|
|
"tools.exec.security",
|
|
"tools.exec.ask",
|
|
"tools.exec.node",
|
|
"tools.agentToAgent.enabled",
|
|
"tools.elevated.enabled",
|
|
"tools.elevated.allowFrom",
|
|
"tools.subagents.tools",
|
|
"tools.sandbox.tools",
|
|
"web",
|
|
"web.enabled",
|
|
"web.heartbeatSeconds",
|
|
"web.reconnect",
|
|
"web.reconnect.initialMs",
|
|
"web.reconnect.maxMs",
|
|
"web.reconnect.factor",
|
|
"web.reconnect.jitter",
|
|
"web.reconnect.maxAttempts",
|
|
"discovery",
|
|
"discovery.wideArea.enabled",
|
|
"discovery.mdns",
|
|
"discovery.mdns.mode",
|
|
"canvasHost",
|
|
"canvasHost.enabled",
|
|
"canvasHost.root",
|
|
"canvasHost.port",
|
|
"canvasHost.liveReload",
|
|
"talk",
|
|
"talk.voiceId",
|
|
"talk.voiceAliases",
|
|
"talk.modelId",
|
|
"talk.outputFormat",
|
|
"talk.interruptOnSpeech",
|
|
"meta",
|
|
"env",
|
|
"env.shellEnv",
|
|
"env.shellEnv.enabled",
|
|
"env.shellEnv.timeoutMs",
|
|
"env.vars",
|
|
"wizard",
|
|
"wizard.lastRunAt",
|
|
"wizard.lastRunVersion",
|
|
"wizard.lastRunCommit",
|
|
"wizard.lastRunCommand",
|
|
"wizard.lastRunMode",
|
|
"diagnostics",
|
|
"diagnostics.otel",
|
|
"diagnostics.cacheTrace",
|
|
"logging",
|
|
"logging.level",
|
|
"logging.file",
|
|
"logging.consoleLevel",
|
|
"logging.consoleStyle",
|
|
"logging.redactSensitive",
|
|
"logging.redactPatterns",
|
|
"update",
|
|
"ui",
|
|
"ui.assistant",
|
|
"plugins",
|
|
"plugins.enabled",
|
|
"plugins.allow",
|
|
"plugins.deny",
|
|
"plugins.load",
|
|
"plugins.load.paths",
|
|
"plugins.slots",
|
|
"plugins.entries",
|
|
"plugins.entries.*.enabled",
|
|
"plugins.entries.*.apiKey",
|
|
"plugins.entries.*.env",
|
|
"plugins.entries.*.config",
|
|
"plugins.installs",
|
|
"auth",
|
|
"auth.cooldowns",
|
|
"models",
|
|
"models.providers",
|
|
"models.providers.*.baseUrl",
|
|
"models.providers.*.apiKey",
|
|
"models.providers.*.api",
|
|
"models.providers.*.headers",
|
|
"models.providers.*.models",
|
|
"models.bedrockDiscovery",
|
|
"models.bedrockDiscovery.enabled",
|
|
"models.bedrockDiscovery.region",
|
|
"models.bedrockDiscovery.providerFilter",
|
|
"models.bedrockDiscovery.refreshInterval",
|
|
"models.bedrockDiscovery.defaultContextWindow",
|
|
"models.bedrockDiscovery.defaultMaxTokens",
|
|
"agents",
|
|
"agents.defaults",
|
|
"agents.list",
|
|
"agents.defaults.compaction",
|
|
"agents.defaults.compaction.mode",
|
|
"agents.defaults.compaction.reserveTokens",
|
|
"agents.defaults.compaction.keepRecentTokens",
|
|
"agents.defaults.compaction.reserveTokensFloor",
|
|
"agents.defaults.compaction.maxHistoryShare",
|
|
"agents.defaults.compaction.identifierPolicy",
|
|
"agents.defaults.compaction.identifierInstructions",
|
|
"agents.defaults.compaction.memoryFlush",
|
|
"agents.defaults.compaction.memoryFlush.enabled",
|
|
"agents.defaults.compaction.memoryFlush.softThresholdTokens",
|
|
"agents.defaults.compaction.memoryFlush.prompt",
|
|
"agents.defaults.compaction.memoryFlush.systemPrompt",
|
|
] as const;
|
|
|
|
const ENUM_EXPECTATIONS: Record<string, string[]> = {
|
|
"memory.citations": ['"auto"', '"on"', '"off"'],
|
|
"memory.backend": ['"builtin"', '"qmd"'],
|
|
"memory.qmd.searchMode": ['"query"', '"search"', '"vsearch"'],
|
|
"models.mode": ['"merge"', '"replace"'],
|
|
"models.providers.*.auth": ['"api-key"', '"token"', '"oauth"', '"aws-sdk"'],
|
|
"gateway.reload.mode": ['"off"', '"restart"', '"hot"', '"hybrid"'],
|
|
"approvals.exec.mode": ['"session"', '"targets"', '"both"'],
|
|
"bindings[].match.peer.kind": ['"direct"', '"group"', '"channel"', '"dm"'],
|
|
"broadcast.strategy": ['"parallel"', '"sequential"'],
|
|
"hooks.mappings[].action": ['"wake"', '"agent"'],
|
|
"hooks.mappings[].wakeMode": ['"now"', '"next-heartbeat"'],
|
|
"hooks.gmail.tailscale.mode": ['"off"', '"serve"', '"funnel"'],
|
|
"hooks.gmail.thinking": ['"off"', '"minimal"', '"low"', '"medium"', '"high"'],
|
|
"messages.queue.mode": [
|
|
'"steer"',
|
|
'"followup"',
|
|
'"collect"',
|
|
'"steer-backlog"',
|
|
'"steer+backlog"',
|
|
'"queue"',
|
|
'"interrupt"',
|
|
],
|
|
"messages.queue.drop": ['"old"', '"new"', '"summarize"'],
|
|
"channels.defaults.groupPolicy": ['"open"', '"disabled"', '"allowlist"'],
|
|
"gateway.mode": ['"local"', '"remote"'],
|
|
"gateway.bind": ['"auto"', '"lan"', '"loopback"', '"custom"', '"tailnet"'],
|
|
"gateway.auth.mode": ['"none"', '"token"', '"password"', '"trusted-proxy"'],
|
|
"gateway.tailscale.mode": ['"off"', '"serve"', '"funnel"'],
|
|
"browser.profiles.*.driver": ['"clawd"', '"extension"'],
|
|
"discovery.mdns.mode": ['"off"', '"minimal"', '"full"'],
|
|
"wizard.lastRunMode": ['"local"', '"remote"'],
|
|
"diagnostics.otel.protocol": ['"http/protobuf"', '"grpc"'],
|
|
"logging.level": ['"silent"', '"fatal"', '"error"', '"warn"', '"info"', '"debug"', '"trace"'],
|
|
"logging.consoleLevel": [
|
|
'"silent"',
|
|
'"fatal"',
|
|
'"error"',
|
|
'"warn"',
|
|
'"info"',
|
|
'"debug"',
|
|
'"trace"',
|
|
],
|
|
"logging.consoleStyle": ['"pretty"', '"compact"', '"json"'],
|
|
"logging.redactSensitive": ['"off"', '"tools"'],
|
|
"update.channel": ['"stable"', '"beta"', '"dev"'],
|
|
"agents.defaults.compaction.mode": ['"default"', '"safeguard"'],
|
|
"agents.defaults.compaction.identifierPolicy": ['"strict"', '"off"', '"custom"'],
|
|
};
|
|
|
|
const TOOLS_HOOKS_TARGET_KEYS = [
|
|
"hooks.gmail.account",
|
|
"hooks.gmail.allowUnsafeExternalContent",
|
|
"hooks.gmail.hookUrl",
|
|
"hooks.gmail.includeBody",
|
|
"hooks.gmail.label",
|
|
"hooks.gmail.model",
|
|
"hooks.gmail.serve",
|
|
"hooks.gmail.subscription",
|
|
"hooks.gmail.tailscale",
|
|
"hooks.gmail.topic",
|
|
"hooks.internal.entries",
|
|
"hooks.internal.installs",
|
|
"hooks.internal.load",
|
|
"hooks.mappings[].allowUnsafeExternalContent",
|
|
"hooks.mappings[].deliver",
|
|
"hooks.mappings[].id",
|
|
"hooks.mappings[].match",
|
|
"hooks.mappings[].messageTemplate",
|
|
"hooks.mappings[].model",
|
|
"hooks.mappings[].name",
|
|
"hooks.mappings[].textTemplate",
|
|
"hooks.mappings[].thinking",
|
|
"hooks.mappings[].transform",
|
|
"tools.alsoAllow",
|
|
"tools.byProvider",
|
|
"tools.exec.approvalRunningNoticeMs",
|
|
"tools.links.enabled",
|
|
"tools.links.maxLinks",
|
|
"tools.links.models",
|
|
"tools.links.scope",
|
|
"tools.links.timeoutSeconds",
|
|
"tools.media.audio.attachments",
|
|
"tools.media.audio.enabled",
|
|
"tools.media.audio.language",
|
|
"tools.media.audio.maxBytes",
|
|
"tools.media.audio.maxChars",
|
|
"tools.media.audio.models",
|
|
"tools.media.audio.prompt",
|
|
"tools.media.audio.scope",
|
|
"tools.media.audio.timeoutSeconds",
|
|
"tools.media.concurrency",
|
|
"tools.media.image.attachments",
|
|
"tools.media.image.enabled",
|
|
"tools.media.image.maxBytes",
|
|
"tools.media.image.maxChars",
|
|
"tools.media.image.models",
|
|
"tools.media.image.prompt",
|
|
"tools.media.image.scope",
|
|
"tools.media.image.timeoutSeconds",
|
|
"tools.media.models",
|
|
"tools.media.video.attachments",
|
|
"tools.media.video.enabled",
|
|
"tools.media.video.maxBytes",
|
|
"tools.media.video.maxChars",
|
|
"tools.media.video.models",
|
|
"tools.media.video.prompt",
|
|
"tools.media.video.scope",
|
|
"tools.media.video.timeoutSeconds",
|
|
"tools.profile",
|
|
] as const;
|
|
|
|
const CHANNELS_AGENTS_TARGET_KEYS = [
|
|
"agents.defaults.memorySearch.chunking.overlap",
|
|
"agents.defaults.memorySearch.chunking.tokens",
|
|
"agents.defaults.memorySearch.enabled",
|
|
"agents.defaults.memorySearch.model",
|
|
"agents.defaults.memorySearch.query.maxResults",
|
|
"agents.defaults.memorySearch.query.minScore",
|
|
"agents.defaults.memorySearch.sync.onSessionStart",
|
|
"agents.defaults.memorySearch.sync.watchDebounceMs",
|
|
"agents.defaults.workspace",
|
|
"agents.list[].tools.alsoAllow",
|
|
"agents.list[].tools.byProvider",
|
|
"agents.list[].tools.profile",
|
|
"channels.bluebubbles",
|
|
"channels.discord",
|
|
"channels.discord.token",
|
|
"channels.imessage",
|
|
"channels.imessage.cliPath",
|
|
"channels.irc",
|
|
"channels.mattermost",
|
|
"channels.msteams",
|
|
"channels.signal",
|
|
"channels.signal.account",
|
|
"channels.slack",
|
|
"channels.slack.appToken",
|
|
"channels.slack.botToken",
|
|
"channels.slack.userToken",
|
|
"channels.slack.userTokenReadOnly",
|
|
"channels.telegram",
|
|
"channels.telegram.botToken",
|
|
"channels.telegram.capabilities.inlineButtons",
|
|
"channels.whatsapp",
|
|
] as const;
|
|
|
|
const FINAL_BACKLOG_TARGET_KEYS = [
|
|
"browser.evaluateEnabled",
|
|
"browser.remoteCdpHandshakeTimeoutMs",
|
|
"browser.remoteCdpTimeoutMs",
|
|
"browser.snapshotDefaults",
|
|
"browser.snapshotDefaults.mode",
|
|
"browser.ssrfPolicy",
|
|
"browser.ssrfPolicy.allowPrivateNetwork",
|
|
"browser.ssrfPolicy.dangerouslyAllowPrivateNetwork",
|
|
"browser.ssrfPolicy.allowedHostnames",
|
|
"browser.ssrfPolicy.hostnameAllowlist",
|
|
"diagnostics.enabled",
|
|
"diagnostics.otel.enabled",
|
|
"diagnostics.otel.endpoint",
|
|
"diagnostics.otel.flushIntervalMs",
|
|
"diagnostics.otel.headers",
|
|
"diagnostics.otel.logs",
|
|
"diagnostics.otel.metrics",
|
|
"diagnostics.otel.sampleRate",
|
|
"diagnostics.otel.serviceName",
|
|
"diagnostics.otel.traces",
|
|
"gateway.remote.password",
|
|
"gateway.remote.token",
|
|
"skills.load.watch",
|
|
"skills.load.watchDebounceMs",
|
|
"talk.apiKey",
|
|
"ui.assistant.avatar",
|
|
"ui.assistant.name",
|
|
"ui.seamColor",
|
|
] as const;
|
|
|
|
describe("config help copy quality", () => {
|
|
function expectOperationalGuidance(
|
|
keys: readonly string[],
|
|
guidancePattern: RegExp,
|
|
minLength = 80,
|
|
) {
|
|
for (const key of keys) {
|
|
const help = FIELD_HELP[key];
|
|
expect(help, `missing help for ${key}`).toBeDefined();
|
|
expect(help.length, `help too short for ${key}`).toBeGreaterThanOrEqual(minLength);
|
|
expect(
|
|
guidancePattern.test(help),
|
|
`help should include operational guidance for ${key}`,
|
|
).toBe(true);
|
|
}
|
|
}
|
|
|
|
it("keeps root section labels and help complete", () => {
|
|
for (const key of ROOT_SECTIONS) {
|
|
expect(FIELD_LABELS[key], `missing root label for ${key}`).toBeDefined();
|
|
expect(FIELD_HELP[key], `missing root help for ${key}`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("keeps labels in parity for all help keys", () => {
|
|
for (const key of Object.keys(FIELD_HELP)) {
|
|
expect(FIELD_LABELS[key], `missing label for help key ${key}`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("covers the target confusing fields with non-trivial explanations", () => {
|
|
expectOperationalGuidance(
|
|
TARGET_KEYS,
|
|
/(default|keep|use|enable|disable|controls|selects|sets|defines)/i,
|
|
);
|
|
});
|
|
|
|
it("covers tools/hooks help keys with non-trivial operational guidance", () => {
|
|
expectOperationalGuidance(
|
|
TOOLS_HOOKS_TARGET_KEYS,
|
|
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i,
|
|
);
|
|
});
|
|
|
|
it("covers channels/agents help keys with non-trivial operational guidance", () => {
|
|
expectOperationalGuidance(
|
|
CHANNELS_AGENTS_TARGET_KEYS,
|
|
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i,
|
|
);
|
|
});
|
|
|
|
it("covers final backlog help keys with non-trivial operational guidance", () => {
|
|
expectOperationalGuidance(
|
|
FINAL_BACKLOG_TARGET_KEYS,
|
|
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i,
|
|
);
|
|
});
|
|
|
|
it("documents option behavior for enum-style fields", () => {
|
|
for (const [key, options] of Object.entries(ENUM_EXPECTATIONS)) {
|
|
const help = FIELD_HELP[key];
|
|
expect(help, `missing help for enum key ${key}`).toBeDefined();
|
|
for (const token of options) {
|
|
expect(help.includes(token), `missing option ${token} in ${key}`).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("explains memory citations mode semantics", () => {
|
|
const help = FIELD_HELP["memory.citations"];
|
|
expect(help.includes('"auto"')).toBe(true);
|
|
expect(help.includes('"on"')).toBe(true);
|
|
expect(help.includes('"off"')).toBe(true);
|
|
expect(/always|always shows/i.test(help)).toBe(true);
|
|
expect(/hides|hide/i.test(help)).toBe(true);
|
|
});
|
|
|
|
it("includes concrete examples on path and interval fields", () => {
|
|
expect(FIELD_HELP["memory.qmd.paths.pattern"].includes("**/*.md")).toBe(true);
|
|
expect(FIELD_HELP["memory.qmd.update.interval"].includes("5m")).toBe(true);
|
|
expect(FIELD_HELP["memory.qmd.update.embedInterval"].includes("60m")).toBe(true);
|
|
expect(FIELD_HELP["agents.defaults.memorySearch.store.path"]).toContain(
|
|
"~/.openclaw/memory/{agentId}.sqlite",
|
|
);
|
|
});
|
|
|
|
it("documents cron deprecation, migration, and retention formats", () => {
|
|
const legacy = FIELD_HELP["cron.webhook"];
|
|
expect(/deprecated|legacy/i.test(legacy)).toBe(true);
|
|
expect(legacy.includes('delivery.mode="webhook"')).toBe(true);
|
|
expect(legacy.includes("delivery.to")).toBe(true);
|
|
|
|
const retention = FIELD_HELP["cron.sessionRetention"];
|
|
expect(retention.includes("24h")).toBe(true);
|
|
expect(retention.includes("7d")).toBe(true);
|
|
expect(retention.includes("1h30m")).toBe(true);
|
|
expect(/false/i.test(retention)).toBe(true);
|
|
|
|
const token = FIELD_HELP["cron.webhookToken"];
|
|
expect(/token|bearer/i.test(token)).toBe(true);
|
|
expect(/secret|env|rotate/i.test(token)).toBe(true);
|
|
});
|
|
|
|
it("documents session send-policy examples and prefix semantics", () => {
|
|
const rules = FIELD_HELP["session.sendPolicy.rules"];
|
|
expect(rules.includes("{ action:")).toBe(true);
|
|
expect(rules.includes('"deny"')).toBe(true);
|
|
expect(rules.includes('"discord"')).toBe(true);
|
|
|
|
const keyPrefix = FIELD_HELP["session.sendPolicy.rules[].match.keyPrefix"];
|
|
expect(/normalized/i.test(keyPrefix)).toBe(true);
|
|
|
|
const rawKeyPrefix = FIELD_HELP["session.sendPolicy.rules[].match.rawKeyPrefix"];
|
|
expect(/raw|unnormalized/i.test(rawKeyPrefix)).toBe(true);
|
|
});
|
|
|
|
it("documents session maintenance duration/size examples and deprecations", () => {
|
|
const pruneAfter = FIELD_HELP["session.maintenance.pruneAfter"];
|
|
expect(pruneAfter.includes("30d")).toBe(true);
|
|
expect(pruneAfter.includes("12h")).toBe(true);
|
|
|
|
const rotate = FIELD_HELP["session.maintenance.rotateBytes"];
|
|
expect(rotate.includes("10mb")).toBe(true);
|
|
expect(rotate.includes("1gb")).toBe(true);
|
|
|
|
const deprecated = FIELD_HELP["session.maintenance.pruneDays"];
|
|
expect(/deprecated/i.test(deprecated)).toBe(true);
|
|
expect(deprecated.includes("session.maintenance.pruneAfter")).toBe(true);
|
|
|
|
const resetRetention = FIELD_HELP["session.maintenance.resetArchiveRetention"];
|
|
expect(resetRetention.includes(".reset.")).toBe(true);
|
|
expect(/false/i.test(resetRetention)).toBe(true);
|
|
|
|
const maxDisk = FIELD_HELP["session.maintenance.maxDiskBytes"];
|
|
expect(maxDisk.includes("500mb")).toBe(true);
|
|
|
|
const highWater = FIELD_HELP["session.maintenance.highWaterBytes"];
|
|
expect(highWater.includes("80%")).toBe(true);
|
|
});
|
|
|
|
it("documents cron run-log retention controls", () => {
|
|
const runLog = FIELD_HELP["cron.runLog"];
|
|
expect(runLog.includes("cron/runs")).toBe(true);
|
|
|
|
const maxBytes = FIELD_HELP["cron.runLog.maxBytes"];
|
|
expect(maxBytes.includes("2mb")).toBe(true);
|
|
|
|
const keepLines = FIELD_HELP["cron.runLog.keepLines"];
|
|
expect(keepLines.includes("2000")).toBe(true);
|
|
});
|
|
|
|
it("documents approvals filters and target semantics", () => {
|
|
const sessionFilter = FIELD_HELP["approvals.exec.sessionFilter"];
|
|
expect(/substring|regex/i.test(sessionFilter)).toBe(true);
|
|
expect(sessionFilter.includes("discord:")).toBe(true);
|
|
expect(sessionFilter.includes("^agent:ops:")).toBe(true);
|
|
|
|
const agentFilter = FIELD_HELP["approvals.exec.agentFilter"];
|
|
expect(agentFilter.includes("primary")).toBe(true);
|
|
expect(agentFilter.includes("ops-agent")).toBe(true);
|
|
|
|
const targetTo = FIELD_HELP["approvals.exec.targets[].to"];
|
|
expect(/channel ID|user ID|thread root/i.test(targetTo)).toBe(true);
|
|
expect(/differs|per provider/i.test(targetTo)).toBe(true);
|
|
});
|
|
|
|
it("documents broadcast and audio command examples", () => {
|
|
const audioCmd = FIELD_HELP["audio.transcription.command"];
|
|
expect(audioCmd.includes("whisper-cli")).toBe(true);
|
|
expect(audioCmd.includes("{input}")).toBe(true);
|
|
|
|
const broadcastMap = FIELD_HELP["broadcast.*"];
|
|
expect(/source peer ID/i.test(broadcastMap)).toBe(true);
|
|
expect(/destination peer IDs/i.test(broadcastMap)).toBe(true);
|
|
});
|
|
|
|
it("documents hook transform safety and queue behavior options", () => {
|
|
const transformModule = FIELD_HELP["hooks.mappings[].transform.module"];
|
|
expect(/relative/i.test(transformModule)).toBe(true);
|
|
expect(/path traversal|reviewed|controlled/i.test(transformModule)).toBe(true);
|
|
|
|
const queueMode = FIELD_HELP["messages.queue.mode"];
|
|
expect(queueMode.includes('"interrupt"')).toBe(true);
|
|
expect(queueMode.includes('"steer+backlog"')).toBe(true);
|
|
});
|
|
|
|
it("documents gateway bind modes and web reconnect semantics", () => {
|
|
const bind = FIELD_HELP["gateway.bind"];
|
|
expect(bind.includes('"loopback"')).toBe(true);
|
|
expect(bind.includes('"tailnet"')).toBe(true);
|
|
|
|
const reconnect = FIELD_HELP["web.reconnect.maxAttempts"];
|
|
expect(/0 means no retries|no retries/i.test(reconnect)).toBe(true);
|
|
expect(/failure sequence|retry/i.test(reconnect)).toBe(true);
|
|
});
|
|
|
|
it("documents metadata/admin semantics for logging, wizard, and plugins", () => {
|
|
const wizardMode = FIELD_HELP["wizard.lastRunMode"];
|
|
expect(wizardMode.includes('"local"')).toBe(true);
|
|
expect(wizardMode.includes('"remote"')).toBe(true);
|
|
|
|
const consoleStyle = FIELD_HELP["logging.consoleStyle"];
|
|
expect(consoleStyle.includes('"pretty"')).toBe(true);
|
|
expect(consoleStyle.includes('"compact"')).toBe(true);
|
|
expect(consoleStyle.includes('"json"')).toBe(true);
|
|
|
|
const pluginApiKey = FIELD_HELP["plugins.entries.*.apiKey"];
|
|
expect(/secret|env|credential/i.test(pluginApiKey)).toBe(true);
|
|
|
|
const pluginEnv = FIELD_HELP["plugins.entries.*.env"];
|
|
expect(/scope|plugin|environment/i.test(pluginEnv)).toBe(true);
|
|
});
|
|
|
|
it("documents auth/model root semantics and provider secret handling", () => {
|
|
const providerKey = FIELD_HELP["models.providers.*.apiKey"];
|
|
expect(/secret|env|credential/i.test(providerKey)).toBe(true);
|
|
|
|
const bedrockRefresh = FIELD_HELP["models.bedrockDiscovery.refreshInterval"];
|
|
expect(/refresh|seconds|interval/i.test(bedrockRefresh)).toBe(true);
|
|
expect(/cost|noise|api/i.test(bedrockRefresh)).toBe(true);
|
|
|
|
const authCooldowns = FIELD_HELP["auth.cooldowns"];
|
|
expect(/cooldown|backoff|retry/i.test(authCooldowns)).toBe(true);
|
|
});
|
|
|
|
it("documents agent compaction safeguards and memory flush behavior", () => {
|
|
const mode = FIELD_HELP["agents.defaults.compaction.mode"];
|
|
expect(mode.includes('"default"')).toBe(true);
|
|
expect(mode.includes('"safeguard"')).toBe(true);
|
|
|
|
const historyShare = FIELD_HELP["agents.defaults.compaction.maxHistoryShare"];
|
|
expect(/0\\.1-0\\.9|fraction|share/i.test(historyShare)).toBe(true);
|
|
|
|
const identifierPolicy = FIELD_HELP["agents.defaults.compaction.identifierPolicy"];
|
|
expect(identifierPolicy.includes('"strict"')).toBe(true);
|
|
expect(identifierPolicy.includes('"off"')).toBe(true);
|
|
expect(identifierPolicy.includes('"custom"')).toBe(true);
|
|
|
|
const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"];
|
|
expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true);
|
|
});
|
|
});
|