Run server tests on Linux+Windows matrix (#809)

* 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.
This commit is contained in:
Mohamed Boudra
2026-05-08 10:57:22 +08:00
committed by GitHub
parent 39e461b872
commit c4e4a28bc0
44 changed files with 5673 additions and 4899 deletions
@@ -2,9 +2,10 @@ import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSyn
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { isPlatform } from "../test-utils/platform.js";
import { searchHomeDirectories, searchWorkspaceEntries } from "./directory-suggestions.js";
const isWindows = process.platform === "win32";
const isWindows = isPlatform("win32");
describe("searchHomeDirectories", () => {
let tempRoot: string;
@@ -18,6 +19,8 @@ describe("searchHomeDirectories", () => {
mkdirSync(homeDir, { recursive: true });
mkdirSync(outsideDir, { recursive: true });
homeDir = realpathSync(homeDir);
outsideDir = realpathSync(outsideDir);
mkdirSync(path.join(homeDir, "projects", "paseo"), { recursive: true });
mkdirSync(path.join(homeDir, "projects", "playground"), { recursive: true });
@@ -26,7 +29,9 @@ describe("searchHomeDirectories", () => {
writeFileSync(path.join(homeDir, "projects", "README.md"), "not a directory\n");
mkdirSync(path.join(outsideDir, "outside-match"), { recursive: true });
symlinkSync(path.join(outsideDir, "outside-match"), path.join(homeDir, "outside-link"));
if (!isWindows) {
symlinkSync(path.join(outsideDir, "outside-match"), path.join(homeDir, "outside-link"));
}
});
afterEach(() => {
@@ -50,8 +55,9 @@ describe("searchHomeDirectories", () => {
limit: 10,
});
expect(results).toContain(path.join(homeDir, "projects"));
expect(results).toContain(path.join(homeDir, "projects", "paseo"));
const resolvedResults = results.map((result) => realpathSync.native(result));
expect(resolvedResults).toContain(realpathSync.native(path.join(homeDir, "projects")));
expect(resolvedResults).toContain(realpathSync.native(path.join(homeDir, "projects", "paseo")));
expect(results).not.toContain(path.join(homeDir, "projects", "README.md"));
});
@@ -62,7 +68,9 @@ describe("searchHomeDirectories", () => {
limit: 10,
});
expect(results).toEqual([path.join(homeDir, "projects", "paseo")]);
expect(results.map((result) => realpathSync.native(result))).toEqual([
realpathSync.native(path.join(homeDir, "projects", "paseo")),
]);
});
it("prioritizes exact segment matches before segment-prefix matches", async () => {
@@ -77,8 +85,9 @@ describe("searchHomeDirectories", () => {
limit: 30,
});
const exactIndex = results.indexOf(exactSegmentPath);
const prefixIndex = results.indexOf(prefixSegmentPath);
const resolvedResults = results.map((result) => realpathSync.native(result));
const exactIndex = resolvedResults.indexOf(realpathSync.native(exactSegmentPath));
const prefixIndex = resolvedResults.indexOf(realpathSync.native(prefixSegmentPath));
expect(exactIndex).toBeGreaterThanOrEqual(0);
expect(prefixIndex).toBeGreaterThanOrEqual(0);
expect(exactIndex).toBeLessThan(prefixIndex);
@@ -96,8 +105,9 @@ describe("searchHomeDirectories", () => {
limit: 30,
});
const earlierIndex = results.indexOf(earlierPath);
const laterIndex = results.indexOf(laterPath);
const resolvedResults = results.map((result) => realpathSync.native(result));
const earlierIndex = resolvedResults.indexOf(realpathSync.native(earlierPath));
const laterIndex = resolvedResults.indexOf(realpathSync.native(laterPath));
expect(earlierIndex).toBeGreaterThanOrEqual(0);
expect(laterIndex).toBeGreaterThanOrEqual(0);
expect(earlierIndex).toBeLessThan(laterIndex);
@@ -125,7 +135,8 @@ describe("searchHomeDirectories", () => {
expect(results).not.toContain(path.join(homeDir, ".hidden", "cache"));
});
it("does not return paths that escape home through symlinks", async () => {
// POSIX-only: creates and follows a symlink escape fixture.
it.skipIf(isWindows)("does not return paths that escape home through symlinks", async () => {
const results = await searchHomeDirectories({
homeDir,
query: "outside",
@@ -170,7 +181,9 @@ describe("searchWorkspaceEntries", () => {
);
writeFileSync(path.join(workspaceDir, "docs", "notes.md"), "notes\n");
symlinkSync(path.join(outsideDir, "escaped"), path.join(workspaceDir, "escaped-link"));
if (!isWindows) {
symlinkSync(path.join(outsideDir, "escaped"), path.join(workspaceDir, "escaped-link"));
}
});
afterEach(() => {
@@ -214,28 +227,32 @@ describe("searchWorkspaceEntries", () => {
expect(filesOnly).toEqual([{ path: "README.md", kind: "file" }]);
});
it("supports path-style queries and does not escape cwd through symlinks", async () => {
const pathResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "src/co",
limit: 20,
includeFiles: true,
includeDirectories: true,
});
expect(pathResults).toContainEqual({
path: "src/components",
kind: "directory",
});
// POSIX-only: creates and follows a symlink escape fixture.
it.skipIf(isWindows)(
"supports path-style queries and does not escape cwd through symlinks",
async () => {
const pathResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "src/co",
limit: 20,
includeFiles: true,
includeDirectories: true,
});
expect(pathResults).toContainEqual({
path: "src/components",
kind: "directory",
});
const escapedResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "escaped",
limit: 20,
includeFiles: true,
includeDirectories: true,
});
expect(escapedResults.some((entry) => entry.path.includes("escaped-link"))).toBe(false);
});
const escapedResults = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "escaped",
limit: 20,
includeFiles: true,
includeDirectories: true,
});
expect(escapedResults.some((entry) => entry.path.includes("escaped-link"))).toBe(false);
},
);
it("ignores node_modules entries so deep workspace files still resolve under scan limits", async () => {
mkdirSync(path.join(workspaceDir, "packages", "app", "src", "app"), { recursive: true });