Files
openclaw/src/config/validation.policy.test.ts
2026-04-11 13:55:08 +01:00

170 lines
4.9 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { validateConfigObjectRaw } from "./validation.js";
vi.mock("../channels/plugins/legacy-config.js", () => ({
collectChannelLegacyConfigRules: () => [],
}));
vi.mock("../plugins/doctor-contract-registry.js", () => ({
collectRelevantDoctorPluginIds: () => [],
listPluginDoctorLegacyConfigRules: () => [],
}));
vi.mock("../secrets/unsupported-surface-policy.js", async () => {
const { isRecord } = await import("../utils.js");
return {
collectUnsupportedSecretRefConfigCandidates: (raw: unknown) => {
if (!isRecord(raw)) {
return [];
}
const candidates: Array<{ path: string; value: unknown }> = [];
const hooks = isRecord(raw.hooks) ? raw.hooks : null;
if (hooks) {
candidates.push({ path: "hooks.token", value: hooks.token });
}
const channels = isRecord(raw.channels) ? raw.channels : null;
const discord = channels && isRecord(channels.discord) ? channels.discord : null;
const threadBindings =
discord && isRecord(discord.threadBindings) ? discord.threadBindings : null;
if (threadBindings) {
candidates.push({
path: "channels.discord.threadBindings.webhookToken",
value: threadBindings.webhookToken,
});
}
return candidates;
},
};
});
describe("config validation SecretRef policy guards", () => {
it("surfaces a policy error for hooks.token SecretRef objects", () => {
const result = validateConfigObjectRaw({
hooks: {
token: {
source: "env",
provider: "default",
id: "HOOK_TOKEN",
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
const issue = result.issues.find((entry) => entry.path === "hooks.token");
expect(issue).toBeDefined();
expect(issue?.message).toContain("SecretRef objects are not supported at hooks.token");
expect(issue?.message).toContain(
"https://docs.openclaw.ai/reference/secretref-credential-surface",
);
expect(
result.issues.some(
(entry) =>
entry.path === "hooks.token" &&
entry.message.includes("Invalid input: expected string, received object"),
),
).toBe(false);
}
});
it("keeps standard schema errors for non-SecretRef objects", () => {
const result = validateConfigObjectRaw({
hooks: {
token: {
unexpected: "value",
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
const issue = result.issues.find((entry) => entry.path === "hooks.token");
expect(issue).toBeDefined();
expect(issue?.message).toBe("Invalid input: expected string, received object");
}
});
it("allows env-template strings on unsupported mutable paths", () => {
const result = validateConfigObjectRaw({
hooks: {
token: "${HOOK_TOKEN}",
},
});
expect(result.ok).toBe(true);
});
it("replaces derived unrecognized-key errors with policy guidance for discord thread binding webhookToken", () => {
const result = validateConfigObjectRaw({
channels: {
discord: {
threadBindings: {
webhookToken: {
source: "env",
provider: "default",
id: "DISCORD_THREAD_BINDING_WEBHOOK_TOKEN",
},
},
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
const policyIssue = result.issues.find(
(entry) => entry.path === "channels.discord.threadBindings.webhookToken",
);
expect(policyIssue).toBeDefined();
expect(policyIssue?.message).toContain(
"SecretRef objects are not supported at channels.discord.threadBindings.webhookToken",
);
expect(
result.issues.some(
(entry) =>
entry.path === "channels.discord.threadBindings" &&
entry.message.includes('Unrecognized key: "webhookToken"'),
),
).toBe(false);
}
});
it("preserves unrelated unknown-key errors when policy and typos coexist", () => {
const result = validateConfigObjectRaw({
channels: {
discord: {
threadBindings: {
webhookToken: {
source: "env",
provider: "default",
id: "DISCORD_THREAD_BINDING_WEBHOOK_TOKEN",
},
webhookTokne: "typo",
},
},
},
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(
result.issues.some(
(entry) =>
entry.path === "channels.discord.threadBindings.webhookToken" &&
entry.message.includes("SecretRef objects are not supported"),
),
).toBe(true);
expect(
result.issues.some(
(entry) =>
entry.path === "channels.discord.threadBindings" &&
entry.message.includes("webhookTokne"),
),
).toBe(true);
}
});
});