Files
openclaw/src/config/schema.base.generated.test.ts
Eric Milgram, PhD 678a0ee944 fix(config): render transform-backed config schema inputs (#67328)
Generate the public config JSON Schema from accepted input shapes so transform-backed fields remain renderable in the Control UI. Keep transform output schemas representable with explicit string pipes, align analyzer metadata handling, and cover the generated schema plus browser-safe UI render shapes.

Co-authored-by: Altay <altay@hey.com>

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-27 00:36:13 +01:00

198 lines
6.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { SENSITIVE_URL_HINT_TAG } from "../shared/net/redact-sensitive-url.js";
import { computeBaseConfigSchemaResponse } from "./schema-base.js";
type TestJsonSchema = {
additionalProperties?: TestJsonSchema | boolean;
allOf?: TestJsonSchema[];
anyOf?: TestJsonSchema[];
const?: unknown;
enum?: unknown[];
items?: TestJsonSchema | TestJsonSchema[];
oneOf?: TestJsonSchema[];
properties?: Record<string, TestJsonSchema>;
type?: unknown;
};
const BASE_CONFIG_SCHEMA = computeBaseConfigSchemaResponse({
generatedAt: "2026-05-05T00:00:00.000Z",
});
const BASE_SCHEMA = BASE_CONFIG_SCHEMA.schema as TestJsonSchema;
const METADATA_KEYS = new Set(["default", "description", "nullable", "tags", "title", "x-tags"]);
function schemaAt(schema: TestJsonSchema, path: string[]): TestJsonSchema | undefined {
let node: TestJsonSchema | undefined = schema;
for (const segment of path) {
if (!node) {
return undefined;
}
if (segment === "[]") {
node = Array.isArray(node.items) ? node.items[0] : node.items;
} else {
node = node.properties?.[segment];
}
}
return node;
}
function sortedAnyOfTypes(node: TestJsonSchema | undefined): string[] {
return (node?.anyOf ?? [])
.map((branch) => String(branch.type))
.toSorted((left, right) => left.localeCompare(right));
}
function itemSchema(node: TestJsonSchema | undefined): TestJsonSchema | undefined {
return Array.isArray(node?.items) ? node.items[0] : node?.items;
}
function expectAnyOfTypes(path: string[], expectedTypes: string[]): TestJsonSchema[] {
const node = schemaAt(BASE_SCHEMA, path);
expect(node, path.join(".")).toBeDefined();
expect(sortedAnyOfTypes(node), path.join(".")).toEqual(expectedTypes);
return node?.anyOf ?? [];
}
function hasOnlyMetadataKeys(schema: TestJsonSchema): boolean {
return Object.keys(schema).every((key) => METADATA_KEYS.has(key));
}
function collectMetadataOnlyCompositionBranches(
schema: TestJsonSchema,
path: string[] = [],
hits: string[] = [],
): string[] {
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
for (const [index, branch] of (schema[keyword] ?? []).entries()) {
const branchPath = `${path.join(".") || "<root>"}.${keyword}[${index}]`;
if (hasOnlyMetadataKeys(branch)) {
hits.push(branchPath);
}
collectMetadataOnlyCompositionBranches(branch, [branchPath], hits);
}
}
for (const [key, child] of Object.entries(schema.properties ?? {})) {
collectMetadataOnlyCompositionBranches(child, [...path, key], hits);
}
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
collectMetadataOnlyCompositionBranches(schema.additionalProperties, [...path, "*"], hits);
}
const items = Array.isArray(schema.items) ? schema.items : schema.items ? [schema.items] : [];
for (const [index, child] of items.entries()) {
collectMetadataOnlyCompositionBranches(child, [...path, `items[${index}]`], hits);
}
return hits;
}
describe("base config schema", () => {
it("is deterministic for a fixed generatedAt timestamp", () => {
expect(
computeBaseConfigSchemaResponse({
generatedAt: BASE_CONFIG_SCHEMA.generatedAt,
}),
).toEqual(BASE_CONFIG_SCHEMA);
});
it("includes explicit URL-secret tags for sensitive URL fields", () => {
expect(BASE_CONFIG_SCHEMA.uiHints["mcp.servers.*.url"]?.tags).toContain(SENSITIVE_URL_HINT_TAG);
expect(BASE_CONFIG_SCHEMA.uiHints["models.providers.*.baseUrl"]?.tags).toContain(
SENSITIVE_URL_HINT_TAG,
);
});
it("omits legacy compatibility paths from the public schema payload", () => {
const rootProperties = (
BASE_CONFIG_SCHEMA.schema as {
properties?: Record<string, unknown>;
}
).properties;
const hooksInternalProperties = (
BASE_CONFIG_SCHEMA.schema as {
properties?: {
hooks?: {
properties?: {
internal?: {
properties?: Record<string, unknown>;
};
};
};
};
}
).properties?.hooks?.properties?.internal?.properties;
const uiHints = BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
expect(rootProperties?.canvasHost).toBeUndefined();
expect(hooksInternalProperties?.handlers).toBeUndefined();
expect(uiHints.canvasHost).toBeUndefined();
expect(uiHints["hooks.internal.handlers"]).toBeUndefined();
});
it("includes videoGenerationModel in the public schema payload", () => {
const agentDefaultsProperties = (
BASE_CONFIG_SCHEMA.schema as {
properties?: {
agents?: {
properties?: {
defaults?: {
properties?: Record<string, unknown>;
};
};
};
};
}
).properties?.agents?.properties?.defaults?.properties;
const uiHints = BASE_CONFIG_SCHEMA.uiHints as Record<string, unknown>;
expect(agentDefaultsProperties).toHaveProperty("videoGenerationModel");
expect(uiHints).toHaveProperty("agents.defaults.videoGenerationModel.primary");
expect(uiHints).toHaveProperty("agents.defaults.videoGenerationModel.fallbacks");
expect(uiHints).toHaveProperty("agents.defaults.mediaGenerationAutoProviderFallback");
});
it("publishes accepted input shapes for transform-backed config fields", () => {
const lastTouchedAtBranches = expectAnyOfTypes(["meta", "lastTouchedAt"], ["number", "string"]);
expect(lastTouchedAtBranches.every((branch) => Object.keys(branch).length > 0)).toBe(true);
for (const path of [
["agents", "defaults", "sandbox", "docker", "setupCommand"],
["agents", "list", "[]", "sandbox", "docker", "setupCommand"],
]) {
const branches = expectAnyOfTypes(path, ["array", "string"]);
expect(itemSchema(branches.find((branch) => branch.type === "array"))?.type).toBe("string");
}
const codexAllowedDomains = schemaAt(BASE_SCHEMA, [
"tools",
"web",
"search",
"openaiCodex",
"allowedDomains",
]);
expect(codexAllowedDomains?.type).toBe("array");
expect(itemSchema(codexAllowedDomains)?.type).toBe("string");
const codexUserLocation = schemaAt(BASE_SCHEMA, [
"tools",
"web",
"search",
"openaiCodex",
"userLocation",
]);
expect(codexUserLocation?.type).toBe("object");
expect(codexUserLocation?.properties?.country?.type).toBe("string");
expect(codexUserLocation?.properties?.region?.type).toBe("string");
expect(codexUserLocation?.properties?.city?.type).toBe("string");
expect(codexUserLocation?.properties?.timezone?.type).toBe("string");
expect(schemaAt(BASE_SCHEMA, ["gateway", "controlUi", "chatMessageMaxWidth"])?.type).toBe(
"string",
);
});
it("does not publish metadata-only composition branches", () => {
expect(collectMetadataOnlyCompositionBranches(BASE_SCHEMA)).toEqual([]);
});
});