mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
* fix(cli): narrow config hint branch * Plan config hints from actual changes * Plan direct unset hints from actual changes * Expand broad unset hint paths * fix(cli): respect reload mode in config hints --------- Co-authored-by: Kiran Magic <kiran@Alices-Laptop.local> Co-authored-by: kiranmagic7 <262980978+kiranmagic7@users.noreply.github.com>
This commit is contained in:
@@ -52,6 +52,43 @@ vi.mock("../config/runtime-schema.js", () => ({
|
||||
readBestEffortRuntimeConfigSchema: () => mockReadBestEffortRuntimeConfigSchema(),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/config-reload-plan.js", () => ({
|
||||
buildGatewayReloadPlan: (changedPaths: string[]) => {
|
||||
const restartReasons = changedPaths.filter(
|
||||
(changedPath) =>
|
||||
changedPath.startsWith("models.pricing.") || changedPath.startsWith("plugins.load."),
|
||||
);
|
||||
const hotReasons = changedPaths.filter(
|
||||
(changedPath) =>
|
||||
!restartReasons.includes(changedPath) &&
|
||||
(changedPath.startsWith("agents.list.") ||
|
||||
changedPath.startsWith("agents.defaults.models.") ||
|
||||
changedPath.startsWith("models.") ||
|
||||
changedPath.startsWith("plugins.")),
|
||||
);
|
||||
restartReasons.push(
|
||||
...changedPaths.filter(
|
||||
(changedPath) => !hotReasons.includes(changedPath) && !restartReasons.includes(changedPath),
|
||||
),
|
||||
);
|
||||
return {
|
||||
changedPaths,
|
||||
restartGateway: restartReasons.length > 0,
|
||||
restartReasons,
|
||||
hotReasons,
|
||||
reloadHooks: false,
|
||||
restartGmailWatcher: false,
|
||||
restartCron: false,
|
||||
restartHeartbeat: hotReasons.length > 0,
|
||||
restartHealthMonitor: false,
|
||||
reloadPlugins: false,
|
||||
restartChannels: new Set(),
|
||||
disposeMcpRuntimes: false,
|
||||
noopPaths: [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/plugin-metadata-snapshot.js")>();
|
||||
return {
|
||||
@@ -3425,6 +3462,291 @@ describe("config cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("config apply hints - issue #80722", () => {
|
||||
it("prints a hot-reload hint for agents.list model changes", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main" },
|
||||
{ id: "mason-vale", model: { primary: "ollama/qwen3-coder-next" } },
|
||||
],
|
||||
},
|
||||
};
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"agents.list[1].model.primary",
|
||||
'"ollama/kimi-k2.6"',
|
||||
"--strict-json",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated agents.list.1.model.primary");
|
||||
expectLogIncludes("Change will apply without restarting the gateway.");
|
||||
expectLogExcludes("Restart the gateway to apply.");
|
||||
});
|
||||
|
||||
it("does not treat legacy per-agent agentRuntime as restart-required", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex-legacy",
|
||||
agentRuntime: { id: "codex" },
|
||||
model: { primary: "openai/gpt-5.5" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"agents.list[0].model.primary",
|
||||
'"openai/gpt-5.4-mini"',
|
||||
"--strict-json",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Change will apply without restarting the gateway.");
|
||||
expectLogExcludes("Restart the gateway to apply.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for hot-path edits when reload mode is off", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [{ id: "main", model: { primary: "openai/gpt-5.4" } }],
|
||||
},
|
||||
gateway: {
|
||||
reload: { mode: "off" },
|
||||
},
|
||||
};
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"agents.list[0].model.primary",
|
||||
'"openai/gpt-5.5"',
|
||||
"--strict-json",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated agents.list.0.model.primary");
|
||||
expectLogIncludes("Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for hot-path edits when reload mode is restart", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [{ id: "main", model: { primary: "openai/gpt-5.4" } }],
|
||||
},
|
||||
gateway: {
|
||||
reload: { mode: "restart" },
|
||||
},
|
||||
};
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"agents.list[0].model.primary",
|
||||
'"openai/gpt-5.5"',
|
||||
"--strict-json",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated agents.list.0.model.primary");
|
||||
expectLogIncludes("Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("prints a hot-reload hint when removing legacy per-agent agentRuntime", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex-legacy",
|
||||
agentRuntime: { id: "codex" },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand(["config", "unset", "agents.list[0].agentRuntime"]);
|
||||
|
||||
expectLogIncludes("Removed agents.list[0].agentRuntime");
|
||||
expectLogIncludes("Change will apply without restarting the gateway.");
|
||||
expectLogExcludes("Restart the gateway to apply.");
|
||||
});
|
||||
|
||||
it("prints a hot-reload hint for provider runtime policy changes", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"models.providers.openai.agentRuntime.id",
|
||||
'"pi"',
|
||||
"--strict-json",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated models.providers.openai.agentRuntime.id");
|
||||
expectLogIncludes("Change will apply without restarting the gateway.");
|
||||
expectLogExcludes("Restart the gateway to apply.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for broad models writes that change pricing bootstrap", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
models: {
|
||||
pricing: {
|
||||
enabled: false,
|
||||
},
|
||||
providers: {
|
||||
openai: {
|
||||
agentRuntime: { id: "node" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"models",
|
||||
'{"pricing":{"enabled":true},"providers":{"openai":{"agentRuntime":{"id":"node"}}}}',
|
||||
"--strict-json",
|
||||
"--replace",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated models. Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for broad plugins writes that change load paths", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
plugins: {
|
||||
load: {
|
||||
paths: ["/tmp/openclaw-plugins-a"],
|
||||
},
|
||||
entries: {
|
||||
canvas: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"plugins",
|
||||
'{"load":{"paths":["/tmp/openclaw-plugins-b"]},"entries":{"canvas":{"enabled":true}}}',
|
||||
"--strict-json",
|
||||
"--replace",
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated plugins. Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for broad models unsets that remove pricing bootstrap", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
models: {
|
||||
pricing: {
|
||||
enabled: false,
|
||||
},
|
||||
providers: {
|
||||
openai: {
|
||||
agentRuntime: { id: "node" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand(["config", "unset", "models"]);
|
||||
|
||||
expectLogIncludes("Removed models. Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for broad plugins unsets that remove load paths", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
plugins: {
|
||||
load: {
|
||||
paths: ["/tmp/openclaw-plugins-a"],
|
||||
},
|
||||
entries: {
|
||||
canvas: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand(["config", "unset", "plugins"]);
|
||||
|
||||
expectLogIncludes("Removed plugins. Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for restart-required config paths", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: { list: [{ id: "main" }] },
|
||||
gateway: { port: 18789 },
|
||||
};
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand(["config", "set", "gateway.auth.mode", "token"]);
|
||||
|
||||
expectLogIncludes("Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps plugin entry config writes restart-backed when reload metadata is absent", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
canvas: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
setSnapshot(resolved, resolved);
|
||||
|
||||
await runConfigCommand(["config", "set", "plugins.entries.canvas.enabled", "false"]);
|
||||
|
||||
expectLogIncludes("Updated plugins.entries.canvas.enabled");
|
||||
expectLogIncludes("Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
|
||||
it("keeps the restart hint for mixed hot and restart batch updates", async () => {
|
||||
const resolved: OpenClawConfig = {
|
||||
agents: { list: [{ id: "main", model: { primary: "openai/gpt-5.4" } }] },
|
||||
gateway: { port: 18789 },
|
||||
};
|
||||
setSnapshot(resolved, withRuntimeDefaults(resolved));
|
||||
|
||||
await runConfigCommand([
|
||||
"config",
|
||||
"set",
|
||||
"--batch-json",
|
||||
'[{"path":"agents.list[0].model.primary","value":"openai/gpt-5.5"},{"path":"gateway.auth.mode","value":"token"}]',
|
||||
]);
|
||||
|
||||
expectLogIncludes("Updated 2 config paths. Restart the gateway to apply.");
|
||||
expectLogExcludes("Change will apply without restarting the gateway.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("config file", () => {
|
||||
it("prints the active config file path", async () => {
|
||||
const resolved: OpenClawConfig = { gateway: { port: 18789 } };
|
||||
|
||||
+129
-7
@@ -23,7 +23,6 @@ import {
|
||||
normalizeAgentModelRefForConfig,
|
||||
} from "../config/model-input.js";
|
||||
import { CONFIG_PATH } from "../config/paths.js";
|
||||
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
||||
import { isPluginPackagingRuntimeOutputInvalidConfigSnapshot } from "../config/recovery-policy.js";
|
||||
import { redactConfigObject } from "../config/redact-snapshot.js";
|
||||
import { readBestEffortRuntimeConfigSchema } from "../config/runtime-schema.js";
|
||||
@@ -42,8 +41,12 @@ import {
|
||||
validateConfigObjectRawWithPlugins,
|
||||
} from "../config/validation.js";
|
||||
import { SecretProviderSchema } from "../config/zod-schema.core.js";
|
||||
import { diffConfigPaths } from "../gateway/config-diff.js";
|
||||
import { buildGatewayReloadPlan } from "../gateway/config-reload-plan.js";
|
||||
import { resolveGatewayReloadSettings } from "../gateway/config-reload-settings.js";
|
||||
import { danger, info, success, warn } from "../globals.js";
|
||||
import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
|
||||
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
||||
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -1001,6 +1004,114 @@ function toDotPath(path: PathSegment[]): string {
|
||||
return path.join(".");
|
||||
}
|
||||
|
||||
const RESTART_HINT = "Restart the gateway to apply.";
|
||||
const HOT_RELOAD_HINT = "Change will apply without restarting the gateway.";
|
||||
const NO_RELOAD_HINT = "No gateway restart needed.";
|
||||
|
||||
function isPluginEntryConfigPath(path: string): boolean {
|
||||
// CLI hints are operator guidance. Keep plugin entry writes conservative
|
||||
// because the CLI cannot prove every plugin's reload metadata is loaded.
|
||||
return path === "plugins.entries" || path.startsWith("plugins.entries.");
|
||||
}
|
||||
|
||||
function configApplyHintForPaths(paths: string[], afterConfig: OpenClawConfig): string {
|
||||
if (paths.length === 0) {
|
||||
return RESTART_HINT;
|
||||
}
|
||||
if (paths.some(isPluginEntryConfigPath)) {
|
||||
return RESTART_HINT;
|
||||
}
|
||||
const plan = buildGatewayReloadPlan(paths);
|
||||
if (plan.restartGateway) {
|
||||
return RESTART_HINT;
|
||||
}
|
||||
if (plan.hotReasons.length > 0) {
|
||||
const { mode } = resolveGatewayReloadSettings(afterConfig);
|
||||
if (mode === "off" || mode === "restart") {
|
||||
return RESTART_HINT;
|
||||
}
|
||||
return HOT_RELOAD_HINT;
|
||||
}
|
||||
return NO_RELOAD_HINT;
|
||||
}
|
||||
|
||||
function configApplyHintForOperations(
|
||||
operations: ReadonlyArray<{ requestedPath?: PathSegment[] }>,
|
||||
beforeConfig: OpenClawConfig,
|
||||
afterConfig: OpenClawConfig,
|
||||
): string {
|
||||
const requestedPaths: string[] = [];
|
||||
for (const operation of operations) {
|
||||
if (!operation.requestedPath) {
|
||||
return RESTART_HINT;
|
||||
}
|
||||
requestedPaths.push(toDotPath(operation.requestedPath));
|
||||
}
|
||||
return configApplyHintForPaths(
|
||||
expandActualChangedPathsWithRequestedDescendants(
|
||||
diffConfigPaths(beforeConfig, afterConfig),
|
||||
requestedPaths,
|
||||
beforeConfig,
|
||||
afterConfig,
|
||||
),
|
||||
afterConfig,
|
||||
);
|
||||
}
|
||||
|
||||
function expandActualChangedPathsWithRequestedDescendants(
|
||||
actualChangedPaths: string[],
|
||||
requestedPaths: string[],
|
||||
beforeConfig: OpenClawConfig,
|
||||
afterConfig: OpenClawConfig,
|
||||
): string[] {
|
||||
const expanded = new Set<string>();
|
||||
for (const actualPath of actualChangedPaths) {
|
||||
const requestedDescendants = requestedPaths.filter(
|
||||
(requestedPath) => requestedPath !== actualPath && requestedPath.startsWith(`${actualPath}.`),
|
||||
);
|
||||
if (requestedDescendants.length > 0) {
|
||||
for (const requestedPath of requestedDescendants) {
|
||||
expanded.add(requestedPath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const expandedPath of expandWholeValueChangePath(actualPath, beforeConfig, afterConfig)) {
|
||||
expanded.add(expandedPath);
|
||||
}
|
||||
}
|
||||
return [...expanded];
|
||||
}
|
||||
|
||||
function expandWholeValueChangePath(
|
||||
actualPath: string,
|
||||
beforeConfig: OpenClawConfig,
|
||||
afterConfig: OpenClawConfig,
|
||||
): string[] {
|
||||
const path = actualPath === "<root>" ? [] : actualPath.split(".");
|
||||
const before = getAtPath(beforeConfig, path);
|
||||
const after = getAtPath(afterConfig, path);
|
||||
if (before.found && !after.found) {
|
||||
return collectChangedLeafPaths(before.value, actualPath);
|
||||
}
|
||||
if (!before.found && after.found) {
|
||||
return collectChangedLeafPaths(after.value, actualPath);
|
||||
}
|
||||
return [actualPath];
|
||||
}
|
||||
|
||||
function collectChangedLeafPaths(value: unknown, prefix: string): string[] {
|
||||
if (!isPlainRecord(value)) {
|
||||
return [prefix];
|
||||
}
|
||||
const entries = Object.entries(value);
|
||||
if (entries.length === 0) {
|
||||
return [prefix];
|
||||
}
|
||||
return entries.flatMap(([key, child]) =>
|
||||
collectChangedLeafPaths(child, prefix ? `${prefix}.${key}` : key),
|
||||
);
|
||||
}
|
||||
|
||||
function parseSecretRefSource(raw: string, label: string): SecretRefSource {
|
||||
const source = raw.trim();
|
||||
if (source === "env" || source === "file" || source === "exec") {
|
||||
@@ -2011,6 +2122,9 @@ async function runConfigOperations(params: {
|
||||
// instead of snapshot.config (runtime-merged with defaults).
|
||||
// This prevents runtime defaults from leaking into the written config file (issue #6070)
|
||||
const next = structuredClone(snapshot.resolved) as Record<string, unknown>;
|
||||
const currentConfigForApplyHint = normalizeConfigMutationModelRefs(
|
||||
structuredClone(snapshot.resolved) as OpenClawConfig,
|
||||
);
|
||||
const mutationSchema = await loadConfigMutationSchema();
|
||||
const unsetPaths: PathSegment[][] = [];
|
||||
const explicitSetPaths: PathSegment[][] = [];
|
||||
@@ -2197,16 +2311,16 @@ async function runConfigOperations(params: {
|
||||
if (params.successMode === "set" && operations.length === 1) {
|
||||
const operation = operations[0];
|
||||
const action = operation?.mutation === "delete" ? "Removed" : "Updated";
|
||||
runtime.log(
|
||||
info(`${action} ${toDotPath(operation?.requestedPath ?? [])}. Restart the gateway to apply.`),
|
||||
);
|
||||
const hint = configApplyHintForOperations(operations, currentConfigForApplyHint, nextConfig);
|
||||
runtime.log(info(`${action} ${toDotPath(operation?.requestedPath ?? [])}. ${hint}`));
|
||||
return;
|
||||
}
|
||||
const hint = configApplyHintForOperations(operations, currentConfigForApplyHint, nextConfig);
|
||||
if (params.successMode === "set") {
|
||||
runtime.log(info(`Updated ${operations.length} config paths. Restart the gateway to apply.`));
|
||||
runtime.log(info(`Updated ${operations.length} config paths. ${hint}`));
|
||||
return;
|
||||
}
|
||||
runtime.log(info(`Applied ${operations.length} config update(s). Restart the gateway to apply.`));
|
||||
runtime.log(info(`Applied ${operations.length} config update(s). ${hint}`));
|
||||
}
|
||||
|
||||
function handleConfigMutationError(params: {
|
||||
@@ -2367,6 +2481,9 @@ export async function runConfigUnset(opts: {
|
||||
// instead of snapshot.config (runtime-merged with defaults).
|
||||
// This prevents runtime defaults from leaking into the written config file (issue #6070)
|
||||
const next = structuredClone(snapshot.resolved) as Record<string, unknown>;
|
||||
const currentConfigForApplyHint = normalizeConfigMutationModelRefs(
|
||||
structuredClone(snapshot.resolved) as OpenClawConfig,
|
||||
);
|
||||
const unsetResult = unsetAtPath(next, parsedPath);
|
||||
if (!unsetResult.removed) {
|
||||
if (cliOptions.dryRun && cliOptions.json) {
|
||||
@@ -2414,7 +2531,12 @@ export async function runConfigUnset(opts: {
|
||||
? {}
|
||||
: { writeOptions: { unsetPaths: [parsedPath] } }),
|
||||
});
|
||||
runtime.log(info(`Removed ${opts.path}. Restart the gateway to apply.`));
|
||||
const hint = configApplyHintForOperations(
|
||||
[buildUnsetOperation(parsedPath)],
|
||||
currentConfigForApplyHint,
|
||||
normalizeConfigMutationModelRefs(structuredClone(next) as OpenClawConfig),
|
||||
);
|
||||
runtime.log(info(`Removed ${opts.path}. ${hint}`));
|
||||
} catch (err) {
|
||||
handleConfigMutationError({ err, runtime, options: cliOptions });
|
||||
}
|
||||
|
||||
@@ -175,6 +175,12 @@ describe("buildGatewayReloadPlan", () => {
|
||||
registration: { restartPrefixes: ["browser"] },
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "canvas",
|
||||
pluginName: "Canvas",
|
||||
registration: { restartPrefixes: ["plugins.entries.canvas"] },
|
||||
source: "test",
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -374,6 +380,14 @@ describe("buildGatewayReloadPlan", () => {
|
||||
expect(plan.hotReasons).toContain("plugins.entries.lossless-claw.config.mode");
|
||||
});
|
||||
|
||||
it("keeps restart-owned plugin entry config changes restart-backed", () => {
|
||||
const plan = buildGatewayReloadPlan(["plugins.entries.canvas.enabled"]);
|
||||
|
||||
expect(plan.restartGateway).toBe(true);
|
||||
expect(plan.restartReasons).toEqual(["plugins.entries.canvas.enabled"]);
|
||||
expect(plan.hotReasons).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("lists plugin install metadata and whole-record paths structurally", () => {
|
||||
const prev = {
|
||||
plugins: {
|
||||
|
||||
Reference in New Issue
Block a user