feat(config-builder): add draft validation utilities

This commit is contained in:
Sebastian
2026-02-09 21:22:57 -05:00
parent dea5ad9c6a
commit 451439e4ff
2 changed files with 107 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { validateConfigDraft } from "./validation.ts";
describe("validateConfigDraft", () => {
it("accepts empty drafts", () => {
const result = validateConfigDraft({});
expect(result.valid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it("collects issues by path and section", () => {
const result = validateConfigDraft({
gateway: {
auth: {
token: 123,
},
},
});
expect(result.valid).toBe(false);
expect(result.issues.length).toBeGreaterThan(0);
expect(result.issuesByPath["gateway.auth.token"]?.length).toBeGreaterThan(0);
expect(result.sectionErrorCounts.gateway).toBeGreaterThan(0);
});
it("tracks root-level schema issues", () => {
const result = validateConfigDraft({
__unexpected__: true,
});
expect(result.valid).toBe(false);
expect(result.sectionErrorCounts.root).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,73 @@
import { OpenClawSchema } from "@openclaw/config/zod-schema.ts";
import type { ConfigDraft } from "./config-store.ts";
export type ValidationIssue = {
path: string;
section: string;
message: string;
};
export type ValidationResult = {
valid: boolean;
issues: ValidationIssue[];
issuesByPath: Record<string, string[]>;
sectionErrorCounts: Record<string, number>;
};
function issuePath(path: Array<string | number>): string {
if (path.length === 0) {
return "";
}
return path
.map((segment) => (typeof segment === "number" ? String(segment) : segment))
.join(".");
}
function issueSection(path: string): string {
if (!path) {
return "root";
}
const [section] = path.split(".");
return section?.trim() || "root";
}
export function validateConfigDraft(config: ConfigDraft): ValidationResult {
const parsed = OpenClawSchema.safeParse(config);
if (parsed.success) {
return {
valid: true,
issues: [],
issuesByPath: {},
sectionErrorCounts: {},
};
}
const issues: ValidationIssue[] = parsed.error.issues.map((issue) => {
const path = issuePath(issue.path);
return {
path,
section: issueSection(path),
message: issue.message,
};
});
const issuesByPath: Record<string, string[]> = {};
const sectionErrorCounts: Record<string, number> = {};
for (const issue of issues) {
const key = issue.path;
if (!issuesByPath[key]) {
issuesByPath[key] = [];
}
issuesByPath[key].push(issue.message);
sectionErrorCounts[issue.section] = (sectionErrorCounts[issue.section] ?? 0) + 1;
}
return {
valid: false,
issues,
issuesByPath,
sectionErrorCounts,
};
}