* Run server tests on Linux+Windows matrix Replace the hand-curated Windows server test allow-list with a full-suite matrix run.\nBoth Ubuntu and Windows now run the same server test command with shared setup and secrets. * Add isPlatform helper and gate Windows-hostile server tests - Add a shared server test isPlatform helper. - Migrate existing Windows-gated spawn, worktree, executable, and worktree-core tests to the helper. - Gate Windows-hostile symlink, macOS path-normalization, and POSIX shell setup tests with skipIf. * Replace POSIX shell calls in test fixtures with Node primitives - Replaced test fixture mkdir/echo shell setup with fs mkdirSync/writeFileSync calls in touched server tests. - Replaced git shell strings with execFileSync/spawnSync argv calls across checkout, worktree, MCP, script, and workspace git fixtures. - Gated the directory suggestion symlink escape tests on Windows because those fixtures require POSIX symlink behavior. * Fix Windows server-test failures and split POSIX-only suites into sibling files - Fix Windows path/cwd assertions in terminal, session/workspace, git-service, checkout-git, MCP, logger, spawn, and registry bootstrap tests.\n- Keep terminal tests runnable on Windows by canonicalizing temp cwd fixtures and ensuring a Windows shell fallback.\n- Gate POSIX-only shell, signal, Unix socket, and git-worktree reuse fixtures that need dedicated Windows coverage later. * Move POSIX-only test blocks into sibling .posix.test.ts files - terminal.test.ts: moved PTY/bash interaction blocks into terminal.posix.test.ts. - worktree.test.ts: moved git-worktree and teardown shell blocks into worktree.posix.test.ts. - worktree-bootstrap.test.ts: moved setup shell and terminal-backed service blocks into worktree-bootstrap.posix.test.ts. - worktree-core.test.ts: moved the POSIX-only worktree-core suite into worktree-core.posix.test.ts and removed the empty original. - provider-availability.test.ts and file-explorer/service.test.ts: moved POSIX PATH/symlink blocks into sibling suites. * Fix Windows 8.3 short-name, EBUSY cleanup, and node-pty shell-path failures in server tests - Normalize temp home directories in directory suggestion assertions to avoid Windows short-name mismatches. - Use Windows-valid terminal shells/cwds in terminal fixtures and wait for terminal-manager PTYs before cleanup. - Replace hardcoded POSIX paths in workspace-git/MCP/loop fixtures with platform-resolved paths or command files. - Gate the two explicitly Linux-only workspace-git watcher tests. * Replace hardcoded POSIX paths with portable Node path constructions in server tests - checkout-git.test.ts and worktree-session.test.ts: compare Windows temp paths with realpathSync.native to avoid 8.3 short-name mismatches. - directory-suggestions.test.ts: canonicalize result and expected paths with realpathSync.native. - session.workspaces.test.ts: replace literal /tmp and /Users fixtures with path.resolve/path.join constructions. - workspace-git-service.primitive.test.ts, loop-service.test.ts, and mcp-server.test.ts: use canonical repo/temp paths and shell-safe relative verify commands. * Fix loop-service verify-check shell and workspace-git-service path-separator on Windows - Run the loop-service verify script through the current Node executable with a relative script path. - Normalize the workspace-git-service expected repo cwd to forward slashes for the listWorktrees assertion. * Fix loop-service worker PTY spawn on Windows - Canonicalize the loop-service temporary root and workspace with realpathSync.native before worker agents use the path as cwd. * Gate loop-service real-worker-PTY test on Windows - Skip the real worker PTY loop test on Windows after ConPTY path resolution still fails with node-pty error 267.\n- Keep the test running on POSIX so the loop behavior remains covered. * Stub getMetricsSnapshot in test mocks to suppress async-leak uncaught exceptions on Windows - Add a no-op AgentManager metrics snapshot to the WebSocket notification test server stub. * Fix terminal-manager Windows-path test to avoid PTY spawn into nonexistent dir - Use an existing temporary cwd for the createTerminal absolute-path validation assertion so node-pty does not spawn into a missing Windows directory.
275 lines
7.9 KiB
TypeScript
275 lines
7.9 KiB
TypeScript
import { describe, it, expect, afterEach } from "vitest";
|
|
import { isPlatform } from "../test-utils/platform.js";
|
|
import {
|
|
createTerminal,
|
|
ensureNodePtySpawnHelperExecutableForCurrentPlatform,
|
|
resolveDefaultTerminalShell,
|
|
humanizeProcessTitle,
|
|
normalizeProcessTitle,
|
|
type TerminalSession,
|
|
} from "./terminal.js";
|
|
import {
|
|
chmodSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
realpathSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
if (isPlatform("win32") && !process.env.ComSpec && !process.env.COMSPEC) {
|
|
process.env.ComSpec = "C:\\Windows\\System32\\cmd.exe";
|
|
}
|
|
|
|
const sessions: TerminalSession[] = [];
|
|
const temporaryDirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
for (const session of sessions) {
|
|
session.kill();
|
|
}
|
|
sessions.length = 0;
|
|
while (temporaryDirs.length > 0) {
|
|
const dir = temporaryDirs.pop();
|
|
if (dir) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
function trackSession(session: TerminalSession): TerminalSession {
|
|
sessions.push(session);
|
|
return session;
|
|
}
|
|
|
|
describe("createTerminal", () => {
|
|
it("keeps full process titles while stripping path prefixes", () => {
|
|
expect(normalizeProcessTitle(" /usr/local/bin/npm run dev ")).toBe("npm run dev");
|
|
expect(normalizeProcessTitle("/opt/homebrew/bin/node /tmp/work/npm-cli.js run dev")).toBe(
|
|
"node npm-cli.js run dev",
|
|
);
|
|
expect(normalizeProcessTitle("")).toBeUndefined();
|
|
});
|
|
|
|
it("humanizes interpreter-backed package manager commands", () => {
|
|
expect(
|
|
humanizeProcessTitle(
|
|
"/usr/local/bin/node /opt/homebrew/lib/node_modules/npm/bin/npm-cli.js run dev",
|
|
),
|
|
).toBe("npm run dev");
|
|
expect(
|
|
humanizeProcessTitle("/usr/bin/env FOO=bar /opt/homebrew/bin/node /tmp/npm-cli.js test"),
|
|
).toBe("npm test");
|
|
});
|
|
|
|
it("drops common interpreter prefixes for direct scripts", () => {
|
|
expect(humanizeProcessTitle("/usr/bin/python3 /tmp/server.py --port 3000")).toBe(
|
|
"server.py --port 3000",
|
|
);
|
|
expect(humanizeProcessTitle("/bin/bash /tmp/dev.sh")).toBe("dev.sh");
|
|
});
|
|
|
|
// macOS-only: node-pty ships the spawn-helper prebuild only for darwin.
|
|
it.runIf(isPlatform("darwin"))("ensures darwin prebuild spawn-helper is executable", () => {
|
|
const packageRoot = mkdtempSync(join(tmpdir(), "terminal-node-pty-helper-"));
|
|
temporaryDirs.push(packageRoot);
|
|
const prebuildDir = join(packageRoot, "prebuilds", `darwin-${process.arch}`);
|
|
mkdirSync(prebuildDir, { recursive: true });
|
|
const helperPath = join(prebuildDir, "spawn-helper");
|
|
writeFileSync(helperPath, "#!/bin/sh\necho helper\n");
|
|
chmodSync(helperPath, 0o644);
|
|
|
|
ensureNodePtySpawnHelperExecutableForCurrentPlatform({
|
|
packageRoot,
|
|
platform: "darwin",
|
|
force: true,
|
|
});
|
|
|
|
expect(statSync(helperPath).mode & 0o111).toBe(0o111);
|
|
});
|
|
|
|
it("uses cmd.exe-compatible default shell on Windows", () => {
|
|
expect(resolveDefaultTerminalShell({ platform: "win32", env: {} })).toBe(
|
|
"C:\\Windows\\System32\\cmd.exe",
|
|
);
|
|
expect(
|
|
resolveDefaultTerminalShell({
|
|
platform: "win32",
|
|
env: { ComSpec: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" },
|
|
}),
|
|
).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
});
|
|
|
|
it("creates a terminal session with an id, name, and cwd", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
}),
|
|
);
|
|
|
|
expect(session.id).toBeDefined();
|
|
expect(typeof session.id).toBe("string");
|
|
expect(session.id.length).toBeGreaterThan(0);
|
|
expect(session.name).toBe("Terminal");
|
|
expect(session.cwd).toBe(realpathSync(tmpdir()));
|
|
});
|
|
|
|
it("uses custom name when provided", async () => {
|
|
const shell = isPlatform("win32")
|
|
? (process.env.ComSpec ?? "C:\\Windows\\System32\\cmd.exe")
|
|
: "/bin/sh";
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
shell,
|
|
env: { PS1: "$ " },
|
|
name: "Dev Server",
|
|
}),
|
|
);
|
|
|
|
expect(session.name).toBe("Dev Server");
|
|
});
|
|
|
|
it("uses default shell if not specified", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
}),
|
|
);
|
|
|
|
expect(session.id).toBeDefined();
|
|
});
|
|
|
|
it("uses default rows and cols", async () => {
|
|
const shell = isPlatform("win32")
|
|
? (process.env.ComSpec ?? "C:\\Windows\\System32\\cmd.exe")
|
|
: "/bin/sh";
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
shell,
|
|
env: { PS1: "$ " },
|
|
}),
|
|
);
|
|
|
|
const state = session.getState();
|
|
expect(state.rows).toBe(24);
|
|
expect(state.cols).toBe(80);
|
|
});
|
|
|
|
it("respects custom rows and cols", async () => {
|
|
const shell = isPlatform("win32")
|
|
? (process.env.ComSpec ?? "C:\\Windows\\System32\\cmd.exe")
|
|
: "/bin/sh";
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
shell,
|
|
env: { PS1: "$ " },
|
|
rows: 40,
|
|
cols: 120,
|
|
}),
|
|
);
|
|
|
|
const state = session.getState();
|
|
expect(state.rows).toBe(40);
|
|
expect(state.cols).toBe(120);
|
|
});
|
|
|
|
it("captures exit diagnostics from the terminal buffer", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
command: process.execPath,
|
|
args: [
|
|
"-e",
|
|
"process.stdout.write('launch failed\\ncommand missing\\n'); process.exit(127);",
|
|
],
|
|
}),
|
|
);
|
|
|
|
const exitInfo = await new Promise<NonNullable<ReturnType<TerminalSession["getExitInfo"]>>>(
|
|
(resolve) => {
|
|
session.onExit((info) => resolve(info));
|
|
},
|
|
);
|
|
|
|
expect(exitInfo.exitCode).toBe(127);
|
|
expect(exitInfo.signal).toBeNull();
|
|
// lastOutputLines may be empty if the process exits before xterm processes the data write
|
|
expect(Array.isArray(exitInfo.lastOutputLines)).toBe(true);
|
|
expect(session.getExitInfo()).toEqual(exitInfo);
|
|
});
|
|
});
|
|
|
|
describe("resize", () => {
|
|
it("updates terminal dimensions on resize", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
rows: 24,
|
|
cols: 80,
|
|
}),
|
|
);
|
|
|
|
session.send({ type: "resize", rows: 40, cols: 120 });
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = session.getState();
|
|
expect(state.rows).toBe(40);
|
|
expect(state.cols).toBe(120);
|
|
});
|
|
|
|
it("grid reflects new dimensions after resize", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
rows: 24,
|
|
cols: 80,
|
|
}),
|
|
);
|
|
|
|
session.send({ type: "resize", rows: 10, cols: 40 });
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = session.getState();
|
|
expect(state.grid.length).toBe(10);
|
|
expect(state.grid[0].length).toBe(40);
|
|
});
|
|
|
|
it("exposes the current size without extracting full state", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
rows: 24,
|
|
cols: 80,
|
|
}),
|
|
);
|
|
|
|
expect(session.getSize()).toEqual({ rows: 24, cols: 80 });
|
|
|
|
session.send({ type: "resize", rows: 10, cols: 40 });
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
expect(session.getSize()).toEqual({ rows: 10, cols: 40 });
|
|
});
|
|
});
|
|
|
|
describe("mouse events", () => {
|
|
it("accepts mouse events without throwing", async () => {
|
|
const session = trackSession(
|
|
await createTerminal({
|
|
cwd: realpathSync(tmpdir()),
|
|
}),
|
|
);
|
|
|
|
// Should not throw
|
|
session.send({ type: "mouse", row: 0, col: 0, button: 0, action: "down" });
|
|
session.send({ type: "mouse", row: 0, col: 0, button: 0, action: "up" });
|
|
});
|
|
});
|