agent-orchestrator/packages/integration-tests/src/terminal-iterm2.integration...

221 lines
7.6 KiB
TypeScript

/**
* Integration tests for terminal-iterm2.
*
* Mocks ONLY the I/O boundary: node:child_process and node:os.
* Everything else runs for real: AppleScript generation, escaping chains, tab reuse logic.
*/
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
vi.mock("node:child_process", () => ({
execFile: vi.fn(),
}));
vi.mock("node:os", () => ({
platform: vi.fn(() => "darwin"),
}));
import { execFile } from "node:child_process";
import { platform } from "node:os";
const mockExecFile = execFile as unknown as Mock;
const mockPlatform = platform as unknown as Mock;
import iterm2Plugin from "@composio/ao-plugin-terminal-iterm2";
import { makeSession } from "./helpers/event-factory.js";
function simulateOsascript(stdout: string) {
mockExecFile.mockImplementation(
(_cmd: string, _args: string[], cb: (err: Error | null, stdout: string) => void) => {
cb(null, stdout);
},
);
}
function simulateOsascriptSequence(results: string[]) {
let callIndex = 0;
mockExecFile.mockImplementation(
(_cmd: string, _args: string[], cb: (err: Error | null, stdout: string) => void) => {
const result = results[callIndex] ?? "NOT_FOUND";
callIndex++;
cb(null, result);
},
);
}
describe("terminal-iterm2 integration", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockPlatform.mockReturnValue("darwin");
});
describe("escaping chain integrity", () => {
it("handles double quotes, backslashes, and single quotes simultaneously in session name", async () => {
simulateOsascript("NOT_FOUND\n");
const terminal = iterm2Plugin.create();
const nastyName = `it's-a-"test"\\session`;
await terminal.openSession(makeSession({ id: nastyName }));
// Call 0: findAndSelectExistingTab
const findScript = mockExecFile.mock.calls[0][1][1] as string;
// AppleScript escaping: " -> \" and \ -> \\
expect(findScript).toContain('it\'s-a-\\"test\\"\\\\session');
// Call 1: openNewTab
const openScript = mockExecFile.mock.calls[1][1][1] as string;
// AppleScript escaping in set name
expect(openScript).toContain('set name to "it\'s-a-\\"test\\"\\\\session"');
// Shell escaping then AppleScript escaping in tmux command:
// Shell: 'it'\''s-a-"test"\session' -> AppleScript doubles backslashes
expect(openScript).toContain("tmux attach -t 'it'\\\\''s-a-\\\"test\\\"\\\\session'");
});
});
describe("tab reuse logic", () => {
it("FOUND response -> no openNewTab call (only 1 execFile call)", async () => {
simulateOsascript("FOUND\n");
const terminal = iterm2Plugin.create();
await terminal.openSession(makeSession());
expect(mockExecFile).toHaveBeenCalledTimes(1);
// Should be the findAndSelectExistingTab call only
const script = mockExecFile.mock.calls[0][1][1] as string;
expect(script).toContain("select aWindow");
expect(script).toContain("select aTab");
});
it("NOT_FOUND response -> openNewTab call (2 execFile calls)", async () => {
simulateOsascript("NOT_FOUND\n");
const terminal = iterm2Plugin.create();
await terminal.openSession(makeSession());
expect(mockExecFile).toHaveBeenCalledTimes(2);
const openScript = mockExecFile.mock.calls[1][1][1] as string;
expect(openScript).toContain("create tab with default profile");
});
it("runtimeHandle.id takes precedence over session.id", async () => {
simulateOsascript("NOT_FOUND\n");
const terminal = iterm2Plugin.create();
await terminal.openSession(
makeSession({
id: "app-1",
runtimeHandle: { id: "tmux-session-42", runtimeName: "tmux", data: {} },
}),
);
const findScript = mockExecFile.mock.calls[0][1][1] as string;
expect(findScript).toContain("tmux-session-42");
expect(findScript).not.toContain('"app-1"');
});
});
describe("isSessionOpen no side effects", () => {
it("script contains no select commands", async () => {
simulateOsascript("FOUND\n");
const terminal = iterm2Plugin.create();
await terminal.isSessionOpen!(makeSession({ id: "check-only" }));
const script = mockExecFile.mock.calls[0][1][1] as string;
expect(script).not.toContain("select aWindow");
expect(script).not.toContain("select aTab");
// It should still check name
expect(script).toContain("name of aSession");
expect(script).toContain('"check-only"');
});
it("returns true when FOUND", async () => {
simulateOsascript("FOUND\n");
const terminal = iterm2Plugin.create();
const result = await terminal.isSessionOpen!(makeSession());
expect(result).toBe(true);
});
it("returns false when NOT_FOUND", async () => {
simulateOsascript("NOT_FOUND\n");
const terminal = iterm2Plugin.create();
const result = await terminal.isSessionOpen!(makeSession());
expect(result).toBe(false);
});
it("returns false when osascript throws", async () => {
mockExecFile.mockImplementation(
(_cmd: string, _args: string[], cb: (err: Error | null, stdout: string) => void) => {
cb(new Error("script error"), "");
},
);
const terminal = iterm2Plugin.create();
const result = await terminal.isSessionOpen!(makeSession());
expect(result).toBe(false);
});
});
describe("platform guard", () => {
it("openSession is no-op on non-macOS and warns", async () => {
mockPlatform.mockReturnValue("linux");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const terminal = iterm2Plugin.create();
await terminal.openSession(makeSession());
expect(mockExecFile).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("only available on macOS"));
warnSpy.mockRestore();
});
it("openAll is no-op on non-macOS", async () => {
mockPlatform.mockReturnValue("win32");
const terminal = iterm2Plugin.create();
await terminal.openAll([makeSession()]);
expect(mockExecFile).not.toHaveBeenCalled();
});
it("isSessionOpen returns false on non-macOS", async () => {
mockPlatform.mockReturnValue("linux");
const terminal = iterm2Plugin.create();
const result = await terminal.isSessionOpen!(makeSession());
expect(result).toBe(false);
expect(mockExecFile).not.toHaveBeenCalled();
});
});
describe("openAll full pipeline", () => {
it("opens tabs for each session with delay between operations", async () => {
simulateOsascript("NOT_FOUND\n");
const terminal = iterm2Plugin.create();
const sessions = [
makeSession({ id: "s1" }),
makeSession({ id: "s2" }),
];
const promise = terminal.openAll(sessions);
// Process first session
await vi.advanceTimersByTimeAsync(300);
// Process second session
await vi.advanceTimersByTimeAsync(300);
await promise;
// 2 sessions * 2 calls each (find + open) = 4
expect(mockExecFile).toHaveBeenCalledTimes(4);
});
it("skips existing sessions and opens only new ones", async () => {
simulateOsascriptSequence(["FOUND\n", "NOT_FOUND\n", ""]);
const terminal = iterm2Plugin.create();
const sessions = [
makeSession({ id: "existing" }),
makeSession({ id: "new-one" }),
];
const promise = terminal.openAll(sessions);
await vi.advanceTimersByTimeAsync(300);
await vi.advanceTimersByTimeAsync(300);
await promise;
// existing: 1 call (find=FOUND), new-one: 2 calls (find + open)
expect(mockExecFile).toHaveBeenCalledTimes(3);
});
});
});