mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:00:47 +00:00
fix(discord): handle SecretRef runtime status (#76987)
* fix(discord): handle SecretRef runtime status * docs(changelog): mention Discord SecretRef fix
This commit is contained in:
@@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/WebChat: collapse duplicate in-flight internal text sends onto the active Gateway run so rapid repeat submits do not start fresh `agent:main:main` dispatches. Fixes #75737. Thanks @dsdsddd1 and @BunsDev.
|
||||
- Mattermost: accept the documented `channels.mattermost.streaming` config and honor `streaming: "off"` by disabling draft preview posts. Thanks @vincentkoc.
|
||||
- Discord: keep progress draft boundary callbacks bound during streaming replies, so extension lint stays green while progress previews transition between assistant and reasoning blocks. Thanks @vincentkoc.
|
||||
- Discord: resolve SecretRef-backed bot tokens from the active runtime snapshot for named accounts and keep unresolved configured tokens from crashing status or health checks. (#76987) Thanks @joshavant.
|
||||
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
|
||||
- Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc.
|
||||
- Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79.
|
||||
|
||||
@@ -5,11 +5,8 @@ export {
|
||||
handleDiscordSubagentEnded,
|
||||
handleDiscordSubagentSpawning,
|
||||
} from "./src/subagent-hooks.js";
|
||||
export {
|
||||
type DiscordCredentialStatus,
|
||||
inspectDiscordAccount,
|
||||
type InspectedDiscordAccount,
|
||||
} from "./src/account-inspect.js";
|
||||
export { inspectDiscordAccount, type InspectedDiscordAccount } from "./src/account-inspect.js";
|
||||
export { type DiscordCredentialStatus } from "./src/token.js";
|
||||
export {
|
||||
createDiscordActionGate,
|
||||
listDiscordAccountIds,
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
resolveDiscordAccountConfig,
|
||||
} from "./accounts.js";
|
||||
import type { DiscordAccountConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
|
||||
import type { DiscordCredentialStatus } from "./token.js";
|
||||
|
||||
export type InspectedDiscordAccount = {
|
||||
accountId: string;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createDiscordActionGate,
|
||||
@@ -9,6 +14,7 @@ import {
|
||||
} from "./accounts.js";
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
@@ -245,3 +251,60 @@ describe("Discord duplicate-token account filtering", () => {
|
||||
expect(listEnabledDiscordAccounts(cfg).map((account) => account.accountId)).toEqual(["active"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiscordAccount runtime config selection", () => {
|
||||
it("resolves named account SecretRefs from the active runtime snapshot", () => {
|
||||
const sourceCfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
name: "Work",
|
||||
token: { source: "env", provider: "default", id: "DISCORD_WORK_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const runtimeCfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
name: "Work",
|
||||
token: "Bot runtime-work-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
setRuntimeConfigSnapshot(runtimeCfg, sourceCfg);
|
||||
|
||||
const resolved = resolveDiscordAccount({ cfg: sourceCfg });
|
||||
|
||||
expect(resolved.accountId).toBe("work");
|
||||
expect(resolved.token).toBe("runtime-work-token");
|
||||
expect(resolved.tokenSource).toBe("config");
|
||||
expect(resolved.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("preserves configured unavailable tokens without falling through to env", () => {
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "env-token");
|
||||
const resolved = resolveDiscordAccount({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(resolved.token).toBe("");
|
||||
expect(resolved.tokenSource).toBe("config");
|
||||
expect(resolved.tokenStatus).toBe("configured_unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { DiscordAccountConfig, DiscordActionConfig, OpenClawConfig } from "./runtime-api.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
import { selectDiscordRuntimeConfig } from "./runtime-config.js";
|
||||
import { resolveDiscordToken, type DiscordCredentialStatus } from "./token.js";
|
||||
|
||||
export type ResolvedDiscordAccount = {
|
||||
accountId: string;
|
||||
@@ -22,6 +23,7 @@ export type ResolvedDiscordAccount = {
|
||||
name?: string;
|
||||
token: string;
|
||||
tokenSource: "env" | "config" | "none";
|
||||
tokenStatus: DiscordCredentialStatus;
|
||||
config: DiscordAccountConfig;
|
||||
};
|
||||
|
||||
@@ -100,20 +102,20 @@ export function resolveDiscordAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedDiscordAccount {
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultDiscordAccountId(params.cfg),
|
||||
);
|
||||
const baseEnabled = params.cfg.channels?.discord?.enabled !== false;
|
||||
const merged = mergeDiscordAccountConfig(params.cfg, accountId);
|
||||
const cfg = selectDiscordRuntimeConfig(params.cfg);
|
||||
const accountId = normalizeAccountId(params.accountId ?? resolveDefaultDiscordAccountId(cfg));
|
||||
const baseEnabled = cfg.channels?.discord?.enabled !== false;
|
||||
const merged = mergeDiscordAccountConfig(cfg, accountId);
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
const tokenResolution = resolveDiscordToken(params.cfg, { accountId });
|
||||
const tokenResolution = resolveDiscordToken(cfg, { accountId });
|
||||
return {
|
||||
accountId,
|
||||
enabled,
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: tokenResolution.token,
|
||||
tokenSource: tokenResolution.source,
|
||||
tokenStatus: tokenResolution.tokenStatus,
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDiscordRestClient } from "./client.js";
|
||||
import type { RequestClient } from "./internal/discord.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("createDiscordRestClient", () => {
|
||||
const fakeRest = {} as RequestClient;
|
||||
|
||||
@@ -58,7 +62,8 @@ describe("createDiscordRestClient", () => {
|
||||
expect(result.account.config.retry).toMatchObject({ attempts: 7 });
|
||||
});
|
||||
|
||||
it("still throws when no explicit token is provided and config token is unresolved", () => {
|
||||
it("still fails closed when no explicit token is provided and config token is unresolved", () => {
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "env-token");
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -71,6 +76,8 @@ describe("createDiscordRestClient", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(/unresolved SecretRef/i);
|
||||
expect(() => createDiscordRestClient({ cfg, rest: fakeRest })).toThrow(
|
||||
/configured for account "default" is unavailable/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,9 +51,18 @@ export function resolveDiscordClientAccountContext(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveToken(params: { accountId: string; fallbackToken?: string }) {
|
||||
function resolveToken(params: {
|
||||
account: ResolvedDiscordAccount;
|
||||
accountId: string;
|
||||
fallbackToken?: string;
|
||||
}) {
|
||||
const fallback = normalizeDiscordToken(params.fallbackToken, "channels.discord.token");
|
||||
if (!fallback) {
|
||||
if (params.account.tokenStatus === "configured_unavailable") {
|
||||
throw new Error(
|
||||
`Discord bot token configured for account "${params.accountId}" is unavailable; resolve SecretRefs against the active runtime snapshot before using this account.`,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Discord bot token missing for account "${params.accountId}" (set discord.accounts.${params.accountId}.token or DISCORD_BOT_TOKEN for default).`,
|
||||
);
|
||||
@@ -92,6 +101,7 @@ function resolveAccountWithoutToken(params: {
|
||||
name: normalizeOptionalString(merged.name),
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
tokenStatus: "missing",
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
@@ -106,6 +116,7 @@ export function createDiscordRestClient(opts: DiscordClientOpts) {
|
||||
const token =
|
||||
explicitToken ??
|
||||
resolveToken({
|
||||
account,
|
||||
accountId: account.accountId,
|
||||
fallbackToken: account.token,
|
||||
});
|
||||
|
||||
16
extensions/discord/src/runtime-config.ts
Normal file
16
extensions/discord/src/runtime-config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
selectApplicableRuntimeConfig,
|
||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
export function selectDiscordRuntimeConfig(inputConfig: OpenClawConfig): OpenClawConfig {
|
||||
return (
|
||||
selectApplicableRuntimeConfig({
|
||||
inputConfig,
|
||||
runtimeConfig: getRuntimeConfigSnapshot(),
|
||||
runtimeSourceConfig: getRuntimeConfigSourceSnapshot(),
|
||||
}) ?? inputConfig
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ function createAccount(
|
||||
enabled: true,
|
||||
token: "t",
|
||||
tokenSource: "config",
|
||||
tokenStatus: "available",
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +62,27 @@ describe("createDiscordPluginBase", () => {
|
||||
);
|
||||
expect(plugin.config.isEnabled?.(workAccount, cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it("describes unresolved SecretRef tokens without marking them startup-configured", () => {
|
||||
const plugin = createDiscordPluginBase({ setup: {} as never });
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const account = plugin.config.resolveAccount(cfg, "default");
|
||||
const described = plugin.config.describeAccount?.(account, cfg);
|
||||
|
||||
expect(account.token).toBe("");
|
||||
expect(account.tokenSource).toBe("config");
|
||||
expect(account.tokenStatus).toBe("configured_unavailable");
|
||||
expect(plugin.config.isConfigured?.(account, cfg)).toBe(false);
|
||||
expect(described?.configured).toBe(false);
|
||||
expect(described?.tokenStatus).toBe("configured_unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("discordConfigAdapter", () => {
|
||||
|
||||
@@ -156,6 +156,7 @@ export function createDiscordPluginBase(params: {
|
||||
configured: Boolean(account.token?.trim()),
|
||||
extra: {
|
||||
tokenSource: account.tokenSource,
|
||||
tokenStatus: account.tokenStatus,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshot,
|
||||
} from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
describe("resolveDiscordToken", () => {
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
@@ -15,6 +20,7 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg);
|
||||
expect(res.token).toBe("cfg-token");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("uses env token when config is missing", () => {
|
||||
@@ -25,6 +31,7 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg);
|
||||
expect(res.token).toBe("env-token");
|
||||
expect(res.source).toBe("env");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("prefers account token for non-default accounts", () => {
|
||||
@@ -42,6 +49,7 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg, { accountId: "work" });
|
||||
expect(res.token).toBe("acct-token");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("falls back to top-level token for non-default accounts without account token", () => {
|
||||
@@ -58,6 +66,7 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg, { accountId: "work" });
|
||||
expect(res.token).toBe("base-token");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("does not inherit top-level token when account token is explicitly blank", () => {
|
||||
@@ -74,6 +83,7 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg, { accountId: "work" });
|
||||
expect(res.token).toBe("");
|
||||
expect(res.source).toBe("none");
|
||||
expect(res.tokenStatus).toBe("missing");
|
||||
});
|
||||
|
||||
it("resolves account token when account key casing differs from normalized id", () => {
|
||||
@@ -89,9 +99,43 @@ describe("resolveDiscordToken", () => {
|
||||
const res = resolveDiscordToken(cfg, { accountId: "work" });
|
||||
expect(res.token).toBe("acct-token");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("throws when token is an unresolved SecretRef object", () => {
|
||||
it("uses the active runtime snapshot when resolving a matching source config", () => {
|
||||
const sourceCfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
work: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_WORK_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const runtimeCfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
work: {
|
||||
token: "Bot runtime-work-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
setRuntimeConfigSnapshot(runtimeCfg, sourceCfg);
|
||||
|
||||
const res = resolveDiscordToken(sourceCfg, { accountId: "work" });
|
||||
|
||||
expect(res.token).toBe("runtime-work-token");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("available");
|
||||
});
|
||||
|
||||
it("treats unresolved top-level SecretRefs as configured unavailable without env fallback", () => {
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "env-token");
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
@@ -100,8 +144,31 @@ describe("resolveDiscordToken", () => {
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(() => resolveDiscordToken(cfg)).toThrow(
|
||||
/channels\.discord\.token: unresolved SecretRef/i,
|
||||
);
|
||||
const res = resolveDiscordToken(cfg);
|
||||
|
||||
expect(res.token).toBe("");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("configured_unavailable");
|
||||
});
|
||||
|
||||
it("treats unresolved account SecretRefs as configured unavailable without top-level fallback", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "base-token",
|
||||
accounts: {
|
||||
work: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_WORK_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const res = resolveDiscordToken(cfg, { accountId: "work" });
|
||||
|
||||
expect(res.token).toBe("");
|
||||
expect(res.source).toBe("config");
|
||||
expect(res.tokenStatus).toBe("configured_unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,50 +2,97 @@ import type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import {
|
||||
normalizeResolvedSecretInputString,
|
||||
resolveSecretInputString,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import { selectDiscordRuntimeConfig } from "./runtime-config.js";
|
||||
|
||||
type DiscordTokenSource = "env" | "config" | "none";
|
||||
export type DiscordCredentialStatus = "available" | "configured_unavailable" | "missing";
|
||||
|
||||
export type DiscordTokenResolution = BaseTokenResolution & {
|
||||
source: DiscordTokenSource;
|
||||
tokenStatus: DiscordCredentialStatus;
|
||||
};
|
||||
|
||||
type DiscordTokenValueResolution =
|
||||
| { status: "available"; value: string }
|
||||
| { status: "configured_unavailable" }
|
||||
| { status: "missing" };
|
||||
|
||||
function stripDiscordBotPrefix(token: string): string {
|
||||
return token.replace(/^Bot\s+/i, "");
|
||||
}
|
||||
|
||||
export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
|
||||
const trimmed = normalizeResolvedSecretInputString({ value: raw, path });
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.replace(/^Bot\s+/i, "");
|
||||
return stripDiscordBotPrefix(trimmed);
|
||||
}
|
||||
|
||||
function resolveDiscordTokenValue(params: {
|
||||
cfg: OpenClawConfig;
|
||||
value: unknown;
|
||||
path: string;
|
||||
}): DiscordTokenValueResolution {
|
||||
const resolved = resolveSecretInputString({
|
||||
value: params.value,
|
||||
path: params.path,
|
||||
defaults: params.cfg.secrets?.defaults,
|
||||
mode: "inspect",
|
||||
});
|
||||
if (resolved.status === "available") {
|
||||
return {
|
||||
status: "available",
|
||||
value: stripDiscordBotPrefix(resolved.value),
|
||||
};
|
||||
}
|
||||
if (resolved.status === "configured_unavailable") {
|
||||
return { status: "configured_unavailable" };
|
||||
}
|
||||
return { status: "missing" };
|
||||
}
|
||||
|
||||
export function resolveDiscordToken(
|
||||
cfg: OpenClawConfig,
|
||||
opts: { accountId?: string | null; envToken?: string | null } = {},
|
||||
): DiscordTokenResolution {
|
||||
const selectedCfg = selectDiscordRuntimeConfig(cfg);
|
||||
const accountId = normalizeAccountId(opts.accountId);
|
||||
const discordCfg = cfg?.channels?.discord;
|
||||
const discordCfg = selectedCfg?.channels?.discord;
|
||||
const accountCfg = resolveAccountEntry(discordCfg?.accounts, accountId);
|
||||
const hasAccountToken = Boolean(
|
||||
accountCfg &&
|
||||
Object.prototype.hasOwnProperty.call(accountCfg as Record<string, unknown>, "token"),
|
||||
);
|
||||
const accountToken = normalizeDiscordToken(
|
||||
(accountCfg as { token?: unknown } | undefined)?.token ?? undefined,
|
||||
`channels.discord.accounts.${accountId}.token`,
|
||||
);
|
||||
if (accountToken) {
|
||||
return { token: accountToken, source: "config" };
|
||||
const accountToken = resolveDiscordTokenValue({
|
||||
cfg: selectedCfg,
|
||||
value: (accountCfg as { token?: unknown } | undefined)?.token,
|
||||
path: `channels.discord.accounts.${accountId}.token`,
|
||||
});
|
||||
if (accountToken.status === "available" && accountToken.value) {
|
||||
return { token: accountToken.value, source: "config", tokenStatus: "available" };
|
||||
}
|
||||
if (accountToken.status === "configured_unavailable") {
|
||||
return { token: "", source: "config", tokenStatus: "configured_unavailable" };
|
||||
}
|
||||
if (hasAccountToken) {
|
||||
return { token: "", source: "none" };
|
||||
return { token: "", source: "none", tokenStatus: "missing" };
|
||||
}
|
||||
|
||||
const configToken = normalizeDiscordToken(
|
||||
discordCfg?.token ?? undefined,
|
||||
"channels.discord.token",
|
||||
);
|
||||
if (configToken) {
|
||||
return { token: configToken, source: "config" };
|
||||
const configToken = resolveDiscordTokenValue({
|
||||
cfg: selectedCfg,
|
||||
value: discordCfg?.token,
|
||||
path: "channels.discord.token",
|
||||
});
|
||||
if (configToken.status === "available" && configToken.value) {
|
||||
return { token: configToken.value, source: "config", tokenStatus: "available" };
|
||||
}
|
||||
if (configToken.status === "configured_unavailable") {
|
||||
return { token: "", source: "config", tokenStatus: "configured_unavailable" };
|
||||
}
|
||||
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
@@ -53,8 +100,8 @@ export function resolveDiscordToken(
|
||||
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN, "DISCORD_BOT_TOKEN")
|
||||
: undefined;
|
||||
if (envToken) {
|
||||
return { token: envToken, source: "env" };
|
||||
return { token: envToken, source: "env", tokenStatus: "available" };
|
||||
}
|
||||
|
||||
return { token: "", source: "none" };
|
||||
return { token: "", source: "none", tokenStatus: "missing" };
|
||||
}
|
||||
|
||||
@@ -172,6 +172,25 @@ describe("channel-health-monitor", () => {
|
||||
monitor.stop();
|
||||
});
|
||||
|
||||
it("keeps running after a runtime snapshot failure", async () => {
|
||||
const manager = createMockChannelManager({
|
||||
getRuntimeSnapshot: vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("snapshot failed");
|
||||
})
|
||||
.mockReturnValue({ channels: {}, channelAccounts: {} }),
|
||||
});
|
||||
const monitor = startDefaultMonitor(manager);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(DEFAULT_CHECK_INTERVAL_MS + 1);
|
||||
await vi.advanceTimersByTimeAsync(DEFAULT_CHECK_INTERVAL_MS + 1);
|
||||
|
||||
expect(manager.getRuntimeSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(manager.startChannel).not.toHaveBeenCalled();
|
||||
monitor.stop();
|
||||
});
|
||||
|
||||
it("accepts timing.monitorStartupGraceMs", async () => {
|
||||
const manager = createMockChannelManager();
|
||||
const monitor = startDefaultMonitor(manager, { timing: { monitorStartupGraceMs: 60_000 } });
|
||||
|
||||
@@ -174,6 +174,8 @@ export function startChannelHealthMonitor(deps: ChannelHealthMonitorDeps): Chann
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error?.(`health-monitor: check failed: ${String(err)}`);
|
||||
} finally {
|
||||
checkInFlight = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user