* 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.
183 lines
5.5 KiB
TypeScript
183 lines
5.5 KiB
TypeScript
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
|
|
import { isPlatform } from "../test-utils/platform.js";
|
|
import { probeExecutable } from "./executable.js";
|
|
|
|
const timeoutMs = 1000;
|
|
const timeoutSlackMs = 500;
|
|
const tempDirs: string[] = [];
|
|
|
|
interface ProbeFixture {
|
|
name: string;
|
|
expected: boolean;
|
|
create: (dir: string) => { executablePath: string; pidFile?: string };
|
|
}
|
|
|
|
function makeTempDir(): string {
|
|
const dir = mkdtempSync(path.join(os.tmpdir(), "paseo-probe-test-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function writeExecutable(filePath: string, content: string | Buffer): string {
|
|
writeFileSync(filePath, content);
|
|
if (process.platform !== "win32") {
|
|
chmodSync(filePath, 0o755);
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
function scriptPath(dir: string, name: string): string {
|
|
return process.platform === "win32" ? path.join(dir, `${name}.cmd`) : path.join(dir, name);
|
|
}
|
|
|
|
function createHangingFixture(dir: string): string {
|
|
if (process.platform === "win32") {
|
|
return writeExecutable(
|
|
scriptPath(dir, "hangs"),
|
|
"@echo off\r\n:loop\r\ntimeout /T 5 /NOBREAK > NUL\r\ngoto loop\r\n",
|
|
);
|
|
}
|
|
return writeExecutable(
|
|
scriptPath(dir, "hangs"),
|
|
`#!/bin/sh\ntrap '' TERM\necho $$ > "${path.join(dir, "hangs.pid")}"\nwhile :; do :; done\n`,
|
|
);
|
|
}
|
|
|
|
function createNoVersionFixture(dir: string): string {
|
|
if (process.platform === "win32") {
|
|
return writeExecutable(scriptPath(dir, "no-version"), "@echo off\r\nexit /b 0\r\n");
|
|
}
|
|
return writeExecutable(scriptPath(dir, "no-version"), "#!/bin/sh\nexit 0\n");
|
|
}
|
|
|
|
function createNonZeroFixture(dir: string): string {
|
|
if (process.platform === "win32") {
|
|
return writeExecutable(
|
|
scriptPath(dir, "non-zero"),
|
|
"@echo off\r\necho oops 1>&2\r\nexit /b 1\r\n",
|
|
);
|
|
}
|
|
return writeExecutable(scriptPath(dir, "non-zero"), "#!/bin/sh\necho oops 1>&2\nexit 1\n");
|
|
}
|
|
|
|
function createSlowSuccessFixture(dir: string): string {
|
|
if (process.platform === "win32") {
|
|
return writeExecutable(
|
|
scriptPath(dir, "slow-success"),
|
|
"@echo off\r\nping -n 1 127.0.0.1 > NUL\r\nexit /b 0\r\n",
|
|
);
|
|
}
|
|
return writeExecutable(scriptPath(dir, "slow-success"), "#!/bin/sh\nsleep 0.05\nexit 0\n");
|
|
}
|
|
|
|
function createDirectoryFixture(dir: string): string {
|
|
const directoryPath = path.join(dir, "candidate-directory");
|
|
mkdirSync(directoryPath);
|
|
return directoryPath;
|
|
}
|
|
|
|
function missingAbsolutePath(): string {
|
|
return process.platform === "win32" ? "C:\\no\\such\\path.exe" : "/no/such/path";
|
|
}
|
|
|
|
async function waitForFile(filePath: string): Promise<void> {
|
|
const deadline = performance.now() + timeoutSlackMs;
|
|
while (!existsSync(filePath) && performance.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
|
|
const fixtures: ProbeFixture[] = [
|
|
{
|
|
name: "hangs forever after starting",
|
|
expected: true,
|
|
create: (dir) => ({
|
|
executablePath: createHangingFixture(dir),
|
|
pidFile: process.platform === "win32" ? undefined : path.join(dir, "hangs.pid"),
|
|
}),
|
|
},
|
|
{
|
|
name: "does not know --version and exits zero",
|
|
expected: true,
|
|
create: (dir) => ({ executablePath: createNoVersionFixture(dir) }),
|
|
},
|
|
{
|
|
name: "exits non-zero immediately",
|
|
expected: true,
|
|
create: (dir) => ({ executablePath: createNonZeroFixture(dir) }),
|
|
},
|
|
{
|
|
name: "starts slowly and exits zero",
|
|
expected: true,
|
|
create: (dir) => ({ executablePath: createSlowSuccessFixture(dir) }),
|
|
},
|
|
{
|
|
name: "points at a directory",
|
|
expected: false,
|
|
create: (dir) => ({ executablePath: createDirectoryFixture(dir) }),
|
|
},
|
|
{
|
|
name: "does not exist at an absolute path",
|
|
expected: false,
|
|
create: () => ({ executablePath: missingAbsolutePath() }),
|
|
},
|
|
];
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("probeExecutable", () => {
|
|
// POSIX-only: positive fixtures rely on direct script probing; Windows command-script probing has separate coverage.
|
|
test.skipIf(isPlatform("win32")).each(fixtures.filter((fixture) => fixture.expected))(
|
|
"$name",
|
|
async ({ create, expected }) => {
|
|
const { executablePath, pidFile } = create(makeTempDir());
|
|
const startedAt = performance.now();
|
|
|
|
const result = await probeExecutable(executablePath, timeoutMs);
|
|
|
|
expect(result).toBe(expected);
|
|
expect(performance.now() - startedAt).toBeLessThanOrEqual(timeoutMs + timeoutSlackMs);
|
|
if (pidFile) {
|
|
await waitForFile(pidFile);
|
|
const pid = Number(readFileSync(pidFile, "utf8"));
|
|
expect(() => process.kill(pid, 0)).toThrow(expect.objectContaining({ code: "ESRCH" }));
|
|
}
|
|
},
|
|
);
|
|
|
|
test.each(fixtures.filter((fixture) => !fixture.expected))(
|
|
"$name",
|
|
async ({ create, expected }) => {
|
|
const { executablePath, pidFile } = create(makeTempDir());
|
|
const startedAt = performance.now();
|
|
|
|
const result = await probeExecutable(executablePath, timeoutMs);
|
|
|
|
expect(result).toBe(expected);
|
|
expect(performance.now() - startedAt).toBeLessThanOrEqual(timeoutMs + timeoutSlackMs);
|
|
if (pidFile) {
|
|
await waitForFile(pidFile);
|
|
const pid = Number(readFileSync(pidFile, "utf8"));
|
|
expect(() => process.kill(pid, 0)).toThrow(expect.objectContaining({ code: "ESRCH" }));
|
|
}
|
|
},
|
|
);
|
|
});
|