test: narrow channel token summary coverage

This commit is contained in:
Peter Steinberger
2026-04-17 08:38:50 +01:00
parent e477125608
commit a90daa5759
3 changed files with 378 additions and 562 deletions

View File

@@ -0,0 +1,251 @@
import { hasConfiguredUnavailableCredentialStatus } from "../../channels/account-snapshot-fields.js";
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js";
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
import { asRecord } from "../../shared/record-coerce.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export type ChannelAccountTokenSummaryRow = {
account: unknown;
enabled: boolean;
snapshot: ChannelAccountSnapshot;
};
function summarizeSources(sources: Array<string | undefined>): {
label: string;
parts: string[];
} {
const counts = new Map<string, number>();
for (const s of sources) {
const key = s?.trim() ? s.trim() : "unknown";
counts.set(key, (counts.get(key) ?? 0) + 1);
}
const parts = [...counts.entries()]
.toSorted((a, b) => b[1] - a[1])
.map(([key, n]) => `${key}${n > 1 ? `×${n}` : ""}`);
const label = parts.length > 0 ? parts.join("+") : "unknown";
return { label, parts };
}
function formatTokenHint(token: string, opts: { showSecrets: boolean }): string {
const t = token.trim();
if (!t) {
return "empty";
}
if (!opts.showSecrets) {
return `sha256:${sha256HexPrefix(t, 8)} · len ${t.length}`;
}
const head = t.slice(0, 4);
const tail = t.slice(-4);
if (t.length <= 10) {
return `${t} · len ${t.length}`;
}
return `${head}${tail} · len ${t.length}`;
}
export function summarizeTokenConfig(params: {
accounts: ChannelAccountTokenSummaryRow[];
showSecrets: boolean;
}): { state: "ok" | "setup" | "warn" | null; detail: string | null } {
const enabled = params.accounts.filter((a) => a.enabled);
if (enabled.length === 0) {
return { state: null, detail: null };
}
const accountRecs = enabled.map((a) => asRecord(a.account));
const hasBotTokenField = accountRecs.some((r) => "botToken" in r);
const hasAppTokenField = accountRecs.some((r) => "appToken" in r);
const hasSigningSecretField = accountRecs.some(
(r) => "signingSecret" in r || "signingSecretSource" in r || "signingSecretStatus" in r,
);
const hasTokenField = accountRecs.some((r) => "token" in r);
if (!hasBotTokenField && !hasAppTokenField && !hasSigningSecretField && !hasTokenField) {
return { state: null, detail: null };
}
const accountIsHttpMode = (rec: Record<string, unknown>) =>
typeof rec.mode === "string" && rec.mode.trim() === "http";
const hasCredentialAvailable = (
rec: Record<string, unknown>,
valueKey: string,
statusKey: string,
) => {
const value = rec[valueKey];
if (typeof value === "string" && value.trim()) {
return true;
}
return rec[statusKey] === "available";
};
if (
hasBotTokenField &&
hasSigningSecretField &&
enabled.every((a) => accountIsHttpMode(asRecord(a.account)))
) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return (
hasCredentialAvailable(rec, "botToken", "botTokenStatus") &&
hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus")
);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const hasBot = hasCredentialAvailable(rec, "botToken", "botTokenStatus");
const hasSigning = hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus");
return (hasBot && !hasSigning) || (!hasBot && hasSigning);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured http credentials unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (partial.length > 0) {
return {
state: "warn",
detail: `partial credentials (need bot+signing) · accounts ${partial.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no credentials (need bot+signing)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const signingSources = summarizeSources(
ready.map((a) => a.snapshot.signingSecretSource ?? "none"),
);
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const signingSecret = typeof sample.signingSecret === "string" ? sample.signingSecret : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const signingHint = signingSecret.trim()
? formatTokenHint(signingSecret, { showSecrets: params.showSecrets })
: "";
const hint =
botHint || signingHint ? ` (bot ${botHint || "?"}, signing ${signingHint || "?"})` : "";
return {
state: "ok",
detail: `credentials ok (bot ${botSources.label}, signing ${signingSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField && hasAppTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
const app = normalizeOptionalString(rec.appToken) ?? "";
return Boolean(bot) && Boolean(app);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
const app = normalizeOptionalString(rec.appToken) ?? "";
const hasBot = Boolean(bot);
const hasApp = Boolean(app);
return (hasBot && !hasApp) || (!hasBot && hasApp);
});
if (partial.length > 0) {
return {
state: "warn",
detail: `partial tokens (need bot+app) · accounts ${partial.length}`,
};
}
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured tokens unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no tokens (need bot+app)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const appSources = summarizeSources(ready.map((a) => a.snapshot.appTokenSource ?? "none"));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const appToken = typeof sample.appToken === "string" ? sample.appToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const appHint = appToken.trim()
? formatTokenHint(appToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint || appHint ? ` (bot ${botHint || "?"}, app ${appHint || "?"})` : "";
return {
state: "ok",
detail: `tokens ok (bot ${botSources.label}, app ${appSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
return Boolean(bot);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured bot token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no bot token" };
}
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint ? ` (${botHint})` : "";
return {
state: "ok",
detail: `bot token config${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return Boolean(normalizeOptionalString(rec.token));
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no token" };
}
const sources = summarizeSources(ready.map((a) => a.snapshot.tokenSource));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const token = typeof sample.token === "string" ? sample.token : "";
const hint = token.trim()
? ` (${formatTokenHint(token, { showSecrets: params.showSecrets })})`
: "";
return {
state: "ok",
detail: `token ${sources.label}${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}

View File

@@ -1,332 +1,141 @@
import { describe, expect, it, vi } from "vitest";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import { makeDirectPlugin } from "../../test-utils/channel-plugin-test-fixtures.js";
import { buildChannelsTable } from "./channels.js";
import { describe, expect, it } from "vitest";
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js";
import {
summarizeTokenConfig,
type ChannelAccountTokenSummaryRow,
} from "./channels-token-summary.js";
vi.mock("../../channels/plugins/index.js", () => ({
listChannelPlugins: vi.fn(),
}));
function makeMattermostPlugin(): ChannelPlugin {
function tokenRow(params: {
account: Record<string, unknown>;
snapshot?: Partial<ChannelAccountSnapshot>;
enabled?: boolean;
}): ChannelAccountTokenSummaryRow {
return {
id: "mattermost",
meta: {
id: "mattermost",
label: "Mattermost",
selectionLabel: "Mattermost",
docsPath: "/channels/mattermost",
blurb: "test",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["echo"],
defaultAccountId: () => "echo",
resolveAccount: () => ({
name: "Echo",
enabled: true,
botToken: "bot-token-value",
baseUrl: "https://mm.example.com",
}),
isConfigured: () => true,
isEnabled: () => true,
},
actions: {
describeMessageTool: () => ({ actions: ["send"] }),
},
account: params.account,
enabled: params.enabled ?? true,
snapshot: {
accountId: "primary",
...params.snapshot,
} as ChannelAccountSnapshot,
};
}
type TestTable = Awaited<ReturnType<typeof buildChannelsTable>>;
function makeSlackDirectPlugin(config: ChannelPlugin["config"]): ChannelPlugin {
return makeDirectPlugin({
id: "slack",
label: "Slack",
docsPath: "/channels/slack",
config,
});
function summarize(accounts: ChannelAccountTokenSummaryRow[]) {
return summarizeTokenConfig({ accounts, showSecrets: false });
}
function createSlackTokenAccount(params?: { botToken?: string; appToken?: string }) {
return {
name: "Primary",
enabled: true,
botToken: params?.botToken ?? "bot-token",
appToken: params?.appToken ?? "app-token",
};
}
function createUnavailableSlackTokenAccount() {
return {
name: "Primary",
enabled: true,
configured: true,
botToken: "",
appToken: "",
botTokenSource: "config",
appTokenSource: "config",
botTokenStatus: "configured_unavailable",
appTokenStatus: "configured_unavailable",
};
}
function makeSlackPlugin(params?: { botToken?: string; appToken?: string }): ChannelPlugin {
return makeSlackDirectPlugin({
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
inspectAccount: () => createSlackTokenAccount(params),
resolveAccount: () => createSlackTokenAccount(params),
isConfigured: () => true,
isEnabled: () => true,
});
}
function makeUnavailableSlackPlugin(): ChannelPlugin {
return makeSlackDirectPlugin({
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
inspectAccount: () => createUnavailableSlackTokenAccount(),
resolveAccount: () => createUnavailableSlackTokenAccount(),
isConfigured: () => true,
isEnabled: () => true,
});
}
function makeSourceAwareUnavailablePlugin(): ChannelPlugin {
return makeSlackDirectPlugin({
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
inspectAccount: (cfg) =>
(cfg as { marker?: string }).marker === "source"
? createUnavailableSlackTokenAccount()
: {
name: "Primary",
enabled: true,
configured: false,
botToken: "",
appToken: "",
botTokenSource: "none",
appTokenSource: "none",
},
resolveAccount: () => ({
name: "Primary",
enabled: true,
botToken: "",
appToken: "",
}),
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
isEnabled: () => true,
});
}
function makeSourceUnavailableResolvedAvailablePlugin(): ChannelPlugin {
return makeDirectPlugin({
id: "discord",
label: "Discord",
docsPath: "/channels/discord",
config: {
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
inspectAccount: (cfg) =>
(cfg as { marker?: string }).marker === "source"
? {
name: "Primary",
enabled: true,
configured: true,
tokenSource: "config",
tokenStatus: "configured_unavailable",
}
: {
name: "Primary",
enabled: true,
configured: true,
tokenSource: "config",
tokenStatus: "available",
},
resolveAccount: () => ({
name: "Primary",
enabled: true,
configured: true,
tokenSource: "config",
tokenStatus: "available",
describe("summarizeTokenConfig", () => {
it("does not require appToken for bot-token-only channels", () => {
const summary = summarize([
tokenRow({
account: {
botToken: "bot-token-value",
baseUrl: "https://mm.example.com",
},
snapshot: { botTokenSource: "config" },
}),
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
isEnabled: () => true,
},
});
}
function makeHttpSlackUnavailablePlugin(): ChannelPlugin {
return makeDirectPlugin({
id: "slack",
label: "Slack",
docsPath: "/channels/slack",
config: {
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
inspectAccount: () => ({
accountId: "primary",
name: "Primary",
enabled: true,
configured: true,
mode: "http",
botToken: "xoxb-http",
signingSecret: "",
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
botTokenStatus: "available",
signingSecretStatus: "configured_unavailable", // pragma: allowlist secret
}),
resolveAccount: () => ({
name: "Primary",
enabled: true,
configured: true,
mode: "http",
botToken: "xoxb-http",
signingSecret: "",
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
botTokenStatus: "available",
signingSecretStatus: "configured_unavailable", // pragma: allowlist secret
}),
isConfigured: () => true,
isEnabled: () => true,
},
});
}
function makeTokenPlugin(): ChannelPlugin {
return makeDirectPlugin({
id: "token-only",
label: "TokenOnly",
docsPath: "/channels/token-only",
config: {
listAccountIds: () => ["primary"],
defaultAccountId: () => "primary",
resolveAccount: () => ({
name: "Primary",
enabled: true,
token: "token-value",
}),
isConfigured: () => true,
isEnabled: () => true,
},
});
}
async function buildTestTable(
plugins: ChannelPlugin[],
params?: { cfg?: Record<string, unknown>; sourceConfig?: Record<string, unknown> },
) {
vi.mocked(listChannelPlugins).mockReturnValue(plugins);
return await buildChannelsTable((params?.cfg ?? { channels: {} }) as never, {
showSecrets: false,
sourceConfig: params?.sourceConfig as never,
});
}
function expectTableRow(
table: TestTable,
params: { id: string; state: string; detailContains?: string; detailEquals?: string },
) {
const row = table.rows.find((entry) => entry.id === params.id);
expect(row).toBeDefined();
expect(row?.state).toBe(params.state);
if (params.detailContains) {
expect(row?.detail).toContain(params.detailContains);
}
if (params.detailEquals) {
expect(row?.detail).toBe(params.detailEquals);
}
return row;
}
function expectTableDetailRows(
table: TestTable,
title: string,
rows: Array<Record<string, string>>,
) {
const detail = table.details.find((entry) => entry.title === title);
expect(detail).toBeDefined();
expect(detail?.rows).toEqual(rows);
}
describe("buildChannelsTable - mattermost token summary", () => {
it("does not require appToken for mattermost accounts", async () => {
const table = await buildTestTable([makeMattermostPlugin()]);
const mattermostRow = expectTableRow(table, { id: "mattermost", state: "ok" });
expect(mattermostRow?.detail).not.toContain("need bot+app");
});
it("keeps bot+app requirement when both fields exist", async () => {
const table = await buildTestTable([makeSlackPlugin({ botToken: "bot-token", appToken: "" })]);
expectTableRow(table, { id: "slack", state: "warn", detailContains: "need bot+app" });
});
it("reports configured-but-unavailable Slack credentials as warn", async () => {
const table = await buildTestTable([makeUnavailableSlackPlugin()]);
expectTableRow(table, {
id: "slack",
state: "warn",
detailContains: "unavailable in this command path",
});
});
it("preserves unavailable credential state from the source config snapshot", async () => {
const table = await buildTestTable([makeSourceAwareUnavailablePlugin()], {
cfg: { marker: "resolved", channels: {} },
sourceConfig: { marker: "source", channels: {} },
});
expectTableRow(table, {
id: "slack",
state: "warn",
detailContains: "unavailable in this command path",
});
expectTableDetailRows(table, "Slack accounts", [
{
Account: "primary (Primary)",
Notes: "bot:config · app:config · secret unavailable in this command path",
Status: "WARN",
},
]);
expect(summary.state).toBe("ok");
expect(summary.detail).toContain("bot token config");
expect(summary.detail).not.toContain("need bot+app");
});
it("treats status-only available credentials as resolved", async () => {
const table = await buildTestTable([makeSourceUnavailableResolvedAvailablePlugin()], {
cfg: { marker: "resolved", channels: {} },
sourceConfig: { marker: "source", channels: {} },
});
expectTableRow(table, { id: "discord", state: "ok", detailEquals: "configured" });
expectTableDetailRows(table, "Discord accounts", [
{
Account: "primary (Primary)",
Notes: "token:config",
Status: "OK",
},
it("keeps bot+app requirement when both fields exist", () => {
const summary = summarize([
tokenRow({
account: {
botToken: "bot-token",
appToken: "",
},
}),
]);
expect(summary.state).toBe("warn");
expect(summary.detail).toContain("need bot+app");
});
it("treats Slack HTTP signing-secret availability as required config", async () => {
const table = await buildTestTable([makeHttpSlackUnavailablePlugin()]);
expectTableRow(table, {
id: "slack",
state: "warn",
detailContains: "configured http credentials unavailable",
});
expectTableDetailRows(table, "Slack accounts", [
{
Account: "primary (Primary)",
Notes: "bot:config · signing:config · secret unavailable in this command path",
Status: "WARN",
},
it("reports configured-but-unavailable Slack credentials as warn", () => {
const summary = summarize([
tokenRow({
account: {
configured: true,
botToken: "",
appToken: "",
botTokenSource: "config",
appTokenSource: "config",
botTokenStatus: "configured_unavailable",
appTokenStatus: "configured_unavailable",
},
snapshot: {
botTokenSource: "config",
appTokenSource: "config",
},
}),
]);
expect(summary.state).toBe("warn");
expect(summary.detail).toContain("unavailable in this command path");
});
it("still reports single-token channels as ok", async () => {
const table = await buildTestTable([makeTokenPlugin()]);
expectTableRow(table, { id: "token-only", state: "ok", detailContains: "token" });
it("treats status-only available HTTP credentials as resolved", () => {
const summary = summarize([
tokenRow({
account: {
mode: "http",
botToken: "",
signingSecret: "", // pragma: allowlist secret
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
botTokenStatus: "available",
signingSecretStatus: "available", // pragma: allowlist secret
},
snapshot: {
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
},
}),
]);
expect(summary.state).toBe("ok");
expect(summary.detail).toContain("credentials ok");
});
it("treats Slack HTTP signing-secret availability as required config", () => {
const summary = summarize([
tokenRow({
account: {
mode: "http",
botToken: "xoxb-http",
signingSecret: "", // pragma: allowlist secret
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
botTokenStatus: "available",
signingSecretStatus: "configured_unavailable", // pragma: allowlist secret
},
snapshot: {
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
},
}),
]);
expect(summary.state).toBe("warn");
expect(summary.detail).toContain("configured http credentials unavailable");
});
it("still reports single-token channels as ok", () => {
const summary = summarize([
tokenRow({
account: {
token: "token-value",
tokenSource: "config",
},
snapshot: { tokenSource: "config" },
}),
]);
expect(summary.state).toBe("ok");
expect(summary.detail).toContain("token config");
});
});

View File

@@ -18,9 +18,12 @@ import type {
} from "../../channels/plugins/types.public.js";
import { inspectReadOnlyChannelAccount } from "../../channels/read-only-account-inspect.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
import { asRecord } from "../../shared/record-coerce.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
summarizeTokenConfig,
type ChannelAccountTokenSummaryRow,
} from "./channels-token-summary.js";
import { formatTimeAgo } from "./format.js";
export type ChannelRow = {
@@ -31,12 +34,9 @@ export type ChannelRow = {
detail: string;
};
type ChannelAccountRow = {
type ChannelAccountRow = ChannelAccountTokenSummaryRow & {
accountId: string;
account: unknown;
enabled: boolean;
configured: boolean;
snapshot: ChannelAccountSnapshot;
};
type ResolvedChannelAccountRowParams = {
@@ -46,22 +46,6 @@ type ResolvedChannelAccountRowParams = {
accountId: string;
};
function summarizeSources(sources: Array<string | undefined>): {
label: string;
parts: string[];
} {
const counts = new Map<string, number>();
for (const s of sources) {
const key = s?.trim() ? s.trim() : "unknown";
counts.set(key, (counts.get(key) ?? 0) + 1);
}
const parts = [...counts.entries()]
.toSorted((a, b) => b[1] - a[1])
.map(([key, n]) => `${key}${n > 1 ? `×${n}` : ""}`);
const label = parts.length > 0 ? parts.join("+") : "unknown";
return { label, parts };
}
function existsSyncMaybe(p: string | undefined): boolean | null {
const path = normalizeOptionalString(p) ?? "";
if (!path) {
@@ -74,22 +58,6 @@ function existsSyncMaybe(p: string | undefined): boolean | null {
}
}
function formatTokenHint(token: string, opts: { showSecrets: boolean }): string {
const t = token.trim();
if (!t) {
return "empty";
}
if (!opts.showSecrets) {
return `sha256:${sha256HexPrefix(t, 8)} · len ${t.length}`;
}
const head = t.slice(0, 4);
const tail = t.slice(-4);
if (t.length <= 10) {
return `${t} · len ${t.length}`;
}
return `${head}${tail} · len ${t.length}`;
}
async function inspectChannelAccount(
plugin: ChannelPlugin,
cfg: OpenClawConfig,
@@ -256,216 +224,6 @@ function collectMissingPaths(accounts: ChannelAccountRow[]): string[] {
return missing;
}
function summarizeTokenConfig(params: {
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accounts: ChannelAccountRow[];
showSecrets: boolean;
}): { state: "ok" | "setup" | "warn" | null; detail: string | null } {
const enabled = params.accounts.filter((a) => a.enabled);
if (enabled.length === 0) {
return { state: null, detail: null };
}
const accountRecs = enabled.map((a) => asRecord(a.account));
const hasBotTokenField = accountRecs.some((r) => "botToken" in r);
const hasAppTokenField = accountRecs.some((r) => "appToken" in r);
const hasSigningSecretField = accountRecs.some(
(r) => "signingSecret" in r || "signingSecretSource" in r || "signingSecretStatus" in r,
);
const hasTokenField = accountRecs.some((r) => "token" in r);
if (!hasBotTokenField && !hasAppTokenField && !hasSigningSecretField && !hasTokenField) {
return { state: null, detail: null };
}
const accountIsHttpMode = (rec: Record<string, unknown>) =>
typeof rec.mode === "string" && rec.mode.trim() === "http";
const hasCredentialAvailable = (
rec: Record<string, unknown>,
valueKey: string,
statusKey: string,
) => {
const value = rec[valueKey];
if (typeof value === "string" && value.trim()) {
return true;
}
return rec[statusKey] === "available";
};
if (
hasBotTokenField &&
hasSigningSecretField &&
enabled.every((a) => accountIsHttpMode(asRecord(a.account)))
) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return (
hasCredentialAvailable(rec, "botToken", "botTokenStatus") &&
hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus")
);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const hasBot = hasCredentialAvailable(rec, "botToken", "botTokenStatus");
const hasSigning = hasCredentialAvailable(rec, "signingSecret", "signingSecretStatus");
return (hasBot && !hasSigning) || (!hasBot && hasSigning);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured http credentials unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (partial.length > 0) {
return {
state: "warn",
detail: `partial credentials (need bot+signing) · accounts ${partial.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no credentials (need bot+signing)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const signingSources = summarizeSources(
ready.map((a) => a.snapshot.signingSecretSource ?? "none"),
);
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const signingSecret = typeof sample.signingSecret === "string" ? sample.signingSecret : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const signingHint = signingSecret.trim()
? formatTokenHint(signingSecret, { showSecrets: params.showSecrets })
: "";
const hint =
botHint || signingHint ? ` (bot ${botHint || "?"}, signing ${signingHint || "?"})` : "";
return {
state: "ok",
detail: `credentials ok (bot ${botSources.label}, signing ${signingSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField && hasAppTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
const app = normalizeOptionalString(rec.appToken) ?? "";
return Boolean(bot) && Boolean(app);
});
const partial = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
const app = normalizeOptionalString(rec.appToken) ?? "";
const hasBot = Boolean(bot);
const hasApp = Boolean(app);
return (hasBot && !hasApp) || (!hasBot && hasApp);
});
if (partial.length > 0) {
return {
state: "warn",
detail: `partial tokens (need bot+app) · accounts ${partial.length}`,
};
}
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured tokens unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no tokens (need bot+app)" };
}
const botSources = summarizeSources(ready.map((a) => a.snapshot.botTokenSource ?? "none"));
const appSources = summarizeSources(ready.map((a) => a.snapshot.appTokenSource ?? "none"));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const appToken = typeof sample.appToken === "string" ? sample.appToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const appHint = appToken.trim()
? formatTokenHint(appToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint || appHint ? ` (bot ${botHint || "?"}, app ${appHint || "?"})` : "";
return {
state: "ok",
detail: `tokens ok (bot ${botSources.label}, app ${appSources.label})${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
if (hasBotTokenField) {
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
const bot = normalizeOptionalString(rec.botToken) ?? "";
return Boolean(bot);
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured bot token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no bot token" };
}
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const botToken = typeof sample.botToken === "string" ? sample.botToken : "";
const botHint = botToken.trim()
? formatTokenHint(botToken, { showSecrets: params.showSecrets })
: "";
const hint = botHint ? ` (${botHint})` : "";
return {
state: "ok",
detail: `bot token config${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
const unavailable = enabled.filter((a) => hasConfiguredUnavailableCredentialStatus(a.account));
const ready = enabled.filter((a) => {
const rec = asRecord(a.account);
return Boolean(normalizeOptionalString(rec.token));
});
if (unavailable.length > 0) {
return {
state: "warn",
detail: `configured token unavailable in this command path · accounts ${unavailable.length}`,
};
}
if (ready.length === 0) {
return { state: "setup", detail: "no token" };
}
const sources = summarizeSources(ready.map((a) => a.snapshot.tokenSource));
const sample = ready[0]?.account ? asRecord(ready[0].account) : {};
const token = typeof sample.token === "string" ? sample.token : "";
const hint = token.trim()
? ` (${formatTokenHint(token, { showSecrets: params.showSecrets })})`
: "";
return {
state: "ok",
detail: `token ${sources.label}${hint} · accounts ${ready.length}/${enabled.length || 1}`,
};
}
// `status --all` channels table.
// Keep this generic: channel-specific rules belong in the channel plugin.
export async function buildChannelsTable(
@@ -530,8 +288,6 @@ export async function buildChannelsTable(
const link = resolveLinkFields(summary);
const missingPaths = collectMissingPaths(enabledAccounts);
const tokenSummary = summarizeTokenConfig({
plugin,
cfg,
accounts,
showSecrets,
});