import { describe, expect, it } from "vitest"; import type { Automation } from "#/types/automation"; import { AutomationFileValidationError, parseAutomationFile, serializeAutomation, } from "./automation-export"; const cronAutomation: Automation = { id: "automation-1", name: "Daily review", trigger: { type: "cron", schedule: "0 9 * * *", schedule_human: "Daily at 09:00", }, enabled: true, repository: "openhands/agent-canvas", branch: "main", model: "fast", prompt: "Review open pull requests.", plugins: ["github:openhands/extensions"], notification: "Post a summary", timezone: "America/Los_Angeles", created_at: "2026-07-01T00:00:00Z", updated_at: "2026-07-02T00:00:00Z", last_triggered_at: "2026-07-03T00:00:00Z", }; describe("automation export files", () => { it("round-trips a cron automation without server-assigned fields", () => { const exported = serializeAutomation(cronAutomation); expect(exported).toEqual({ version: 1, kind: "automation", spec: { name: cronAutomation.name, trigger: cronAutomation.trigger, enabled: true, repository: cronAutomation.repository, model: cronAutomation.model, prompt: cronAutomation.prompt, branch: cronAutomation.branch, plugins: cronAutomation.plugins, notification: cronAutomation.notification, timezone: cronAutomation.timezone, }, }); expect(parseAutomationFile(exported)).toEqual(exported.spec); expect(exported.spec).not.toHaveProperty("id"); expect(exported.spec).not.toHaveProperty("created_at"); expect(exported.spec).not.toHaveProperty("updated_at"); expect(exported.spec).not.toHaveProperty("last_triggered_at"); }); it("round-trips event trigger fields", () => { const exported = serializeAutomation({ ...cronAutomation, trigger: { type: "event", source: "github", on: ["pull_request.opened", "pull_request.synchronize"], filter: "repository.full_name == 'openhands/agent-canvas'", }, timezone: undefined, }); expect(parseAutomationFile(exported).trigger).toEqual( exported.spec.trigger, ); }); it("preserves a positive timeout through an export/import round-trip", () => { const withTimeout: Automation = { ...cronAutomation, timeout: 900 }; const exported = serializeAutomation(withTimeout); expect(parseAutomationFile(exported).timeout).toBe(900); }); it("reports every malformed field with its path", () => { const malformed = { version: 2, kind: "workflow", spec: { name: "", prompt: null, enabled: "yes", trigger: { type: "event", source: 42, on: [] }, plugins: ["github:openhands/extensions", 42], model: false, timeout: -5, }, }; expect(() => parseAutomationFile(malformed)).toThrow( AutomationFileValidationError, ); try { parseAutomationFile(malformed); } catch (error) { expect(error).toBeInstanceOf(AutomationFileValidationError); expect((error as AutomationFileValidationError).issues).toEqual( expect.arrayContaining([ "version: expected 1", 'kind: expected "automation"', "spec.name: expected a non-empty string", "spec.prompt: expected a non-empty string", "spec.enabled: expected a boolean", "spec.trigger.source: expected a string", "spec.trigger.source: required for an event trigger", "spec.trigger.on: expected a string or a non-empty array of strings", "spec.trigger.on: required for an event trigger", "spec.plugins: expected an array of non-empty strings", "spec.model: expected a non-empty string or null", "spec.timeout: expected a positive integer", ]), ); } }); it("rejects non-object files and missing specs", () => { expect(() => parseAutomationFile(null)).toThrow( "file: expected a JSON object", ); expect(() => parseAutomationFile({ version: 1, kind: "automation" }), ).toThrow("spec: expected an object"); }); });