From f73bf1e313e770aa738198c3a7598fff49e5abab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 7 May 2026 02:16:15 +0000 Subject: [PATCH] test(workflow): add unit tests for validateWorkflowDescriptor Fixes #19 --- .../__tests__/workflow-descriptor.test.ts | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 packages/workflow/__tests__/workflow-descriptor.test.ts diff --git a/packages/workflow/__tests__/workflow-descriptor.test.ts b/packages/workflow/__tests__/workflow-descriptor.test.ts new file mode 100644 index 0000000..171df79 --- /dev/null +++ b/packages/workflow/__tests__/workflow-descriptor.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, test } from "bun:test"; +import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js"; + +describe("validateWorkflowDescriptor", () => { + // 1. Valid minimal descriptor + test("accepts a minimal descriptor with empty roles", () => { + const result = validateWorkflowDescriptor({ description: "x", roles: {} }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.description).toBe("x"); + expect(result.value.roles).toEqual({}); + } + }); + + // 2. Valid descriptor with one role + test("accepts a descriptor with one role", () => { + const result = validateWorkflowDescriptor({ + description: "workflow", + roles: { + solver: { description: "solves things", schema: { type: "object" } }, + }, + }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.description).toBe("workflow"); + expect(result.value.roles.solver.description).toBe("solves things"); + expect(result.value.roles.solver.schema).toEqual({ type: "object" }); + } + }); + + // 3. Valid descriptor with multiple roles + test("accepts a descriptor with multiple roles", () => { + const result = validateWorkflowDescriptor({ + description: "multi", + roles: { + a: { description: "role a", schema: {} }, + b: { description: "role b", schema: { type: "string" } }, + }, + }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(Object.keys(result.value.roles)).toEqual(["a", "b"]); + } + }); + + // 4-6. Root is null / array / string / number / undefined + test("rejects null", () => { + const result = validateWorkflowDescriptor(null); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); + }); + + test("rejects an array", () => { + const result = validateWorkflowDescriptor([]); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); + }); + + test("rejects a string", () => { + const result = validateWorkflowDescriptor("hello"); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); + }); + + test("rejects a number", () => { + const result = validateWorkflowDescriptor(42); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); + }); + + test("rejects undefined", () => { + const result = validateWorkflowDescriptor(undefined); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor must be a non-array object"); + }); + + // 7-8. Missing or non-string description + test("rejects missing description", () => { + const result = validateWorkflowDescriptor({ roles: {} }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); + }); + + test("rejects numeric description", () => { + const result = validateWorkflowDescriptor({ description: 123, roles: {} }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); + }); + + test("rejects null description", () => { + const result = validateWorkflowDescriptor({ description: null, roles: {} }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); + }); + + test("rejects boolean description", () => { + const result = validateWorkflowDescriptor({ description: true, roles: {} }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.description must be a string"); + }); + + // 9-11. Missing / null / array roles + test("rejects missing roles", () => { + const result = validateWorkflowDescriptor({ description: "x" }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); + }); + + test("rejects null roles", () => { + const result = validateWorkflowDescriptor({ description: "x", roles: null }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); + }); + + test("rejects array roles", () => { + const result = validateWorkflowDescriptor({ description: "x", roles: [] }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles must be a non-array object"); + }); + + // 12-13. Role entry is null / array + test("rejects null role entry", () => { + const result = validateWorkflowDescriptor({ description: "x", roles: { bad: null } }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles.bad must be a non-array object"); + }); + + test("rejects array role entry", () => { + const result = validateWorkflowDescriptor({ description: "x", roles: { bad: [] } }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles.bad must be a non-array object"); + }); + + // 14-15. Role missing description / non-string description + test("rejects role with missing description", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { r: { schema: {} } }, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles.r.description must be a string"); + }); + + test("rejects role with non-string description", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { r: { description: 99, schema: {} } }, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles.r.description must be a string"); + }); + + // 16-18. Role schema null / array / missing + test("rejects role with null schema", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { r: { description: "d", schema: null } }, + }); + expect(result.ok).toBe(false); + if (!result.ok) + expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); + }); + + test("rejects role with array schema", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { r: { description: "d", schema: [] } }, + }); + expect(result.ok).toBe(false); + if (!result.ok) + expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); + }); + + test("rejects role with missing schema", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { r: { description: "d" } }, + }); + expect(result.ok).toBe(false); + if (!result.ok) + expect(result.error).toBe("descriptor.roles.r.schema must be a non-array object"); + }); + + // 19. First role valid, second role invalid + test("rejects at first invalid role when earlier roles are valid", () => { + const result = validateWorkflowDescriptor({ + description: "x", + roles: { + good: { description: "ok", schema: {} }, + bad: { description: 123, schema: {} }, + }, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBe("descriptor.roles.bad.description must be a string"); + }); +});