openclaw/src/plugins/schema-validator.test.ts

212 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { validateJsonSchemaValue } from "./schema-validator.js";
describe("schema validator", () => {
it("includes allowed values in enum validation errors", () => {
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.enum",
schema: {
type: "object",
properties: {
fileFormat: {
type: "string",
enum: ["markdown", "html", "json"],
},
},
required: ["fileFormat"],
},
value: { fileFormat: "txt" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "fileFormat");
expect(issue?.message).toContain("(allowed:");
expect(issue?.allowedValues).toEqual(["markdown", "html", "json"]);
expect(issue?.allowedValuesHiddenCount).toBe(0);
}
});
it("includes allowed value in const validation errors", () => {
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.const",
schema: {
type: "object",
properties: {
mode: {
const: "strict",
},
},
required: ["mode"],
},
value: { mode: "relaxed" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "mode");
expect(issue?.message).toContain("(allowed:");
expect(issue?.allowedValues).toEqual(["strict"]);
expect(issue?.allowedValuesHiddenCount).toBe(0);
}
});
it("truncates long allowed-value hints", () => {
const values = [
"v1",
"v2",
"v3",
"v4",
"v5",
"v6",
"v7",
"v8",
"v9",
"v10",
"v11",
"v12",
"v13",
];
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.enum.truncate",
schema: {
type: "object",
properties: {
mode: {
type: "string",
enum: values,
},
},
required: ["mode"],
},
value: { mode: "not-listed" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "mode");
expect(issue?.message).toContain("(allowed:");
expect(issue?.message).toContain("... (+1 more)");
expect(issue?.allowedValues).toEqual([
"v1",
"v2",
"v3",
"v4",
"v5",
"v6",
"v7",
"v8",
"v9",
"v10",
"v11",
"v12",
]);
expect(issue?.allowedValuesHiddenCount).toBe(1);
}
});
it("appends missing required property to the structured path", () => {
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.required.path",
schema: {
type: "object",
properties: {
settings: {
type: "object",
properties: {
mode: { type: "string" },
},
required: ["mode"],
},
},
required: ["settings"],
},
value: { settings: {} },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "settings.mode");
expect(issue).toBeDefined();
expect(issue?.allowedValues).toBeUndefined();
}
});
it("appends missing dependency property to the structured path", () => {
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.dependencies.path",
schema: {
type: "object",
properties: {
settings: {
type: "object",
dependencies: {
mode: ["format"],
},
},
},
},
value: { settings: { mode: "strict" } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "settings.format");
expect(issue).toBeDefined();
expect(issue?.allowedValues).toBeUndefined();
}
});
it("truncates oversized allowed value entries", () => {
const oversizedAllowed = "a".repeat(300);
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.enum.long-value",
schema: {
type: "object",
properties: {
mode: {
type: "string",
enum: [oversizedAllowed],
},
},
required: ["mode"],
},
value: { mode: "not-listed" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors.find((entry) => entry.path === "mode");
expect(issue).toBeDefined();
expect(issue?.message).toContain("(allowed:");
expect(issue?.message).toContain("... (+");
}
});
it("sanitizes terminal text while preserving structured fields", () => {
const maliciousProperty = "evil\nkey\t\x1b[31mred\x1b[0m";
const res = validateJsonSchemaValue({
cacheKey: "schema-validator.test.terminal-sanitize",
schema: {
type: "object",
properties: {},
required: [maliciousProperty],
},
value: {},
});
expect(res.ok).toBe(false);
if (!res.ok) {
const issue = res.errors[0];
expect(issue).toBeDefined();
expect(issue?.path).toContain("\n");
expect(issue?.message).toContain("\n");
expect(issue?.text).toContain("\\n");
expect(issue?.text).toContain("\\t");
expect(issue?.text).not.toContain("\n");
expect(issue?.text).not.toContain("\t");
expect(issue?.text).not.toContain("\x1b");
}
});
});