mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 17:50:42 +00:00
Keep startup-derived plugin enablement, gateway auth tokens, control UI origins, and owner-display secrets runtime-only instead of persisting them into openclaw.json.
Refuse config writers, mutating update/plugin lifecycle commands, and doctor repair/token generation in Nix mode with agent-first nix-openclaw guidance.
Verification:
- pnpm check
- pnpm build
- pnpm test -- src/config/io.write-config.test.ts src/config/mutate.test.ts src/config/io.owner-display-secret.test.ts src/gateway/server-startup-config.recovery.test.ts src/gateway/startup-auth.test.ts src/gateway/startup-control-ui-origins.test.ts src/cli/plugins-cli.install.test.ts src/cli/plugins-cli.policy.test.ts src/cli/plugins-cli.uninstall.test.ts src/cli/plugins-cli.update.test.ts src/cli/update-cli.test.ts src/auto-reply/reply/commands-plugins.install.test.ts src/auto-reply/reply/commands-plugins.test.ts src/commands/onboarding-plugin-install.test.ts src/commands/doctor.runs-legacy-state-migrations-yes-mode-without.e2e.test.ts src/commands/doctor/shared/codex-route-warnings.test.ts src/commands/doctor/repair-sequencing.test.ts src/agents/auth-profile-runtime-contract.test.ts src/auto-reply/reply/agent-runner-execution.test.ts
- GitHub CI green on 05a2c71b90
Co-authored-by: Codex <noreply@openai.com>
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
buildPluginRegistrySnapshotReport,
|
|
enablePluginInConfig,
|
|
loadConfig,
|
|
refreshPluginRegistry,
|
|
resetPluginsCliTestState,
|
|
runtimeErrors,
|
|
runPluginsCommand,
|
|
writeConfigFile,
|
|
} from "./plugins-cli-test-helpers.js";
|
|
|
|
const ORIGINAL_OPENCLAW_NIX_MODE = process.env.OPENCLAW_NIX_MODE;
|
|
|
|
describe("plugins cli policy mutations", () => {
|
|
const compatibilityPluginIds = [
|
|
{ alias: "openai-codex", pluginId: "openai" },
|
|
{ alias: "google-gemini-cli", pluginId: "google" },
|
|
{ alias: "minimax-portal-auth", pluginId: "minimax" },
|
|
] as const;
|
|
|
|
beforeEach(() => {
|
|
resetPluginsCliTestState();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIGINAL_OPENCLAW_NIX_MODE === undefined) {
|
|
delete process.env.OPENCLAW_NIX_MODE;
|
|
} else {
|
|
process.env.OPENCLAW_NIX_MODE = ORIGINAL_OPENCLAW_NIX_MODE;
|
|
}
|
|
});
|
|
|
|
function mockPluginRegistry(ids: string[]) {
|
|
buildPluginRegistrySnapshotReport.mockReturnValue({
|
|
plugins: ids.map((id) => ({ id })),
|
|
diagnostics: [],
|
|
registrySource: "derived",
|
|
registryDiagnostics: [],
|
|
});
|
|
}
|
|
|
|
it("refreshes the persisted plugin registry after enabling a plugin", async () => {
|
|
const sourceConfig = {} as OpenClawConfig;
|
|
const enabledConfig = {
|
|
plugins: {
|
|
entries: {
|
|
alpha: { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
loadConfig.mockReturnValue(sourceConfig);
|
|
enablePluginInConfig.mockReturnValue({
|
|
config: enabledConfig,
|
|
enabled: true,
|
|
pluginId: "alpha",
|
|
});
|
|
mockPluginRegistry(["alpha"]);
|
|
|
|
await runPluginsCommand(["plugins", "enable", "alpha"]);
|
|
|
|
expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, "alpha", {
|
|
updateChannelConfig: false,
|
|
});
|
|
expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledWith({
|
|
config: enabledConfig,
|
|
installRecords: {},
|
|
policyPluginIds: ["alpha"],
|
|
reason: "policy-changed",
|
|
});
|
|
});
|
|
|
|
it("refuses plugin enablement in Nix mode before config mutation", async () => {
|
|
const previous = process.env.OPENCLAW_NIX_MODE;
|
|
process.env.OPENCLAW_NIX_MODE = "1";
|
|
try {
|
|
await expect(runPluginsCommand(["plugins", "enable", "alpha"])).rejects.toThrow(
|
|
"OPENCLAW_NIX_MODE=1",
|
|
);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.OPENCLAW_NIX_MODE;
|
|
} else {
|
|
process.env.OPENCLAW_NIX_MODE = previous;
|
|
}
|
|
}
|
|
|
|
expect(enablePluginInConfig).not.toHaveBeenCalled();
|
|
expect(writeConfigFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refreshes the persisted plugin registry after disabling a plugin", async () => {
|
|
loadConfig.mockReturnValue({
|
|
plugins: {
|
|
entries: {
|
|
alpha: { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig);
|
|
mockPluginRegistry(["alpha"]);
|
|
|
|
await runPluginsCommand(["plugins", "disable", "alpha"]);
|
|
|
|
const nextConfig = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig;
|
|
expect(nextConfig.plugins?.entries?.alpha?.enabled).toBe(false);
|
|
expect(refreshPluginRegistry).toHaveBeenCalledWith({
|
|
config: nextConfig,
|
|
installRecords: {},
|
|
policyPluginIds: ["alpha"],
|
|
reason: "policy-changed",
|
|
});
|
|
});
|
|
|
|
it.each(compatibilityPluginIds)(
|
|
"enables compatibility id $alias through canonical plugin $pluginId",
|
|
async ({ alias, pluginId }) => {
|
|
const sourceConfig = {} as OpenClawConfig;
|
|
const enabledConfig = {
|
|
plugins: {
|
|
entries: {
|
|
[pluginId]: { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
loadConfig.mockReturnValue(sourceConfig);
|
|
enablePluginInConfig.mockReturnValue({
|
|
config: enabledConfig,
|
|
enabled: true,
|
|
});
|
|
mockPluginRegistry([pluginId]);
|
|
|
|
await runPluginsCommand(["plugins", "enable", alias]);
|
|
|
|
expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, pluginId, {
|
|
updateChannelConfig: false,
|
|
});
|
|
expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig);
|
|
},
|
|
);
|
|
|
|
it.each(compatibilityPluginIds)(
|
|
"disables compatibility id $alias through canonical plugin $pluginId",
|
|
async ({ alias, pluginId }) => {
|
|
loadConfig.mockReturnValue({
|
|
plugins: {
|
|
entries: {
|
|
[pluginId]: { enabled: true },
|
|
},
|
|
},
|
|
} as OpenClawConfig);
|
|
mockPluginRegistry([pluginId]);
|
|
|
|
await runPluginsCommand(["plugins", "disable", alias]);
|
|
|
|
const nextConfig = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig;
|
|
expect(nextConfig.plugins?.entries?.[pluginId]?.enabled).toBe(false);
|
|
expect(nextConfig.plugins?.entries?.[alias]).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
it.each(["enable", "disable"] as const)(
|
|
"rejects %s for a plugin that is not discovered",
|
|
async (command) => {
|
|
mockPluginRegistry(["alpha"]);
|
|
|
|
await expect(runPluginsCommand(["plugins", command, "missing-plugin"])).rejects.toThrow(
|
|
"__exit__:1",
|
|
);
|
|
|
|
expect(runtimeErrors).toContain(
|
|
"Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins.",
|
|
);
|
|
expect(enablePluginInConfig).not.toHaveBeenCalled();
|
|
expect(writeConfigFile).not.toHaveBeenCalled();
|
|
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("does not create a channel config when disabling a channel plugin by policy", async () => {
|
|
loadConfig.mockReturnValue({} as OpenClawConfig);
|
|
mockPluginRegistry(["twitch"]);
|
|
|
|
await runPluginsCommand(["plugins", "disable", "twitch"]);
|
|
|
|
const nextConfig = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig;
|
|
expect(nextConfig.plugins?.entries?.twitch?.enabled).toBe(false);
|
|
expect(nextConfig.channels?.twitch).toBeUndefined();
|
|
});
|
|
});
|