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".

Comment thread src/infra/home-dir.ts
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 thread src/infra/stable-node-path.ts Outdated
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".

Comment thread src/infra/home-dir.ts
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 👍 / 👎.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: needs changes before merge.

Summary
The PR updates Windows-specific path and schtasks tests and modifies executable lookup, home-prefix expansion, and stable Homebrew Node path matching helpers.

Reproducibility: yes. Source inspection of the PR diff gives helper-level reproductions: ~alice/bin/tool is resolved after a non-match, and caller PATHEXT is applied to already-qualified executables.

Next step before merge
A bounded repair can keep the Windows test normalization while removing the home-prefix and PATHEXT regressions and adding focused regression coverage.

Security
Needs attention: Needs attention because the PR changes executable and approval-path helper semantics, though it does not touch workflows, dependencies, lockfiles, or release scripts.

Review findings

  • [P2] Preserve unmatched tilde tokens — src/infra/home-dir.ts:73
  • [P2] Keep PATHEXT out of explicit file checks — src/infra/executable-path.ts:41
  • [P3] Normalize formula casing before fallback — src/infra/stable-node-path.ts:32
Review details

Best possible solution:

Keep the Windows and schtasks test normalization, preserve current home and executable resolution semantics, and add focused regressions for unmatched tilde tokens, restricted PATHEXT, and stable-node casing.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection of the PR diff gives helper-level reproductions: ~alice/bin/tool is resolved after a non-match, and caller PATHEXT is applied to already-qualified executables.

Is this the best way to solve the issue?

No. The schtasks assertion update is aligned with current runtime, but the production helper changes should be narrowed or covered by regressions instead of changing approval and executable semantics as a side effect.

Full review comments:

  • [P2] Preserve unmatched tilde tokens — src/infra/home-dir.ts:73
    Wrapping the replacement in path.resolve(...) also resolves inputs the regex leaves untouched. A value like ~alice/bin/tool becomes cwd-absolute, and callers feed this helper into exec allowlist and command path logic, so an unresolved token can silently target a different filesystem path.
    Confidence: 0.92
  • [P2] Keep PATHEXT out of explicit file checks — src/infra/executable-path.ts:41
    Passing caller env into isExecutableFile makes Windows reject an already-found file when its extension is not in PATHEXT. With PATHEXT=.CMD, a concrete node.exe path can return undefined even though the executable exists; PATHEXT should drive suffix search, not invalidate qualified paths.
    Confidence: 0.88
  • [P3] Normalize formula casing before fallback — src/infra/stable-node-path.ts:32
    The Cellar matcher is case-insensitive, but the fallback still checks formula === "node". A path like Cellar/Node/.../node.exe matches only because of the i flag and then skips the <prefix>/bin/node.exe fallback.
    Confidence: 0.62

Overall correctness: patch is incorrect
Overall confidence: 0.88

Security concerns:

  • [low] Home-prefix expansion can retarget approval paths — src/infra/home-dir.ts:73
    The proposed path.resolve(...) wrapper can turn an unmatched ~user/... token into a cwd-relative absolute path. Because this helper feeds exec allowlist and command-resolution paths, that can change what an approved pattern or command target refers to.
    Confidence: 0.82
  • [low] PATHEXT gating can deny explicit executables — src/infra/executable-path.ts:41
    Forwarding caller PATHEXT into isExecutableFile makes explicit files depend on extension-search policy, which can break host exec resolution under restricted environments and create surprising allowlist misses for concrete binaries.
    Confidence: 0.78

Acceptance criteria:

  • pnpm test src/daemon/schtasks.startup-fallback.test.ts src/daemon/schtasks.stop.test.ts src/infra/home-dir.test.ts src/infra/executable-path.test.ts src/infra/exec-command-resolution.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/stable-node-path.test.ts
  • pnpm check:changed

What I checked:

  • PR retargets unmatched tilde tokens: The PR wraps expandHomePrefix replacement in path.resolve(...); inputs that the regex intentionally leaves alone, such as ~alice/bin/tool, become cwd-absolute paths. (src/infra/home-dir.ts:73, e7b069bacec1)
  • Current main preserves replacement-only expansion: Current main returns the regex replacement result directly, so unmatched tilde tokens remain unresolved. (src/infra/home-dir.ts:99, ecd562b2b5f0)
  • PR gates found executables on caller PATHEXT: The PR passes caller env into isExecutableFile, so a concrete Windows executable can be rejected when its extension is absent from the supplied PATHEXT. (src/infra/executable-path.ts:41, e7b069bacec1)
  • Current main separates suffix lookup from file validation: Current main uses PATHEXT for extensionless search but validates found files through isExecutableFile(candidate) without the caller env. (src/infra/executable-path.ts:93, ecd562b2b5f0)
  • Schtasks test direction matches runtime: Current runtime uses killProcessTree off Windows and shells out to taskkill.exe on Windows, so the schtasks assertion normalization is directionally correct. (src/daemon/schtasks.ts:488, ecd562b2b5f0)
  • Prior review context: Existing review comments already flagged the same P2 tilde and PATHEXT blockers plus a P3 stable-node casing issue, and the PR discussion asked whether runtime code changed beyond tests. (e7b069bacec1)

Likely related people:

  • Vincent Koc: Current-main blame in this checkout points the relevant infra and schtasks lines to the recent grafted snapshot commit authored by Vincent Koc. (role: recent maintainer/current-main snapshot owner; confidence: medium; commits: f275e9d4b91b, ecd562b2b5f0; files: src/infra/home-dir.ts, src/infra/executable-path.ts, src/infra/stable-node-path.ts)
  • dsantoreis: Commented on this PR asking whether the Windows test alignment also changed runtime helpers, which is the central merge risk here. (role: reviewer/adjacent maintainer; confidence: medium; files: src/infra/executable-path.ts, src/infra/home-dir.ts, src/daemon/schtasks.stop.test.ts)
  • BradGroux: Maintainer-authored Microsoft/Windows tracker PRs cross-reference this Windows-platform PR and group it with the broader Windows cleanup backlog. (role: adjacent Windows backlog maintainer; confidence: low; files: src/daemon/schtasks.ts, src/infra/executable-path.ts)

Remaining risk / open question:

  • The branch base predates current main, so a repair should refresh the diff before validation.
  • The PR touches exec approval and command-resolution helpers, so merging the current production helper changes could retarget or reject executable paths.

Codex review notes: model gpt-5.5, reasoning high; reviewed against ecd562b2b5f0.

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