test: dedupe config compatibility fixtures

This commit is contained in:
Peter Steinberger
2026-03-26 15:44:25 +00:00
parent 6bdf5e5634
commit 4ed5895637
3 changed files with 82 additions and 148 deletions

View File

@@ -74,6 +74,25 @@ async function writeManifestlessClaudeBundleFixture(params: { dir: string }) {
await fs.writeFile(path.join(params.dir, "settings.json"), '{"hideThinkingBlock":true}', "utf-8");
}
function expectRemovedPluginWarnings(
result: { ok: boolean; warnings?: Array<{ path: string; message: string }> },
removedId: string,
removedLabel: string,
) {
expect(result.ok).toBe(true);
if (result.ok) {
const message = `plugin removed: ${removedLabel} (stale config entry ignored; remove it from plugins config)`;
expect(result.warnings).toEqual(
expect.arrayContaining([
{ path: `plugins.entries.${removedId}`, message },
{ path: "plugins.allow", message },
{ path: "plugins.deny", message },
{ path: "plugins.slots.memory", message },
]),
);
}
}
describe("config plugin validation", () => {
let fixtureRoot = "";
let suiteHome = "";
@@ -99,6 +118,18 @@ describe("config plugin validation", () => {
const validateInSuite = (raw: unknown) =>
validateConfigObjectWithPlugins(raw, { env: suiteEnv() });
const validateRemovedPluginConfig = (removedId: string) =>
validateInSuite({
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
entries: { [removedId]: { enabled: true } },
allow: [removedId],
deny: [removedId],
slots: { memory: removedId },
},
});
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-plugin-validation-"));
await chmodSafeDir(fixtureRoot);
@@ -267,84 +298,14 @@ describe("config plugin validation", () => {
it("warns for removed legacy plugin ids instead of failing validation", async () => {
const removedId = "google-antigravity-auth";
const res = validateInSuite({
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
entries: { [removedId]: { enabled: true } },
allow: [removedId],
deny: [removedId],
slots: { memory: removedId },
},
});
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.warnings).toEqual(
expect.arrayContaining([
{
path: `plugins.entries.${removedId}`,
message:
"plugin removed: google-antigravity-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.allow",
message:
"plugin removed: google-antigravity-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.deny",
message:
"plugin removed: google-antigravity-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.slots.memory",
message:
"plugin removed: google-antigravity-auth (stale config entry ignored; remove it from plugins config)",
},
]),
);
}
const res = validateRemovedPluginConfig(removedId);
expectRemovedPluginWarnings(res, removedId, removedId);
});
it("warns for removed google gemini auth plugin ids instead of failing validation", async () => {
const removedId = "google-gemini-cli-auth";
const res = validateInSuite({
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: false,
entries: { [removedId]: { enabled: true } },
allow: [removedId],
deny: [removedId],
slots: { memory: removedId },
},
});
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.warnings).toEqual(
expect.arrayContaining([
{
path: `plugins.entries.${removedId}`,
message:
"plugin removed: google-gemini-cli-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.allow",
message:
"plugin removed: google-gemini-cli-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.deny",
message:
"plugin removed: google-gemini-cli-auth (stale config entry ignored; remove it from plugins config)",
},
{
path: "plugins.slots.memory",
message:
"plugin removed: google-gemini-cli-auth (stale config entry ignored; remove it from plugins config)",
},
]),
);
}
const res = validateRemovedPluginConfig(removedId);
expectRemovedPluginWarnings(res, removedId, removedId);
});
it("does not auto-allow config-loaded overrides of bundled web search plugin ids", async () => {

View File

@@ -35,6 +35,36 @@ function createIoForHome(home: string, env: NodeJS.ProcessEnv = {} as NodeJS.Pro
});
}
async function expectNoNewerVersionWarning(touchedVersion: string) {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify({ meta: { lastTouchedVersion: touchedVersion } }, null, 2),
);
const logger = {
warn: vi.fn(),
error: vi.fn(),
};
const io = createConfigIO({
env: {} as NodeJS.ProcessEnv,
homedir: () => home,
logger,
});
io.loadConfig();
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("Config was last written by a newer OpenClaw"),
);
expect(io.configPath).toBe(configPath);
});
}
describe("config io paths", () => {
it("uses ~/.openclaw/openclaw.json when config exists", async () => {
await withTempHome(async (home) => {
@@ -166,34 +196,7 @@ describe("config io paths", () => {
throw new Error(`Unable to parse VERSION: ${VERSION}`);
}
const touchedVersion = `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}-${(parsedVersion.revision ?? 0) + 1}`;
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify({ meta: { lastTouchedVersion: touchedVersion } }, null, 2),
);
const logger = {
warn: vi.fn(),
error: vi.fn(),
};
const io = createConfigIO({
env: {} as NodeJS.ProcessEnv,
homedir: () => home,
logger,
});
io.loadConfig();
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("Config was last written by a newer OpenClaw"),
);
expect(io.configPath).toBe(configPath);
});
await expectNoNewerVersionWarning(touchedVersion);
});
it("does not warn for same-base prerelease configs when current version is newer", async () => {
@@ -202,33 +205,6 @@ describe("config io paths", () => {
throw new Error(`Unable to parse VERSION: ${VERSION}`);
}
const touchedVersion = `${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}-beta.1`;
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify({ meta: { lastTouchedVersion: touchedVersion } }, null, 2),
);
const logger = {
warn: vi.fn(),
error: vi.fn(),
};
const io = createConfigIO({
env: {} as NodeJS.ProcessEnv,
homedir: () => home,
logger,
});
io.loadConfig();
expect(logger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("Config was last written by a newer OpenClaw"),
);
expect(io.configPath).toBe(configPath);
});
await expectNoNewerVersionWarning(touchedVersion);
});
});

View File

@@ -38,6 +38,19 @@ function makeSnapshot(params: { valid: boolean; config?: OpenClawConfig }): Conf
};
}
async function readSchemaNodes() {
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
const result = await readBestEffortRuntimeConfigSchema();
const schema = result.schema as { properties?: Record<string, unknown> };
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
const pluginProps = pluginsNode?.properties as Record<string, unknown> | undefined;
const entriesNode = pluginProps?.entries as Record<string, unknown> | undefined;
const entryProps = entriesNode?.properties as Record<string, unknown> | undefined;
return { channelProps, entryProps };
}
describe("readBestEffortRuntimeConfigSchema", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -90,15 +103,7 @@ describe("readBestEffortRuntimeConfigSchema", () => {
channelSetups: [],
});
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
const result = await readBestEffortRuntimeConfigSchema();
const schema = result.schema as { properties?: Record<string, unknown> };
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
const pluginProps = pluginsNode?.properties as Record<string, unknown> | undefined;
const entriesNode = pluginProps?.entries as Record<string, unknown> | undefined;
const entryProps = entriesNode?.properties as Record<string, unknown> | undefined;
const { channelProps, entryProps } = await readSchemaNodes();
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
@@ -157,15 +162,7 @@ describe("readBestEffortRuntimeConfigSchema", () => {
],
});
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
const result = await readBestEffortRuntimeConfigSchema();
const schema = result.schema as { properties?: Record<string, unknown> };
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
const pluginProps = pluginsNode?.properties as Record<string, unknown> | undefined;
const entriesNode = pluginProps?.entries as Record<string, unknown> | undefined;
const entryProps = entriesNode?.properties as Record<string, unknown> | undefined;
const { channelProps, entryProps } = await readSchemaNodes();
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({