Files
openclaw/src/config/validation.policy.test.ts
Josh Avant 788f56f30f Secrets: hard-fail unsupported SecretRef policy and fix gateway restart token drift (#58141)
* Secrets: enforce C2 SecretRef policy and drift resolution

* Tests: add gateway auth startup/reload SecretRef runtime coverage

* Docs: sync C2 SecretRef policy and coverage matrix

* Config: hard-fail parent SecretRef policy writes

* Secrets: centralize unsupported SecretRef policy metadata

* Daemon: test service-env precedence for token drift refs

* Config: keep per-ref dry-run resolvability errors

* Docs: clarify config-set parent-object policy checks

* Gateway: fix drift fallback and schema-key filtering

* Gateway: align drift fallback with credential planner

* changelog

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>

---------

Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-03-31 02:37:31 -05:00

130 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { validateConfigObjectRaw } from "./validation.js";
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);
}
});
});