UNPKG

windmill-yaml-validator

Version:

YAML validator for Windmill flow, schedule, and trigger files

290 lines (289 loc) 17.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const yaml_validator_1 = require("../yaml-validator"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); describe("WindmillYamlValidator", () => { let validator; const samplesDir = path.join(__dirname, "test-samples"); const readSample = (filename) => { return fs.readFileSync(path.join(samplesDir, filename), "utf-8"); }; beforeEach(() => { validator = new yaml_validator_1.WindmillYamlValidator(); }); describe("constructor", () => { it("should create a validator instance", () => { expect(validator).toBeInstanceOf(yaml_validator_1.WindmillYamlValidator); }); it("should initialize without throwing", () => { expect(() => new yaml_validator_1.WindmillYamlValidator()).not.toThrow(); }); }); describe("validate (flow target)", () => { it("should throw error for non-string input", () => { expect(() => validator.validate(null, { type: "flow" })).toThrow("Document must be a string"); expect(() => validator.validate(123, { type: "flow" })).toThrow("Document must be a string"); expect(() => validator.validate({}, { type: "flow" })).toThrow("Document must be a string"); expect(() => validator.validate([], { type: "flow" })).toThrow("Document must be a string"); }); describe("valid flows", () => { it("should validate a valid minimal flow from sample file", () => { const validFlow = readSample("valid-minimal.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); expect(result.parsed).toBeDefined(); expect(result.parsed.data).toMatchObject({ summary: "Test Flow", value: { modules: [], }, }); }); it("should validate a script flow from sample file", () => { const validFlow = readSample("valid-script-flow.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); expect(result.parsed.data).toMatchObject({ summary: "Simple Script Flow", description: "A basic flow that runs a TypeScript script", schema: expect.objectContaining({ type: "object", properties: expect.objectContaining({ message: expect.objectContaining({ type: "string", default: "Hello World", }), }), }), value: { modules: expect.arrayContaining([ expect.objectContaining({ id: "script_step", value: expect.objectContaining({ type: "rawscript", language: "deno", input_transforms: {}, }), }), ]), }, }); }); }); describe("invalid flows", () => { it("should return errors for missing summary from sample file", () => { const invalidFlow = readSample("invalid-missing-summary.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.some((error) => error.instancePath === "" && error.keyword === "required" && error.params?.missingProperty === "summary")).toBe(true); }); it("should return errors for invalid types from sample file", () => { const invalidFlow = readSample("invalid-wrong-types.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.some((error) => error.instancePath === "/summary" && error.keyword === "type")).toBe(true); }); it("should return errors for invalid language from sample file", () => { const invalidFlow = readSample("invalid-language.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.some((error) => error.instancePath === "/value/modules/0/value/language" && error.keyword === "enum")).toBe(true); }); it("should handle empty file from sample", () => { const emptyFlow = readSample("empty.yaml"); const result = validator.validate(emptyFlow, { type: "flow" }); expect(result.parsed).toBeDefined(); expect(result.errors.length).toBeGreaterThan(0); }); it("should handle complex invalid flow with comprehensive error detection", () => { const complexInvalidFlow = readSample("invalid-complex-flow.yaml"); const result = validator.validate(complexInvalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(20); // Should have many errors // Check for missing required fields errors const missingRequiredErrors = result.errors.filter((error) => error.keyword === "required"); expect(missingRequiredErrors.length).toBeGreaterThan(10); // Check for invalid enum errors (invalid language) const enumErrors = result.errors.filter((error) => error.keyword === "enum"); expect(enumErrors.length).toBeGreaterThan(0); // Check for type errors (string vs number, boolean vs string, etc.) const typeErrors = result.errors.filter((error) => error.keyword === "type"); expect(typeErrors.length).toBeGreaterThan(5); // Should detect invalid forloop structure (by checking for forloop-related required fields) const forloopRequiredErrors = result.errors.filter((error) => error.keyword === "required" && error.message && (error.message.includes("modules") || error.message.includes("iterator") || error.message.includes("skip_failures"))); expect(forloopRequiredErrors.length).toBeGreaterThan(0); // Should detect invalid branch structure const branchRequiredErrors = result.errors.filter((error) => error.keyword === "required" && error.message && (error.message.includes("branches") || error.message.includes("default") || error.message.includes("expr"))); expect(branchRequiredErrors.length).toBeGreaterThan(0); // Should detect discriminator errors for invalid transform types const discriminatorErrors = result.errors.filter((error) => error.keyword === "discriminator"); expect(discriminatorErrors.length).toBeGreaterThan(0); }); it("should handle deeply nested invalid structures with detailed error reporting", () => { const nestedInvalidFlow = readSample("invalid-nested-structures.yaml"); const result = validator.validate(nestedInvalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(10); // Should have many nested errors // Check for deeply nested path errors (paths with many levels) const deepNestedErrors = result.errors.filter((error) => error.instancePath.split("/").length > 6 // Deep nesting ); expect(deepNestedErrors.length).toBeGreaterThan(0); // Check for transform-related errors const transformErrors = result.errors.filter((error) => error.instancePath.includes("input_transforms") || error.instancePath.includes("iterator")); expect(transformErrors.length).toBeGreaterThan(0); // Check for type errors in general const typeErrors = result.errors.filter((error) => error.keyword === "type"); expect(typeErrors.length).toBeGreaterThan(0); // Check for missing required fields in nested structures const nestedRequiredErrors = result.errors.filter((error) => error.keyword === "required" && error.instancePath.includes("/modules/")); expect(nestedRequiredErrors.length).toBeGreaterThan(0); // Check for discriminator errors in nested transforms const nestedDiscriminatorErrors = result.errors.filter((error) => error.keyword === "discriminator"); expect(nestedDiscriminatorErrors.length).toBeGreaterThan(0); }); it("should provide specific error locations for complex validation failures", () => { const complexInvalidFlow = readSample("invalid-complex-flow.yaml"); const result = validator.validate(complexInvalidFlow, { type: "flow" }); // Verify that errors have meaningful instance paths const errorsWithPaths = result.errors.filter((error) => error.instancePath && error.instancePath.length > 0); expect(errorsWithPaths.length).toBeGreaterThan(5); // Check that we can identify specific problematic modules const moduleSpecificErrors = result.errors.filter((error) => error.instancePath.includes("/value/modules/")); expect(moduleSpecificErrors.length).toBeGreaterThan(0); // Verify error messages are descriptive const descriptiveErrors = result.errors.filter((error) => error.message && error.message.length > 0); expect(descriptiveErrors.length).toBe(result.errors.length); }); it("should handle all major flow control structures with validation errors", () => { const complexInvalidFlow = readSample("invalid-complex-flow.yaml"); const result = validator.validate(complexInvalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(20); // Should find errors related to flow control structures by checking instance paths const flowControlErrors = result.errors.filter((error) => error.instancePath.includes("/modules/2/") || // forloop module error.instancePath.includes("/modules/3/") || // forloop module error.instancePath.includes("/modules/4/") || // branch module error.instancePath.includes("/modules/5/") || // branch module error.instancePath.includes("/modules/6/") || // branch all module error.instancePath.includes("/modules/7/") // while loop module ); expect(flowControlErrors.length).toBeGreaterThan(10); // Should find errors in script and flow references const pathReferenceErrors = result.errors.filter((error) => error.instancePath.includes("/modules/8/") || // script path module error.instancePath.includes("/modules/9/") // flow path module ); expect(pathReferenceErrors.length).toBeGreaterThan(0); // Should detect modules with missing IDs const missingIdErrors = result.errors.filter((error) => error.keyword === "required" && error.message && error.message.includes("id")); expect(missingIdErrors.length).toBeGreaterThan(0); // Should detect type mismatches at the flow level const flowLevelTypeErrors = result.errors.filter((error) => error.instancePath.startsWith("/value/") && !error.instancePath.includes("/modules/") && error.keyword === "type"); expect(flowLevelTypeErrors.length).toBeGreaterThan(0); }); }); describe("AI agent flows", () => { describe("valid AI agent flows", () => { it("should validate a basic AI agent flow with FlowModule tools", () => { const validFlow = readSample("valid-aiagent-basic.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); }); it("should validate an AI agent flow with MCP tools", () => { const validFlow = readSample("valid-aiagent-mcp.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); }); it("should validate an AI agent flow with mixed FlowModule and MCP tools", () => { const validFlow = readSample("valid-aiagent-mixed.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); }); it("should validate an AI agent flow linked to an ai_agent resource", () => { const validFlow = readSample("valid-aiagent-linked.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); }); it("should validate an AI agent flow with parallel execution enabled", () => { const validFlow = readSample("valid-aiagent-parallel.yaml"); const result = validator.validate(validFlow, { type: "flow" }); expect(result.errors).toHaveLength(0); // Verify tools array has expected structure const agentModule = result.parsed.data.value.modules[0]; expect(agentModule.value.tools).toHaveLength(2); }); }); describe("invalid AI agent flows", () => { it("should return errors for AI agent missing type field", () => { const invalidFlow = readSample("invalid-aiagent-missing-type.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); // Should fail discriminator validation since type is missing expect(result.errors.some((error) => error.keyword === "required" || error.keyword === "discriminator")).toBe(true); }); it("should return errors for AI agent with invalid tool_type", () => { const invalidFlow = readSample("invalid-aiagent-invalid-tool-type.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); // Should fail discriminator validation for invalid tool_type expect(result.errors.some((error) => error.keyword === "discriminator" || error.keyword === "enum")).toBe(true); }); it("should return errors for MCP tool missing resource_path", () => { const invalidFlow = readSample("invalid-aiagent-mcp-missing-resource.yaml"); const result = validator.validate(invalidFlow, { type: "flow" }); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.some((error) => error.keyword === "required" && error.params?.missingProperty === "resource_path")).toBe(true); }); }); }); }); });