import { describe, expect, it, vi } from "vitest"; import { parseTerminalMacros, runTerminalMacro, type MacroStep, type TerminalMacro, } from "@/lib/terminal-macros"; function macro(steps: MacroStep[]): TerminalMacro { return { id: "macro-1", name: "Test", steps, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }; } describe("terminal macro runner", () => { it("runs send, delay, regex wait, condition, and repeat steps", async () => { vi.useFakeTimers(); const sent: string[] = []; let listener = (_data: string) => {}; const run = runTerminalMacro( macro([ { id: "send", type: "send", text: "status", pressEnter: true }, { id: "wait", type: "wait", pattern: "READY\\s+42", timeoutMs: 1000, onTimeout: "stop", }, { id: "if", type: "if", pattern: "READY", then: [ { id: "repeat", type: "repeat", count: 2, steps: [ { id: "ok", type: "send", text: "ok", pressEnter: false }, ], }, ], else: [], }, ]), { send: (data) => sent.push(data), subscribe: (next) => { listener = next; return () => {}; }, }, ); await vi.advanceTimersByTimeAsync(1); listener("READY 42"); await run; expect(sent).toEqual(["status\r", "ok", "ok"]); vi.useRealTimers(); }); it("stops when a required regex times out", async () => { vi.useFakeTimers(); const run = runTerminalMacro( macro([ { id: "wait", type: "wait", pattern: "never", timeoutMs: 100, onTimeout: "stop", }, ]), { send: () => {}, subscribe: () => () => {} }, ); const rejection = expect(run).rejects.toThrow("Timed out"); await vi.advanceTimersByTimeAsync(101); await rejection; vi.useRealTimers(); }); it("sanitizes persisted macro bounds and drops unknown steps", () => { const parsed = parseTerminalMacros( JSON.stringify([ { id: "m", name: "Macro", steps: [ { id: "d", type: "delay", milliseconds: 999_999 }, { id: "r", type: "repeat", count: 1000, steps: [] }, { id: "bad", type: "execute-javascript", code: "nope" }, ], }, ]), ); expect(parsed[0].steps).toEqual([ { id: "d", type: "delay", milliseconds: 300_000 }, { id: "r", type: "repeat", count: 100, steps: [] }, ]); }); it("matches literal text without regex escaping when isRegex is false", async () => { vi.useFakeTimers(); const sent: string[] = []; let listener = (_data: string) => {}; const run = runTerminalMacro( macro([ { id: "wait", type: "wait", pattern: "progress (1/2)", isRegex: false, timeoutMs: 1000, onTimeout: "stop", }, { id: "send", type: "send", text: "done", pressEnter: false }, ]), { send: (data) => sent.push(data), subscribe: (next) => { listener = next; return () => {}; }, }, ); await vi.advanceTimersByTimeAsync(1); listener("progress (1/2)"); await run; expect(sent).toEqual(["done"]); vi.useRealTimers(); }); it("falls back to literal matching when a regex is invalid", async () => { vi.useFakeTimers(); const sent: string[] = []; let listener = (_data: string) => {}; const run = runTerminalMacro( macro([ { id: "wait", type: "wait", pattern: "oops(", isRegex: true, timeoutMs: 1000, onTimeout: "stop", }, { id: "send", type: "send", text: "ok", pressEnter: false }, ]), { send: (data) => sent.push(data), subscribe: (next) => { listener = next; return () => {}; }, }, ); await vi.advanceTimersByTimeAsync(1); listener("oops("); await run; expect(sent).toEqual(["ok"]); vi.useRealTimers(); }); it("branches on literal if-step text", async () => { const sent: string[] = []; await runTerminalMacro( macro([ { id: "if", type: "if", pattern: "a.b", isRegex: false, then: [{ id: "y", type: "send", text: "yes", pressEnter: false }], else: [{ id: "n", type: "send", text: "no", pressEnter: false }], }, ]), { send: (data) => sent.push(data), subscribe: () => () => {} }, ); // "a.b" as a literal must not match "axb" expect(sent).toEqual(["no"]); }); });