221 lines
7.6 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|