mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
doctor: expose channel plugin blocker findings (#97496)
This commit is contained in:
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import * as manifestRegistry from "../../../plugins/manifest-registry.js";
|
||||
import {
|
||||
channelPluginBlockerHitToHealthFinding,
|
||||
collectConfiguredChannelPluginBlockerWarnings,
|
||||
isWarningBlockedByChannelPlugin,
|
||||
scanConfiguredChannelPluginBlockers,
|
||||
@@ -63,6 +64,16 @@ describe("channel plugin blockers", () => {
|
||||
expect(collectConfiguredChannelPluginBlockerWarnings(hits)).toEqual([
|
||||
'- channels.discord: channel is configured, but external plugin "discord" is installed without explicit trust. Add plugins.entries.discord.enabled=true. Fix plugin enablement before relying on setup guidance for this channel.',
|
||||
]);
|
||||
expect(channelPluginBlockerHitToHealthFinding(hits[0])).toEqual({
|
||||
checkId: "core/doctor/channel-plugin-blockers",
|
||||
severity: "warning",
|
||||
message:
|
||||
'channels.discord: channel is configured, but external plugin "discord" is installed without explicit trust. Add plugins.entries.discord.enabled=true. Fix plugin enablement before relying on setup guidance for this channel.',
|
||||
path: "channels.discord",
|
||||
target: "discord",
|
||||
requirement: "missing explicit enablement",
|
||||
fixHint: "Fix plugin enablement before relying on setup guidance for this channel.",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports blockers for enabled-only channel intent", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/s
|
||||
import { sanitizeForLog } from "../../../../packages/terminal-core/src/ansi.js";
|
||||
import { listExplicitlyDisabledChannelIdsForConfig } from "../../../channels/config-presence.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import type { HealthFinding } from "../../../flows/health-checks.js";
|
||||
import {
|
||||
hasExplicitChannelConfig,
|
||||
listExplicitConfiguredChannelIdsForConfig,
|
||||
@@ -20,7 +21,9 @@ import type { PluginManifestRecord } from "../../../plugins/manifest-registry.js
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js";
|
||||
import { isSafeChannelEnvVarTriggerName } from "../../../secrets/channel-env-var-names.js";
|
||||
|
||||
type ChannelPluginBlockerHit = {
|
||||
const CHANNEL_PLUGIN_BLOCKERS_CHECK_ID = "core/doctor/channel-plugin-blockers";
|
||||
|
||||
export type ChannelPluginBlockerHit = {
|
||||
/** Normalized configured channel id whose backing plugin is unavailable. */
|
||||
channelId: string;
|
||||
/** Plugin id that would provide the configured channel. */
|
||||
@@ -359,6 +362,25 @@ export function collectConfiguredChannelPluginBlockerWarnings(
|
||||
);
|
||||
}
|
||||
|
||||
function stripListMarker(message: string): string {
|
||||
return message.startsWith("- ") ? message.slice(2) : message;
|
||||
}
|
||||
|
||||
/** Convert a configured channel plugin blocker into a structured Doctor finding. */
|
||||
export function channelPluginBlockerHitToHealthFinding(
|
||||
hit: ChannelPluginBlockerHit,
|
||||
): HealthFinding {
|
||||
return {
|
||||
checkId: CHANNEL_PLUGIN_BLOCKERS_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: stripListMarker(collectConfiguredChannelPluginBlockerWarnings([hit])[0] ?? ""),
|
||||
path: `channels.${hit.channelId}`,
|
||||
target: hit.pluginId,
|
||||
requirement: hit.reason,
|
||||
fixHint: "Fix plugin enablement before relying on setup guidance for this channel.",
|
||||
};
|
||||
}
|
||||
|
||||
/** Return true when a setup warning targets a channel already explained by plugin blockers. */
|
||||
export function isWarningBlockedByChannelPlugin(
|
||||
warning: string,
|
||||
|
||||
@@ -77,6 +77,19 @@ const mocks = vi.hoisted(() => ({
|
||||
noteWorkspaceStatus: vi.fn(),
|
||||
collectWorkspaceStatusHealthFindings: vi.fn().mockResolvedValue([]),
|
||||
collectDevicePairingHealthFindings: vi.fn(async () => []),
|
||||
scanConfiguredChannelPluginBlockers: vi.fn(
|
||||
(): Array<{ channelId: string; pluginId: string; reason: string }> => [],
|
||||
),
|
||||
channelPluginBlockerHitToHealthFinding: vi.fn(
|
||||
(hit: { channelId: string; pluginId: string; reason: string }) => ({
|
||||
checkId: "core/doctor/channel-plugin-blockers",
|
||||
severity: "warning" as const,
|
||||
message: "channels." + hit.channelId + " blocked",
|
||||
path: "channels." + hit.channelId,
|
||||
target: hit.pluginId,
|
||||
requirement: hit.reason,
|
||||
}),
|
||||
),
|
||||
applyWizardMetadata: vi.fn((cfg: unknown) => cfg),
|
||||
logConfigUpdated: vi.fn(),
|
||||
isRecord: vi.fn(
|
||||
@@ -278,6 +291,11 @@ vi.mock("../commands/doctor-device-pairing.js", () => ({
|
||||
noteDevicePairingHealth: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../commands/doctor/shared/channel-plugin-blockers.js", () => ({
|
||||
scanConfiguredChannelPluginBlockers: mocks.scanConfiguredChannelPluginBlockers,
|
||||
channelPluginBlockerHitToHealthFinding: mocks.channelPluginBlockerHitToHealthFinding,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/onboard-helpers.js", () => ({
|
||||
applyWizardMetadata: mocks.applyWizardMetadata,
|
||||
randomToken: vi.fn(() => "generated-gateway-token"),
|
||||
@@ -452,6 +470,9 @@ describe("doctor health contributions", () => {
|
||||
mocks.collectWorkspaceStatusHealthFindings.mockResolvedValue([]);
|
||||
mocks.collectDevicePairingHealthFindings.mockReset();
|
||||
mocks.collectDevicePairingHealthFindings.mockResolvedValue([]);
|
||||
mocks.scanConfiguredChannelPluginBlockers.mockReset();
|
||||
mocks.scanConfiguredChannelPluginBlockers.mockReturnValue([]);
|
||||
mocks.channelPluginBlockerHitToHealthFinding.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1169,6 +1190,7 @@ describe("doctor health contributions", () => {
|
||||
expect(contributionIds).toContain("core/doctor/plugin-registry");
|
||||
expect(contributionIds).toContain("core/doctor/configured-plugin-installs");
|
||||
expect(contributionIds).toContain("core/doctor/device-pairing");
|
||||
expect(contributionIds).toContain("core/doctor/channel-plugin-blockers");
|
||||
expect(contributionChecks.map((check) => check.id)).toEqual(contributionIds);
|
||||
});
|
||||
|
||||
@@ -1279,7 +1301,6 @@ describe("doctor health contributions", () => {
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
} as const;
|
||||
const checks = [devicePairingCheck!];
|
||||
|
||||
await expect(runDoctorLintChecks(ctx, { checks })).resolves.toMatchObject({
|
||||
checksRun: 0,
|
||||
checksSkipped: 1,
|
||||
@@ -1298,6 +1319,46 @@ describe("doctor health contributions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps channel plugin blockers opt-in for default lint selection", async () => {
|
||||
const contributionChecks = await resolveDoctorContributionHealthChecks();
|
||||
const blockerCheck = contributionChecks.find(
|
||||
(check) => check.id === "core/doctor/channel-plugin-blockers",
|
||||
);
|
||||
expect(blockerCheck).toMatchObject({ defaultEnabled: false });
|
||||
expect(blockerCheck).toBeDefined();
|
||||
mocks.scanConfiguredChannelPluginBlockers.mockReturnValue([
|
||||
{ channelId: "discord", pluginId: "discord", reason: "missing explicit enablement" },
|
||||
]);
|
||||
|
||||
const ctx = {
|
||||
cfg: { channels: { discord: { enabled: true } } },
|
||||
mode: "lint",
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
} as const;
|
||||
const checks = [blockerCheck!];
|
||||
|
||||
await expect(runDoctorLintChecks(ctx, { checks })).resolves.toMatchObject({
|
||||
checksRun: 0,
|
||||
checksSkipped: 1,
|
||||
});
|
||||
expect(mocks.scanConfiguredChannelPluginBlockers).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
runDoctorLintChecks(ctx, { checks, onlyIds: ["core/doctor/channel-plugin-blockers"] }),
|
||||
).resolves.toMatchObject({
|
||||
checksRun: 1,
|
||||
checksSkipped: 0,
|
||||
findings: [
|
||||
expect.objectContaining({
|
||||
checkId: "core/doctor/channel-plugin-blockers",
|
||||
path: "channels.discord",
|
||||
target: "discord",
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(mocks.scanConfiguredChannelPluginBlockers).toHaveBeenCalledWith(ctx.cfg, process.env);
|
||||
});
|
||||
|
||||
it("uses legacy run when a contribution also declares structured health", async () => {
|
||||
const legacyRun = vi.fn();
|
||||
const healthChecks = {
|
||||
|
||||
@@ -1638,6 +1638,18 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
|
||||
createDoctorHealthContribution({
|
||||
id: "doctor:startup-channel-maintenance",
|
||||
label: "Startup channel maintenance",
|
||||
healthChecks: {
|
||||
id: "core/doctor/channel-plugin-blockers",
|
||||
description: "Configured channels must have loadable backing channel plugins.",
|
||||
defaultEnabled: false,
|
||||
async detect(ctx) {
|
||||
const { channelPluginBlockerHitToHealthFinding, scanConfiguredChannelPluginBlockers } =
|
||||
await import("../commands/doctor/shared/channel-plugin-blockers.js");
|
||||
return scanConfiguredChannelPluginBlockers(ctx.cfg, process.env).map(
|
||||
channelPluginBlockerHitToHealthFinding,
|
||||
);
|
||||
},
|
||||
},
|
||||
run: runStartupChannelMaintenanceHealth,
|
||||
}),
|
||||
createDoctorHealthContribution({
|
||||
|
||||
Reference in New Issue
Block a user