Skip to content

fix: align windows path tests with runtime behavior#45870

Open
NickHood1984 wants to merge 3 commits intoopenclaw:mainfrom
NickHood1984:codex/fix-windows-path-tests
Open

fix: align windows path tests with runtime behavior#45870
NickHood1984 wants to merge 3 commits intoopenclaw:mainfrom
NickHood1984:codex/fix-windows-path-tests

Conversation

@NickHood1984
Copy link
Copy Markdown
Contributor

Summary

  • make Windows path handling and executable resolution assertions consistent with the current runtime behavior
  • update schtasks tests to account for Windows cleanup using taskkill.exe instead of the non-Windows killProcessTree helper
  • normalize platform-specific expectations in the affected unit tests

Why

The Windows shard failures inherited by #40791 are unrelated to the watchdog feature itself. This PR isolates the Windows path and runtime test fixes into a reviewable slice.

Validation

  • pnpm exec vitest run --config vitest.unit.config.ts src/daemon/schtasks.startup-fallback.test.ts src/daemon/schtasks.stop.test.ts src/infra/home-dir.test.ts src/infra/update-global.test.ts src/infra/exec-approvals-store.test.ts src/infra/executable-path.test.ts src/infra/exec-allowlist-pattern.test.ts src/shared/config-eval.test.ts src/infra/run-node.test.ts src/infra/json-file.test.ts src/infra/stable-node-path.test.ts src/infra/hardlink-guards.test.ts

@openclaw-barnacle openclaw-barnacle bot added the gateway Gateway runtime label Mar 14, 2026
@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce3c5e55c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return input;
}
return input.replace(/^~(?=$|[\\/])/, home);
return path.resolve(input.replace(/^~(?=$|[\\/])/, home));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not resolve unmatched tilde prefixes

expandHomePrefix now wraps the replacement in path.resolve(...), but the regex only replaces ~ when followed by end-of-string or a path separator. Inputs like ~alice/bin/tool therefore remain unchanged by replace and then get coerced into a cwd-relative absolute path (for example /cwd/~alice/bin/tool), which silently changes a non-home token into a different filesystem target. This can mis-resolve command paths and allowlist patterns that start with ~ but are not home-prefix syntax.

Useful? React with 👍 / 👎.

Comment on lines +40 to 42
if (process.platform === "win32") {
return getWindowsExecutableExtensions(env).includes(path.extname(filePath).toLowerCase());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip PATHEXT gating for explicit executable files

On Windows this new branch treats a file as executable only if its extension appears in PATHEXT, even for explicit absolute/relative paths. PATHEXT is only meant for extension lookup when no extension is provided; with this change, resolveExecutablePath can reject a real file like C:\...\node.exe whenever the provided environment narrows PATHEXT (for example to .CMD), causing false negatives in command resolution under sanitized/custom envs.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR isolates Windows-specific test fixes from PR #40791, aligning unit test assertions with actual Windows runtime behaviour across path handling, executable resolution, and scheduled task cleanup. The changes are correct and well-scoped.

Key changes:

  • executable-path.ts: isExecutableFile now performs PATHEXT extension-based detection on Windows instead of silently skipping the X_OK check — this is a meaningful correctness fix, not just a test tweak.
  • home-dir.ts: expandHomePrefix wraps its result in path.resolve, ensuring tilde-expanded paths are always absolute with OS-native separators.
  • stable-node-path.ts: Regex extended to tolerate backslash separators and node.exe; introduces a custom joinStablePath helper. Since Homebrew is macOS-only, the Windows path support is largely dead code, and joinStablePath doesn't normalise double separators the way path.join would (see inline comment).
  • Test files: Platform guards (process.platform !== "win32"), path.join/path.resolve normalisation, .cmd executable suffixes, and setPlatform("darwin") anchor tests that should not exercise Windows code paths.
  • schtasks tests: New expectGatewayTermination helpers correctly branch on win32 vs non-Windows for killProcessTree assertions.

Confidence Score: 4/5

  • Safe to merge; changes are well-contained test and minor implementation fixes with no regressions on non-Windows paths.
  • The core logic changes in executable-path.ts and home-dir.ts are correct and confirmed by updated tests. The two minor style issues in stable-node-path.ts (case-sensitive formula guard after a case-insensitive regex, and raw string join vs path.join) are unlikely to cause real-world problems but are worth addressing before they bite. All other changes are test-only adjustments that improve cross-platform reliability.
  • src/infra/stable-node-path.ts — review the formula === "node" guard and joinStablePath helper.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/stable-node-path.ts
Line: 33

Comment:
**Case-insensitive regex but case-sensitive formula guard**

The regex was made case-insensitive with the `i` flag, meaning `cellarMatch[2]` (the `formula` capture) preserves the original casing from the input path. The `formula === "node"` guard directly below is a strict equality check — if a path ever contained `NODE` or `Node` in the Cellar segment, the fallback to `<prefix>/bin/node` would be silently skipped.

Homebrew always uses lowercase formula names in practice, so this is unlikely to cause real problems today. But since the regex was intentionally made case-insensitive, a consistent fix would be:

```suggestion
  if (formula.toLowerCase() === "node") {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/stable-node-path.ts
Line: 20-21

Comment:
**Custom path join bypasses normalisation**

`joinStablePath` concatenates parts with a raw separator character rather than using `path.join` (or `path.posix.join`/`path.win32.join`). If `prefix` happens to have a trailing separator (e.g. from `cellarMatch[1]` ending with `/`), the resulting path would contain a doubled slash like `/opt/homebrew//opt/node/bin/node`.

The new regex uses `[\\/]+` (greedy, one-or-more), so the lazy `(.+?)` stops before the first run of slashes, giving a clean prefix in well-formed paths. However, using `path.posix.join` / `path.win32.join` (selected by the same `nodePath.includes("\\")` heuristic) would be more robust:

```ts
const join = nodePath.includes("\\") ? path.win32.join : path.posix.join;
const optPath = join(prefix, "opt", formula, "bin", binaryName);
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: ce3c5e5

}

// For the default "node" formula, also try the direct bin symlink.
if (formula === "node") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case-insensitive regex but case-sensitive formula guard

The regex was made case-insensitive with the i flag, meaning cellarMatch[2] (the formula capture) preserves the original casing from the input path. The formula === "node" guard directly below is a strict equality check — if a path ever contained NODE or Node in the Cellar segment, the fallback to <prefix>/bin/node would be silently skipped.

Homebrew always uses lowercase formula names in practice, so this is unlikely to cause real problems today. But since the regex was intentionally made case-insensitive, a consistent fix would be:

Suggested change
if (formula === "node") {
if (formula.toLowerCase() === "node") {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/stable-node-path.ts
Line: 33

Comment:
**Case-insensitive regex but case-sensitive formula guard**

The regex was made case-insensitive with the `i` flag, meaning `cellarMatch[2]` (the `formula` capture) preserves the original casing from the input path. The `formula === "node"` guard directly below is a strict equality check — if a path ever contained `NODE` or `Node` in the Cellar segment, the fallback to `<prefix>/bin/node` would be silently skipped.

Homebrew always uses lowercase formula names in practice, so this is unlikely to cause real problems today. But since the regex was intentionally made case-insensitive, a consistent fix would be:

```suggestion
  if (formula.toLowerCase() === "node") {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +20 to +21
const separator = nodePath.includes("\\") ? "\\" : "/";
const joinStablePath = (...parts: string[]) => parts.join(separator);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom path join bypasses normalisation

joinStablePath concatenates parts with a raw separator character rather than using path.join (or path.posix.join/path.win32.join). If prefix happens to have a trailing separator (e.g. from cellarMatch[1] ending with /), the resulting path would contain a doubled slash like /opt/homebrew//opt/node/bin/node.

The new regex uses [\\/]+ (greedy, one-or-more), so the lazy (.+?) stops before the first run of slashes, giving a clean prefix in well-formed paths. However, using path.posix.join / path.win32.join (selected by the same nodePath.includes("\\") heuristic) would be more robust:

const join = nodePath.includes("\\") ? path.win32.join : path.posix.join;
const optPath = join(prefix, "opt", formula, "bin", binaryName);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/stable-node-path.ts
Line: 20-21

Comment:
**Custom path join bypasses normalisation**

`joinStablePath` concatenates parts with a raw separator character rather than using `path.join` (or `path.posix.join`/`path.win32.join`). If `prefix` happens to have a trailing separator (e.g. from `cellarMatch[1]` ending with `/`), the resulting path would contain a doubled slash like `/opt/homebrew//opt/node/bin/node`.

The new regex uses `[\\/]+` (greedy, one-or-more), so the lazy `(.+?)` stops before the first run of slashes, giving a clean prefix in well-formed paths. However, using `path.posix.join` / `path.win32.join` (selected by the same `nodePath.includes("\\")` heuristic) would be more robust:

```ts
const join = nodePath.includes("\\") ? path.win32.join : path.posix.join;
const optPath = join(prefix, "opt", formula, "bin", binaryName);
```

How can I resolve this? If you propose a fix, please make it concise.

…ath-tests

# Conflicts:
#	src/daemon/schtasks.startup-fallback.test.ts
#	src/daemon/schtasks.stop.test.ts
#	src/infra/exec-allowlist-pattern.test.ts
#	src/infra/executable-path.test.ts
#	src/infra/executable-path.ts
#	src/infra/hardlink-guards.test.ts
#	src/infra/json-file.test.ts
#	src/infra/stable-node-path.ts
#	src/shared/config-eval.test.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7b069bace

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return input;
}
return input.replace(/^~(?=$|[\\/])/, home);
return path.resolve(input.replace(/^~(?=$|[\\/])/, home));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unmatched tilde tokens in home prefix expansion

Wrapping the replacement in path.resolve(...) changes non-home tokens like ~alice/bin into cwd-anchored absolute paths, because the regex only replaces bare ~ or ~/.../~\\... and leaves ~alice untouched. Fresh evidence: callers like resolveAllowlistCandidatePath and resolveExecutablePath then treat that rewritten value as a concrete filesystem path, so an unresolved token can silently target <cwd>/~alice/... instead of remaining unresolved.

Useful? React with 👍 / 👎.

Comment on lines 40 to 42
if (process.platform === "win32") {
const ext = path.extname(filePath).toLowerCase();
if (!ext) {
return true;
}
return resolveWindowsExecutableExtSet(undefined).has(ext);
return getWindowsExecutableExtensions(env).includes(path.extname(filePath).toLowerCase());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass PATHEXT checks for already-qualified executables

This Windows branch now requires the file extension to be present in PATHEXT even after a concrete file path has already been found, which can reject real binaries such as node.exe under restricted envs (for example PATHEXT=.CMD). Fresh evidence: resolveExecutableFromPathEnv now forwards the caller env into isExecutableFile, so PATH lookups of commands that already include an extension can incorrectly return undefined despite the file existing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants