Files
Termix/src/ui/tests/lib/terminal-macros.test.ts
T

186 lines
4.8 KiB
TypeScript
Raw Normal View History

+5
2026-08-19 14:12:06 -05:00
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"]);
});
});