import type { ConfigValidationIssue } from "../../schema/issues"; import type { CheckerValidationInput } from "../types"; import { validateOperatorObject } from "../../expect/validate-operator"; import { issue, joinPath } from "../../schema/issues"; import { parseSize } from "../../utils"; export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; const defaults = isRecord(input.defaults) && isRecord(input.defaults["cmd"]) ? input.defaults["cmd"] : undefined; if (isSizeInput(defaults?.["maxOutputBytes"])) { issues.push(...validateSizeValue(defaults["maxOutputBytes"], "defaults.cmd.maxOutputBytes")); } for (let i = 0; i < input.targets.length; i++) { const target = input.targets[i] as unknown; if (!isRecord(target)) continue; if (target["type"] !== "cmd") continue; issues.push(...validateCommandTarget(target, `targets[${i}]`)); } return issues; } function getTargetName(target: Record): string | undefined { return typeof target["name"] === "string" ? target["name"] : undefined; } function isNonNegativeFiniteNumber(value: unknown): boolean { return typeof value === "number" && Number.isFinite(value) && value >= 0; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isSizeInput(value: unknown): value is number | string { return typeof value === "number" || typeof value === "string"; } function validateCommandExpect(target: Record, path: string): ConfigValidationIssue[] { const targetName = getTargetName(target); const expect = target["expect"]; if (expect === undefined || expect === null || !isRecord(expect)) return []; const issues: ConfigValidationIssue[] = []; const expectPath = joinPath(path, "expect"); if (expect["stdout"] !== undefined) { issues.push(...validateTextRules(expect["stdout"], joinPath(expectPath, "stdout"), targetName)); } if (expect["stderr"] !== undefined) { issues.push(...validateTextRules(expect["stderr"], joinPath(expectPath, "stderr"), targetName)); } if (expect["maxDurationMs"] !== undefined && !isNonNegativeFiniteNumber(expect["maxDurationMs"])) { issues.push(issue("invalid-type", joinPath(expectPath, "maxDurationMs"), "必须为非负有限数字", targetName)); } return issues; } function validateCommandTarget(target: Record, path: string): ConfigValidationIssue[] { const issues: ConfigValidationIssue[] = []; const targetName = getTargetName(target); const cmd = target["cmd"]; if (!isRecord(cmd)) { issues.push(issue("required", joinPath(path, "cmd"), "缺少 cmd.exec 字段", targetName)); issues.push(...validateCommandExpect(target, path)); return issues; } if (typeof cmd["exec"] !== "string" || cmd["exec"].trim() === "") { issues.push(issue("required", joinPath(joinPath(path, "cmd"), "exec"), "缺少 cmd.exec 字段", targetName)); } if (isSizeInput(cmd["maxOutputBytes"])) { issues.push( ...validateSizeValue(cmd["maxOutputBytes"], joinPath(joinPath(path, "cmd"), "maxOutputBytes"), targetName), ); } issues.push(...validateCommandExpect(target, path)); return issues; } function validateSizeValue(value: number | string, path: string, targetName?: string): ConfigValidationIssue[] { try { parseSize(value); return []; } catch (error) { return [issue("invalid-size", path, error instanceof Error ? error.message : "size 格式不合法", targetName)]; } } function validateTextRules(rules: unknown, path: string, targetName?: string): ConfigValidationIssue[] { if (!Array.isArray(rules)) return [issue("invalid-type", path, "必须为数组", targetName)]; return rules.flatMap((rule, index) => validateOperatorObject(rule, `${path}[${index}]`, targetName)); }