mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
568 lines
19 KiB
TypeScript
568 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { TerminalService, getTerminalService } from "@/services/terminal-service.js";
|
|
import * as pty from "node-pty";
|
|
import * as os from "os";
|
|
import * as fs from "fs";
|
|
|
|
vi.mock("node-pty");
|
|
vi.mock("fs");
|
|
vi.mock("os");
|
|
|
|
describe("terminal-service.ts", () => {
|
|
let service: TerminalService;
|
|
let mockPtyProcess: any;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = new TerminalService();
|
|
|
|
// Mock PTY process
|
|
mockPtyProcess = {
|
|
onData: vi.fn(),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
|
|
vi.mocked(os.homedir).mockReturnValue("/home/user");
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.mocked(os.arch).mockReturnValue("x64");
|
|
});
|
|
|
|
afterEach(() => {
|
|
service.cleanup();
|
|
});
|
|
|
|
describe("detectShell", () => {
|
|
it("should detect PowerShell Core on Windows when available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("win32");
|
|
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
|
|
return path === "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe");
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it("should fall back to PowerShell on Windows if Core not available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("win32");
|
|
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
|
|
return path === "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it("should fall back to cmd.exe on Windows if no PowerShell", () => {
|
|
vi.mocked(os.platform).mockReturnValue("win32");
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("cmd.exe");
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it("should detect user shell on macOS", () => {
|
|
vi.mocked(os.platform).mockReturnValue("darwin");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/zsh" });
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/zsh");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
|
|
it("should fall back to zsh on macOS if user shell not available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("darwin");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({});
|
|
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
|
|
return path === "/bin/zsh";
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/zsh");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
|
|
it("should fall back to bash on macOS if zsh not available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("darwin");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({});
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/bash");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
|
|
it("should detect user shell on Linux", () => {
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/bash");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
|
|
it("should fall back to bash on Linux if user shell not available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({});
|
|
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
|
|
return path === "/bin/bash";
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/bash");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
|
|
it("should fall back to sh on Linux if bash not available", () => {
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({});
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/sh");
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it("should detect WSL and use appropriate shell", () => {
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe("/bin/bash");
|
|
expect(result.args).toEqual(["--login"]);
|
|
});
|
|
});
|
|
|
|
describe("isWSL", () => {
|
|
it("should return true if /proc/version contains microsoft", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it("should return true if /proc/version contains wsl", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-wsl2");
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it("should return true if WSL_DISTRO_NAME is set", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ WSL_DISTRO_NAME: "Ubuntu" });
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it("should return true if WSLENV is set", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ WSLENV: "PATH/l" });
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it("should return false if not in WSL", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({});
|
|
|
|
expect(service.isWSL()).toBe(false);
|
|
});
|
|
|
|
it("should return false if error reading /proc/version", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
throw new Error("Permission denied");
|
|
});
|
|
|
|
expect(service.isWSL()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getPlatformInfo", () => {
|
|
it("should return platform information", () => {
|
|
vi.mocked(os.platform).mockReturnValue("linux");
|
|
vi.mocked(os.arch).mockReturnValue("x64");
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const info = service.getPlatformInfo();
|
|
|
|
expect(info.platform).toBe("linux");
|
|
expect(info.arch).toBe("x64");
|
|
expect(info.defaultShell).toBe("/bin/bash");
|
|
expect(typeof info.isWSL).toBe("boolean");
|
|
});
|
|
});
|
|
|
|
describe("createSession", () => {
|
|
it("should create a new terminal session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession({
|
|
cwd: "/test/dir",
|
|
cols: 100,
|
|
rows: 30,
|
|
});
|
|
|
|
expect(session.id).toMatch(/^term-/);
|
|
expect(session.cwd).toBe("/test/dir");
|
|
expect(session.shell).toBe("/bin/bash");
|
|
expect(pty.spawn).toHaveBeenCalledWith(
|
|
"/bin/bash",
|
|
["--login"],
|
|
expect.objectContaining({
|
|
cwd: "/test/dir",
|
|
cols: 100,
|
|
rows: 30,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should use default cols and rows if not provided", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
service.createSession();
|
|
|
|
expect(pty.spawn).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Array),
|
|
expect.objectContaining({
|
|
cols: 80,
|
|
rows: 24,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should fall back to home directory if cwd does not exist", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockImplementation(() => {
|
|
throw new Error("ENOENT");
|
|
});
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession({
|
|
cwd: "/nonexistent",
|
|
});
|
|
|
|
expect(session.cwd).toBe("/home/user");
|
|
});
|
|
|
|
it("should fall back to home directory if cwd is not a directory", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => false } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession({
|
|
cwd: "/file.txt",
|
|
});
|
|
|
|
expect(session.cwd).toBe("/home/user");
|
|
});
|
|
|
|
it("should fix double slashes in path", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession({
|
|
cwd: "//test/dir",
|
|
});
|
|
|
|
expect(session.cwd).toBe("/test/dir");
|
|
});
|
|
|
|
it("should preserve WSL UNC paths", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession({
|
|
cwd: "//wsl$/Ubuntu/home",
|
|
});
|
|
|
|
expect(session.cwd).toBe("//wsl$/Ubuntu/home");
|
|
});
|
|
|
|
it("should handle data events from PTY", () => {
|
|
vi.useFakeTimers();
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const dataCallback = vi.fn();
|
|
service.onData(dataCallback);
|
|
|
|
service.createSession();
|
|
|
|
// Simulate data event
|
|
const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
|
|
onDataHandler("test data");
|
|
|
|
// Wait for throttled output
|
|
vi.advanceTimersByTime(20);
|
|
|
|
expect(dataCallback).toHaveBeenCalled();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should handle exit events from PTY", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const exitCallback = vi.fn();
|
|
service.onExit(exitCallback);
|
|
|
|
const session = service.createSession();
|
|
|
|
// Simulate exit event
|
|
const onExitHandler = mockPtyProcess.onExit.mock.calls[0][0];
|
|
onExitHandler({ exitCode: 0 });
|
|
|
|
expect(exitCallback).toHaveBeenCalledWith(session.id, 0);
|
|
expect(service.getSession(session.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("write", () => {
|
|
it("should write data to existing session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession();
|
|
const result = service.write(session.id, "ls\n");
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.write).toHaveBeenCalledWith("ls\n");
|
|
});
|
|
|
|
it("should return false for non-existent session", () => {
|
|
const result = service.write("nonexistent", "data");
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockPtyProcess.write).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("resize", () => {
|
|
it("should resize existing session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession();
|
|
const result = service.resize(session.id, 120, 40);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
|
|
});
|
|
|
|
it("should return false for non-existent session", () => {
|
|
const result = service.resize("nonexistent", 120, 40);
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockPtyProcess.resize).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should handle resize errors", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
mockPtyProcess.resize.mockImplementation(() => {
|
|
throw new Error("Resize failed");
|
|
});
|
|
|
|
const session = service.createSession();
|
|
const result = service.resize(session.id, 120, 40);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("killSession", () => {
|
|
it("should kill existing session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession();
|
|
const result = service.killSession(session.id);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.kill).toHaveBeenCalled();
|
|
expect(service.getSession(session.id)).toBeUndefined();
|
|
});
|
|
|
|
it("should return false for non-existent session", () => {
|
|
const result = service.killSession("nonexistent");
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should handle kill errors", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
mockPtyProcess.kill.mockImplementation(() => {
|
|
throw new Error("Kill failed");
|
|
});
|
|
|
|
const session = service.createSession();
|
|
const result = service.killSession(session.id);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getSession", () => {
|
|
it("should return existing session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession();
|
|
const retrieved = service.getSession(session.id);
|
|
|
|
expect(retrieved).toBe(session);
|
|
});
|
|
|
|
it("should return undefined for non-existent session", () => {
|
|
const retrieved = service.getSession("nonexistent");
|
|
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("getScrollback", () => {
|
|
it("should return scrollback buffer for existing session", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session = service.createSession();
|
|
session.scrollbackBuffer = "test scrollback";
|
|
|
|
const scrollback = service.getScrollback(session.id);
|
|
|
|
expect(scrollback).toBe("test scrollback");
|
|
});
|
|
|
|
it("should return null for non-existent session", () => {
|
|
const scrollback = service.getScrollback("nonexistent");
|
|
|
|
expect(scrollback).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getAllSessions", () => {
|
|
it("should return all active sessions", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session1 = service.createSession({ cwd: "/dir1" });
|
|
const session2 = service.createSession({ cwd: "/dir2" });
|
|
|
|
const sessions = service.getAllSessions();
|
|
|
|
expect(sessions).toHaveLength(2);
|
|
expect(sessions[0].id).toBe(session1.id);
|
|
expect(sessions[1].id).toBe(session2.id);
|
|
expect(sessions[0].cwd).toBe("/dir1");
|
|
expect(sessions[1].cwd).toBe("/dir2");
|
|
});
|
|
|
|
it("should return empty array if no sessions", () => {
|
|
const sessions = service.getAllSessions();
|
|
|
|
expect(sessions).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("onData and onExit", () => {
|
|
it("should allow subscribing and unsubscribing from data events", () => {
|
|
const callback = vi.fn();
|
|
const unsubscribe = service.onData(callback);
|
|
|
|
expect(typeof unsubscribe).toBe("function");
|
|
|
|
unsubscribe();
|
|
});
|
|
|
|
it("should allow subscribing and unsubscribing from exit events", () => {
|
|
const callback = vi.fn();
|
|
const unsubscribe = service.onExit(callback);
|
|
|
|
expect(typeof unsubscribe).toBe("function");
|
|
|
|
unsubscribe();
|
|
});
|
|
});
|
|
|
|
describe("cleanup", () => {
|
|
it("should clean up all sessions", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
|
|
const session1 = service.createSession();
|
|
const session2 = service.createSession();
|
|
|
|
service.cleanup();
|
|
|
|
expect(service.getSession(session1.id)).toBeUndefined();
|
|
expect(service.getSession(session2.id)).toBeUndefined();
|
|
expect(service.getAllSessions()).toHaveLength(0);
|
|
});
|
|
|
|
it("should handle cleanup errors gracefully", () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
|
|
mockPtyProcess.kill.mockImplementation(() => {
|
|
throw new Error("Kill failed");
|
|
});
|
|
|
|
service.createSession();
|
|
|
|
expect(() => service.cleanup()).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("getTerminalService", () => {
|
|
it("should return singleton instance", () => {
|
|
const instance1 = getTerminalService();
|
|
const instance2 = getTerminalService();
|
|
|
|
expect(instance1).toBe(instance2);
|
|
});
|
|
});
|
|
});
|