test: dedupe command fixtures

This commit is contained in:
Peter Steinberger
2026-04-23 18:09:01 +01:00
parent 7a8d304a65
commit 23b751b112
5 changed files with 194 additions and 360 deletions

View File

@@ -136,6 +136,38 @@ function buildParams(overrides: Partial<ApplyAuthChoiceParams> = {}): ApplyAuthC
};
}
function buildLocalProviderInstallCatalogEntry() {
return {
pluginId: "local-provider-plugin",
providerId: LOCAL_PROVIDER_ID,
methodId: LOCAL_AUTH_METHOD_ID,
choiceId: LOCAL_PROVIDER_ID,
choiceLabel: LOCAL_PROVIDER_LABEL,
label: LOCAL_PROVIDER_LABEL,
origin: "bundled" as const,
install: {
npmSpec: "@openclaw/local-provider",
},
};
}
function buildInstalledLocalProviderPluginResult() {
return {
cfg: {
plugins: {
entries: {
"local-provider-plugin": {
enabled: true,
},
},
},
},
installed: true,
pluginId: "local-provider-plugin",
status: "installed" as const,
};
}
describe("applyAuthChoiceLoadedPluginProvider", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -290,32 +322,8 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
it("installs a missing provider plugin and retries setup resolution", async () => {
const provider = buildProvider();
resolveProviderInstallCatalogEntry.mockReturnValue({
pluginId: "local-provider-plugin",
providerId: LOCAL_PROVIDER_ID,
methodId: LOCAL_AUTH_METHOD_ID,
choiceId: LOCAL_PROVIDER_ID,
choiceLabel: LOCAL_PROVIDER_LABEL,
label: LOCAL_PROVIDER_LABEL,
origin: "bundled",
install: {
npmSpec: "@openclaw/local-provider",
},
});
ensureOnboardingPluginInstalled.mockResolvedValue({
cfg: {
plugins: {
entries: {
"local-provider-plugin": {
enabled: true,
},
},
},
},
installed: true,
pluginId: "local-provider-plugin",
status: "installed",
});
resolveProviderInstallCatalogEntry.mockReturnValue(buildLocalProviderInstallCatalogEntry());
ensureOnboardingPluginInstalled.mockResolvedValue(buildInstalledLocalProviderPluginResult());
resolvePluginProviders.mockReturnValue([provider]);
resolveProviderPluginChoice.mockReturnValueOnce(null).mockReturnValueOnce({
provider,
@@ -341,18 +349,7 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
});
it("does not persist plugin enablement when install is skipped", async () => {
resolveProviderInstallCatalogEntry.mockReturnValue({
pluginId: "local-provider-plugin",
providerId: LOCAL_PROVIDER_ID,
methodId: LOCAL_AUTH_METHOD_ID,
choiceId: LOCAL_PROVIDER_ID,
choiceLabel: LOCAL_PROVIDER_LABEL,
label: LOCAL_PROVIDER_LABEL,
origin: "bundled",
install: {
npmSpec: "@openclaw/local-provider",
},
});
resolveProviderInstallCatalogEntry.mockReturnValue(buildLocalProviderInstallCatalogEntry());
resolveProviderPluginChoice.mockReturnValue(null);
const result = await applyAuthChoiceLoadedPluginProvider(buildParams());
@@ -362,32 +359,8 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
});
it("preserves install config when the chosen provider still cannot resolve after install", async () => {
resolveProviderInstallCatalogEntry.mockReturnValue({
pluginId: "local-provider-plugin",
providerId: LOCAL_PROVIDER_ID,
methodId: LOCAL_AUTH_METHOD_ID,
choiceId: LOCAL_PROVIDER_ID,
choiceLabel: LOCAL_PROVIDER_LABEL,
label: LOCAL_PROVIDER_LABEL,
origin: "bundled",
install: {
npmSpec: "@openclaw/local-provider",
},
});
ensureOnboardingPluginInstalled.mockResolvedValue({
cfg: {
plugins: {
entries: {
"local-provider-plugin": {
enabled: true,
},
},
},
},
installed: true,
pluginId: "local-provider-plugin",
status: "installed",
});
resolveProviderInstallCatalogEntry.mockReturnValue(buildLocalProviderInstallCatalogEntry());
ensureOnboardingPluginInstalled.mockResolvedValue(buildInstalledLocalProviderPluginResult());
resolveProviderPluginChoice.mockReturnValue(null);
const result = await applyAuthChoiceLoadedPluginProvider(buildParams());

View File

@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
import { channelsStatusCommand } from "./channels/status.js";
import { createCapturingTestRuntime } from "./test-runtime-config-helpers.js";
const resolveDefaultAccountId = () => DEFAULT_ACCOUNT_ID;
@@ -176,17 +177,6 @@ function createTokenOnlyPlugin() {
};
}
function createRuntimeCapture() {
const logs: string[] = [];
const errors: string[] = [];
const runtime = {
log: (message: unknown) => logs.push(String(message)),
error: (message: unknown) => errors.push(String(message)),
exit: (_code?: number) => undefined,
};
return { runtime, logs, errors };
}
describe("channelsStatusCommand SecretRef fallback flow", () => {
beforeEach(() => {
mocks.callGateway.mockReset();
@@ -210,7 +200,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
"channels status: channels.discord.token is unavailable in this command path; continuing with degraded read-only config.",
],
});
const { runtime, logs, errors } = createRuntimeCapture();
const { runtime, logs, errors } = createCapturingTestRuntime();
await channelsStatusCommand({ probe: false }, runtime as never);
@@ -239,7 +229,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
effectiveConfig: { secretResolved: true, channels: {} },
diagnostics: [],
});
const { runtime, logs } = createRuntimeCapture();
const { runtime, logs } = createCapturingTestRuntime();
await channelsStatusCommand({ probe: false }, runtime as never);
@@ -267,7 +257,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
effectiveConfig: { secretResolved: true, channels: {} },
diagnostics: [],
});
const { runtime, logs, errors } = createRuntimeCapture();
const { runtime, logs, errors } = createCapturingTestRuntime();
await channelsStatusCommand({ json: true, probe: false }, runtime as never);

View File

@@ -11,6 +11,7 @@ import {
} from "../plugins/loader.test-fixtures.js";
import { withEnvAsync } from "../test-utils/env.js";
import { channelsStatusCommand } from "./channels/status.js";
import { createCapturingTestRuntime } from "./test-runtime-config-helpers.js";
const mocks = vi.hoisted(() => ({
callGateway: vi.fn(),
@@ -89,17 +90,6 @@ function writeExternalEnvChannelPlugin() {
return { pluginDir, fullMarker };
}
function createRuntimeCapture() {
const logs: string[] = [];
const errors: string[] = [];
const runtime = {
log: (message: unknown) => logs.push(String(message)),
error: (message: unknown) => errors.push(String(message)),
exit: (_code?: number) => undefined,
};
return { runtime, logs, errors };
}
describe("channelsStatusCommand external env-only channel fallback", () => {
beforeEach(() => {
mocks.callGateway.mockReset();
@@ -127,7 +117,7 @@ describe("channelsStatusCommand external env-only channel fallback", () => {
effectiveConfig: config,
diagnostics: [],
});
const { runtime, logs } = createRuntimeCapture();
const { runtime, logs } = createCapturingTestRuntime();
await withEnvAsync({ EXTERNAL_ENV_CHANNEL_TOKEN: "token" }, async () => {
await channelsStatusCommand({ json: true, probe: false }, runtime as never);

View File

@@ -9,6 +9,12 @@ import {
import { maybeRepairBundledPluginRuntimeDeps } from "./doctor-bundled-plugin-runtime-deps.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
type InstalledRuntimeDeps = Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}>;
function writeJson(filePath: string, value: unknown) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -25,6 +31,31 @@ function writeBundledChannelPlugin(root: string, id: string, dependencies: Recor
});
}
function createInstalledRuntimeDeps(): InstalledRuntimeDeps {
return [];
}
function createNonInteractivePrompter(
options: { updateInProgress?: boolean } = {},
): DoctorPrompter {
return {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: options.updateInProgress ?? false,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
}
describe("doctor bundled plugin runtime deps", () => {
it("skips source checkouts", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
@@ -175,31 +206,11 @@ describe("doctor bundled plugin runtime deps", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
writeJson(path.join(root, "package.json"), { name: "openclaw" });
writeBundledChannelPlugin(root, "telegram", { grammy: "1.37.0" });
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: false,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
const installed = createInstalledRuntimeDeps();
await maybeRepairBundledPluginRuntimeDeps({
runtime: { error: () => {} } as never,
prompter,
prompter: createNonInteractivePrompter(),
packageRoot: root,
config: {
plugins: { enabled: true },
@@ -223,31 +234,11 @@ describe("doctor bundled plugin runtime deps", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
writeJson(path.join(root, "package.json"), { name: "openclaw" });
writeBundledChannelPlugin(root, "feishu", { "@larksuiteoapi/node-sdk": "^1.61.0" });
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: true,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
const installed = createInstalledRuntimeDeps();
await maybeRepairBundledPluginRuntimeDeps({
runtime: { error: () => {} } as never,
prompter,
prompter: createNonInteractivePrompter({ updateInProgress: true }),
packageRoot: root,
includeConfiguredChannels: true,
config: {
@@ -274,31 +265,11 @@ describe("doctor bundled plugin runtime deps", () => {
writeJson(path.join(root, "package.json"), { name: "openclaw", version: "2026.4.22" });
writeBundledChannelPlugin(root, "slack", { "@slack/web-api": "7.15.1" });
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: false,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
const installed = createInstalledRuntimeDeps();
await maybeRepairBundledPluginRuntimeDeps({
runtime: { error: () => {} } as never,
prompter,
prompter: createNonInteractivePrompter(),
env,
packageRoot: root,
config: {
@@ -330,31 +301,11 @@ describe("doctor bundled plugin runtime deps", () => {
name: "@slack/web-api",
version: "7.15.1",
});
const installed: Array<{
installRoot: string;
missingSpecs: string[];
installSpecs: string[];
}> = [];
const prompter = {
shouldRepair: false,
shouldForce: false,
repairMode: {
shouldRepair: false,
shouldForce: false,
nonInteractive: true,
canPrompt: false,
updateInProgress: false,
},
confirm: async () => false,
confirmAutoFix: async () => false,
confirmAggressiveAutoFix: async () => false,
confirmRuntimeRepair: async () => false,
select: async (_params: unknown, fallback: unknown) => fallback,
} as DoctorPrompter;
const installed = createInstalledRuntimeDeps();
await maybeRepairBundledPluginRuntimeDeps({
runtime: { error: () => {} } as never,
prompter,
prompter: createNonInteractivePrompter(),
packageRoot: root,
includeConfiguredChannels: true,
config: {

View File

@@ -32,6 +32,80 @@ vi.mock("../../../channels/plugins/read-only.js", () => ({
) => mocks.resolveReadOnlyChannelPluginsForConfig(...args),
}));
function createMatrixEnabledConfig() {
return {
channels: {
matrix: {
enabled: true,
},
},
};
}
function createNormalizeCompatibilityConfig(change = "matrix") {
return vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: [change],
}));
}
function mockReadOnlyMatrixPlugin(doctor?: Record<string, unknown>) {
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
...(doctor ? { doctor } : {}),
},
],
});
}
function mockBundledMatrixSetupPlugin(doctor?: Record<string, unknown>) {
mocks.getBundledChannelSetupPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
...(doctor ? { doctor } : {}),
}
: undefined,
);
}
function mockBundledMatrixRuntimePlugin(doctor?: Record<string, unknown>) {
mocks.getBundledChannelPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
...(doctor ? { doctor } : {}),
}
: undefined,
);
}
function expectMatrixDoctorLookupCalls(cfg?: unknown) {
if (cfg) {
expect(mocks.resolveReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(cfg, {
includePersistedAuthState: false,
});
}
expect(mocks.getLoadedChannelPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelSetupPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelPlugin).toHaveBeenCalledWith("matrix");
}
async function expectRuntimeWarningFallback(params: {
cfg: unknown;
normalizeCompatibilityConfig: ReturnType<typeof vi.fn>;
collectMutableAllowlistWarnings: ReturnType<typeof vi.fn>;
}) {
expect(collectChannelDoctorCompatibilityMutations(params.cfg as never)).toHaveLength(1);
await expect(
collectChannelDoctorMutableAllowlistWarnings({ cfg: params.cfg as never }),
).resolves.toEqual(["runtime warning"]);
expect(params.normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(params.collectMutableAllowlistWarnings).toHaveBeenCalledTimes(1);
}
describe("channel doctor compatibility mutations", () => {
beforeEach(() => {
mocks.getLoadedChannelPlugin.mockReset();
@@ -84,224 +158,80 @@ describe("channel doctor compatibility mutations", () => {
});
it("uses read-only doctor adapters for configured channel ids", () => {
const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: ["matrix"],
}));
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
doctor: { normalizeCompatibilityConfig },
},
],
});
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
const normalizeCompatibilityConfig = createNormalizeCompatibilityConfig();
mockReadOnlyMatrixPlugin({ normalizeCompatibilityConfig });
const cfg = createMatrixEnabledConfig();
const result = collectChannelDoctorCompatibilityMutations(cfg as never);
expect(result).toHaveLength(1);
expect(normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(mocks.resolveReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(cfg, {
includePersistedAuthState: false,
});
expect(mocks.getLoadedChannelPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelSetupPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelPlugin).toHaveBeenCalledWith("matrix");
expectMatrixDoctorLookupCalls(cfg);
expect(mocks.getBundledChannelSetupPlugin).not.toHaveBeenCalledWith("discord");
});
it("merges partial doctor adapters instead of masking runtime-only hooks", async () => {
const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: ["matrix"],
}));
const normalizeCompatibilityConfig = createNormalizeCompatibilityConfig();
const collectMutableAllowlistWarnings = vi.fn(() => ["runtime warning"]);
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
doctor: { normalizeCompatibilityConfig },
},
],
mockReadOnlyMatrixPlugin({ normalizeCompatibilityConfig });
mockBundledMatrixRuntimePlugin({ collectMutableAllowlistWarnings });
const cfg = createMatrixEnabledConfig();
await expectRuntimeWarningFallback({
cfg,
normalizeCompatibilityConfig,
collectMutableAllowlistWarnings,
});
mocks.getBundledChannelPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
doctor: { collectMutableAllowlistWarnings },
}
: undefined,
);
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
expect(collectChannelDoctorCompatibilityMutations(cfg as never)).toHaveLength(1);
await expect(
collectChannelDoctorMutableAllowlistWarnings({ cfg: cfg as never }),
).resolves.toEqual(["runtime warning"]);
expect(normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(collectMutableAllowlistWarnings).toHaveBeenCalledTimes(1);
});
it("ignores malformed doctor adapter values so valid fallbacks still run", async () => {
const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: ["setup"],
}));
const normalizeCompatibilityConfig = createNormalizeCompatibilityConfig("setup");
const collectMutableAllowlistWarnings = vi.fn(() => ["runtime warning"]);
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
doctor: {
normalizeCompatibilityConfig: null,
collectMutableAllowlistWarnings: "not-a-function",
warnOnEmptyGroupSenderAllowlist: "yes",
},
},
],
mockReadOnlyMatrixPlugin({
normalizeCompatibilityConfig: null,
collectMutableAllowlistWarnings: "not-a-function",
warnOnEmptyGroupSenderAllowlist: "yes",
});
mocks.getBundledChannelSetupPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
doctor: { normalizeCompatibilityConfig },
}
: undefined,
);
mocks.getBundledChannelPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
doctor: { collectMutableAllowlistWarnings },
}
: undefined,
);
mockBundledMatrixSetupPlugin({ normalizeCompatibilityConfig });
mockBundledMatrixRuntimePlugin({ collectMutableAllowlistWarnings });
const cfg = createMatrixEnabledConfig();
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
expect(collectChannelDoctorCompatibilityMutations(cfg as never)).toHaveLength(1);
await expect(
collectChannelDoctorMutableAllowlistWarnings({ cfg: cfg as never }),
).resolves.toEqual(["runtime warning"]);
expect(normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(collectMutableAllowlistWarnings).toHaveBeenCalledTimes(1);
await expectRuntimeWarningFallback({
cfg,
normalizeCompatibilityConfig,
collectMutableAllowlistWarnings,
});
});
it("falls back to setup doctor adapters when read-only plugins lack doctor hooks", () => {
const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: ["matrix"],
}));
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
},
],
});
mocks.getBundledChannelSetupPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
doctor: { normalizeCompatibilityConfig },
}
: undefined,
);
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
const normalizeCompatibilityConfig = createNormalizeCompatibilityConfig();
mockReadOnlyMatrixPlugin();
mockBundledMatrixSetupPlugin({ normalizeCompatibilityConfig });
const cfg = createMatrixEnabledConfig();
const result = collectChannelDoctorCompatibilityMutations(cfg as never);
expect(result).toHaveLength(1);
expect(normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(mocks.resolveReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(cfg, {
includePersistedAuthState: false,
});
expect(mocks.getLoadedChannelPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelSetupPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelPlugin).toHaveBeenCalledWith("matrix");
expectMatrixDoctorLookupCalls(cfg);
});
it("falls back to bundled runtime doctor adapters when setup adapters lack doctor hooks", () => {
const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({
config: cfg,
changes: ["matrix"],
}));
mocks.resolveReadOnlyChannelPluginsForConfig.mockReturnValue({
plugins: [
{
id: "matrix",
},
],
});
mocks.getBundledChannelSetupPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
}
: undefined,
);
mocks.getBundledChannelPlugin.mockImplementation((id: string) =>
id === "matrix"
? {
id: "matrix",
doctor: { normalizeCompatibilityConfig },
}
: undefined,
);
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
const normalizeCompatibilityConfig = createNormalizeCompatibilityConfig();
mockReadOnlyMatrixPlugin();
mockBundledMatrixSetupPlugin();
mockBundledMatrixRuntimePlugin({ normalizeCompatibilityConfig });
const cfg = createMatrixEnabledConfig();
const result = collectChannelDoctorCompatibilityMutations(cfg as never);
expect(result).toHaveLength(1);
expect(normalizeCompatibilityConfig).toHaveBeenCalledTimes(1);
expect(mocks.getLoadedChannelPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelSetupPlugin).toHaveBeenCalledWith("matrix");
expect(mocks.getBundledChannelPlugin).toHaveBeenCalledWith("matrix");
expectMatrixDoctorLookupCalls();
});
it("passes explicit env into read-only channel plugin discovery", () => {
const cfg = {
channels: {
matrix: {
enabled: true,
},
},
};
const cfg = createMatrixEnabledConfig();
const env = { OPENCLAW_HOME: "/tmp/openclaw-test-home" };
collectChannelDoctorCompatibilityMutations(cfg as never, { env });