mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 03:17:50 +02:00
170 lines
4.9 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|