mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* 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.
219 lines
6.3 KiB
TypeScript
219 lines
6.3 KiB
TypeScript
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
import * as fs from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import { buildSelfNodeCommand } from "../server/paseo-env.js";
|
|
import { execCommand, spawnProcess } from "./spawn.js";
|
|
|
|
const printEnvScript = `
|
|
const keys = [
|
|
"CUSTOM",
|
|
"ELECTRON_NO_ATTACH_CONSOLE",
|
|
"ELECTRON_RUN_AS_NODE",
|
|
"PASEO_DESKTOP_MANAGED",
|
|
"PASEO_NODE_ENV",
|
|
"PASEO_SUPERVISED",
|
|
];
|
|
const values = Object.fromEntries(keys.map((key) => [key, process.env[key] ?? null]));
|
|
console.log(JSON.stringify(values));
|
|
`;
|
|
|
|
function parsePrintedEnv(stdout: string): Record<string, string | null> {
|
|
return JSON.parse(stdout.trim()) as Record<string, string | null>;
|
|
}
|
|
|
|
describe("execCommand", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const tempDir of tempDirs) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
tempDirs.length = 0;
|
|
});
|
|
|
|
test("returns stdout and stderr for a successful command", async () => {
|
|
const result = await execCommand("echo", ["hello"]);
|
|
|
|
expect(result.stdout.trim()).toBe("hello");
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
test("rejects when the command times out", async () => {
|
|
const command =
|
|
process.platform === "win32"
|
|
? {
|
|
command: process.execPath,
|
|
args: ["-e", "setTimeout(() => {}, 10_000)"],
|
|
}
|
|
: { command: "sleep", args: ["10"] };
|
|
|
|
await expect(execCommand(command.command, command.args, { timeout: 100 })).rejects.toThrow();
|
|
});
|
|
|
|
test("runs the command in the provided cwd", async () => {
|
|
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "spawn-test-")));
|
|
tempDirs.push(cwd);
|
|
|
|
const command =
|
|
process.platform === "win32"
|
|
? {
|
|
command: process.execPath,
|
|
args: ["-e", "console.log(process.cwd())"],
|
|
}
|
|
: { command: "pwd", args: [] };
|
|
|
|
const result = await execCommand(command.command, command.args, { cwd });
|
|
|
|
expect(realpathSync(result.stdout.trim())).toBe(cwd);
|
|
expect(result.stderr).toBe("");
|
|
});
|
|
|
|
test("treats env as the replacement base and finalizes external command env", async () => {
|
|
const result = await execCommand(process.execPath, ["-e", printEnvScript], {
|
|
baseEnv: {
|
|
ELECTRON_RUN_AS_NODE: "0",
|
|
CUSTOM: "from-base",
|
|
PATH: process.env.PATH,
|
|
PASEO_NODE_ENV: "production",
|
|
PASEO_SUPERVISED: "1",
|
|
},
|
|
env: {
|
|
CUSTOM: "from-env",
|
|
ELECTRON_NO_ATTACH_CONSOLE: "1",
|
|
PASEO_DESKTOP_MANAGED: "1",
|
|
PASEO_NODE_ENV: "test",
|
|
},
|
|
envOverlay: {
|
|
CUSTOM: "from-overlay",
|
|
ELECTRON_RUN_AS_NODE: undefined,
|
|
},
|
|
});
|
|
|
|
expect(parsePrintedEnv(result.stdout)).toEqual({
|
|
CUSTOM: "from-overlay",
|
|
ELECTRON_NO_ATTACH_CONSOLE: null,
|
|
ELECTRON_RUN_AS_NODE: null,
|
|
PASEO_DESKTOP_MANAGED: null,
|
|
PASEO_NODE_ENV: null,
|
|
PASEO_SUPERVISED: null,
|
|
});
|
|
});
|
|
|
|
test("does not inherit process.env when env replacement is supplied", async () => {
|
|
process.env.PASEO_TEST_SHOULD_NOT_LEAK = "leaked";
|
|
try {
|
|
const result = await execCommand(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
"console.log(JSON.stringify({ leaked: process.env.PASEO_TEST_SHOULD_NOT_LEAK ?? null }))",
|
|
],
|
|
{
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(JSON.parse(result.stdout.trim())).toEqual({ leaked: null });
|
|
} finally {
|
|
delete process.env.PASEO_TEST_SHOULD_NOT_LEAK;
|
|
}
|
|
});
|
|
|
|
test("spawnProcess finalizes external command env", async () => {
|
|
const child = spawnProcess(process.execPath, ["-e", printEnvScript], {
|
|
baseEnv: {
|
|
ELECTRON_RUN_AS_NODE: "0",
|
|
PATH: process.env.PATH,
|
|
PASEO_NODE_ENV: "production",
|
|
},
|
|
envOverlay: {
|
|
CUSTOM: "spawn-overlay",
|
|
PASEO_SUPERVISED: "1",
|
|
},
|
|
});
|
|
|
|
const stdoutChunks: Buffer[] = [];
|
|
const stderrChunks: Buffer[] = [];
|
|
child.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
child.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
|
|
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
child.on("error", reject);
|
|
child.on("close", resolve);
|
|
});
|
|
|
|
expect(Buffer.concat(stderrChunks).toString()).toBe("");
|
|
expect(exitCode).toBe(0);
|
|
expect(parsePrintedEnv(Buffer.concat(stdoutChunks).toString())).toEqual({
|
|
CUSTOM: "spawn-overlay",
|
|
ELECTRON_NO_ATTACH_CONSOLE: null,
|
|
ELECTRON_RUN_AS_NODE: null,
|
|
PASEO_DESKTOP_MANAGED: null,
|
|
PASEO_NODE_ENV: null,
|
|
PASEO_SUPERVISED: null,
|
|
});
|
|
});
|
|
|
|
test("internal env mode preserves Paseo-owned launcher env", async () => {
|
|
const result = await execCommand(process.execPath, ["-e", printEnvScript], {
|
|
envMode: "internal",
|
|
baseEnv: {
|
|
ELECTRON_RUN_AS_NODE: "1",
|
|
PATH: process.env.PATH,
|
|
PASEO_NODE_ENV: "production",
|
|
},
|
|
envOverlay: {
|
|
CUSTOM: "internal",
|
|
PASEO_SUPERVISED: "1",
|
|
},
|
|
});
|
|
|
|
expect(parsePrintedEnv(result.stdout)).toEqual({
|
|
CUSTOM: "internal",
|
|
ELECTRON_NO_ATTACH_CONSOLE: null,
|
|
ELECTRON_RUN_AS_NODE: "1",
|
|
PASEO_DESKTOP_MANAGED: null,
|
|
PASEO_NODE_ENV: "production",
|
|
PASEO_SUPERVISED: "1",
|
|
});
|
|
});
|
|
|
|
test("does not realpath commands while finalizing external command env", async () => {
|
|
const realpathSpy = vi.spyOn(fs.realpathSync, "native");
|
|
|
|
await execCommand("/some/random/binary", ["--version"], {
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
},
|
|
timeout: 100,
|
|
}).catch(() => {});
|
|
|
|
expect(realpathSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("self node command explicitly enables Electron node mode", async () => {
|
|
const command = buildSelfNodeCommand(["-e", printEnvScript], {
|
|
CUSTOM: "from-helper",
|
|
});
|
|
|
|
const result = await execCommand(command.command, command.args, {
|
|
env: command.env,
|
|
envMode: "internal",
|
|
});
|
|
|
|
expect(parsePrintedEnv(result.stdout)).toEqual({
|
|
CUSTOM: "from-helper",
|
|
ELECTRON_NO_ATTACH_CONSOLE: null,
|
|
ELECTRON_RUN_AS_NODE: "1",
|
|
PASEO_DESKTOP_MANAGED: null,
|
|
PASEO_NODE_ENV: null,
|
|
PASEO_SUPERVISED: null,
|
|
});
|
|
});
|
|
});
|