Skip to content

fix(app): Find resolves programs on Windows — PATHEXT candidates, no mode bit - #22

Merged
Snider merged 1 commit into
mainfrom
fix/app-find-windows-executables
Aug 8, 2026
Merged

fix(app): Find resolves programs on Windows — PATHEXT candidates, no mode bit#22
Snider merged 1 commit into
mainfrom
fix/app-find-windows-executables

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes the Program.Find: "git": not found in PATH arm of the go-inference Windows suite (3 packages: agent/orchestrator, agent/provider, engine/driver — see the classified 34 in go-inference's ledger).

Three defects, one function, all invisible from POSIX runners:

  1. mode&0111 on a platform with no execute bit — Stat synthesises 0666/0444 on Windows, so isExecutable rejected every file; Find could never succeed there. Same defect class go-process fixed in v0.16.2.
  2. No PATHEXT candidatesPathJoin(dir, "git") never tries git.exe, so the mode fix alone changes nothing.
  3. Path-vs-name matched only the platform separatorbin/tool (valid on Windows) was hunted on PATH instead of checked directly.

Shape mirrors go-process's proven fix: findWith/isExecutableWith take PATH + PATHEXT as arguments and the extension list is the platform switch — empty = POSIX semantics, non-empty = Windows semantics — so the Windows rules are fixture-tested from any host. Find passes the real environment, defaulting PATHEXT to Go's own .COM;.EXE;.BAT;.CMD when unset on Windows.

Receipt: TestApp_isExecutableWith_Good is a 0644 git.exe accepted under a listed extension — the exact file the old logic rejected, failing against the old code for the right reason. Full module green, vet + gofmt clean, existing Find/isExecutable triplets pass unmodified (POSIX behaviour byte-identical).

core/go is review-only: this awaits Snider's review alongside #21 — together they clear 33 of go-inference's 34 remaining Windows failures.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

Summary by CodeRabbit

  • Bug Fixes

    • Improved executable discovery on Windows using PATHEXT and standard executable extensions.
    • Direct paths now work with both Windows and POSIX path separators.
    • Windows executable files are recognised without requiring POSIX execute permissions.
    • Enhanced PATH searches and direct executable checks for more reliable cross-platform behaviour.
  • Tests

    • Added comprehensive coverage for Windows extension handling, PATH lookup, direct paths, and edge cases.

…mode bit

Three defects, one function, all Windows-fatal and all invisible from a
POSIX runner:

1. isExecutable asked mode&0111. Windows has no execute bit — Stat
   synthesises 0666, or 0444 for read-only — so the test rejected EVERY
   file on the platform, git.exe included, and Find could never succeed
   there. This is the 'Program.Find: "git": not found in PATH' arm of
   the go-inference Windows failures (3 packages), and the same defect
   go-process fixed in v0.16.2.
2. No extension candidates: PathJoin(dir, "git") never tries git.exe,
   so fixing the mode test alone changes nothing.
3. The path-vs-name test matched only the platform separator, so
   "bin/tool" — which Go accepts on Windows — was hunted on PATH
   instead of checked directly.

Shape mirrors go-process's proven fix: findWith/isExecutableWith take
PATH and PATHEXT as arguments, and the extension list IS the platform
switch (empty = POSIX mode-bit semantics, non-empty = Windows PATHEXT
semantics), so every Windows rule is exercised by fixture from any host.
Find passes the real environment, defaulting PATHEXT to Go's own
.COM;.EXE;.BAT;.CMD when unset on Windows.

Receipt: TestApp_isExecutableWith_Good is a 0644 git.exe accepted under
a listed extension — the exact file the old logic rejected. Full module:
ok, vet + gofmt clean. POSIX behaviour byte-identical (existing
Find/isExecutable triplets pass unmodified).

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The executable lookup now supports Windows PATHEXT resolution, default Windows extensions, both path separators, and injected environment values. POSIX lookups retain execute-bit checks. Windows lookups validate regular files by extension.

Executable resolution

Layer / File(s) Summary
Extension handling and validation
app.go, app_internal_test.go
The code parses and normalises PATHEXT, generates filename candidates, and separates POSIX mode checks from Windows extension checks. Tests cover parsing, candidate ordering, directories, listed extensions, and file modes.
Extension-aware executable lookup
app.go, app_internal_test.go
Find delegates environment-dependent work to findWith. PATH and direct-path searches check applicable extension candidates. Tests cover fixture PATH lookups, direct paths, empty PATH values, and non-matching extensions.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: Windows executable resolution using PATHEXT without relying on mode bits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Snider
Snider merged commit 47bfe66 into main Aug 8, 2026
0 of 2 checks passed
@Snider
Snider deleted the fix/app-find-windows-executables branch August 8, 2026 10:37

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app_internal_test.go`:
- Around line 76-103: Add the missing splitPathExt _Ugly test and
executableCandidates _Bad and _Ugly tests alongside the existing coverage.
Exercise boundary and invalid inputs, including whitespace/empty extension
entries and unusual or empty executable names, while preserving the current
_Good and splitPathExt _Bad expectations.

In `@app.go`:
- Around line 50-55: The PATHEXT lookup currently affects POSIX behavior; update
both path-resolution sites in app.go at lines 50-55 and 157-161 to read or
initialize pathExt only when OS() == "windows", leaving it empty on POSIX so
execute-bit checks remain unchanged. Apply the change consistently in both
methods using the existing findWith flow.
- Line 67: Update the path classification condition in the relevant
filename-resolution function to recognize drive-prefixed Windows paths such as
C:git as direct paths, including when extension-based Windows mode is active,
rather than searching PATH. Add a regression test covering this drive-relative
input and asserting direct-path resolution.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b44d23e0-5894-4567-84db-110ee5ca7dfb

📥 Commits

Reviewing files that changed from the base of the PR and between 702e8c4 and a71a9e9.

📒 Files selected for processing (2)
  • app.go
  • app_internal_test.go

Comment thread app_internal_test.go
Comment on lines +76 to +103
func TestApp_splitPathExt_Good(t *T) {
exts := splitPathExt(".COM;.EXE;bat; .Cmd ;")
AssertEqual(t, 4, len(exts))
AssertEqual(t, ".com", exts[0])
AssertEqual(t, ".exe", exts[1])
AssertEqual(t, ".bat", exts[2])
AssertEqual(t, ".cmd", exts[3])
}
func TestApp_splitPathExt_Bad(t *T) {
AssertEqual(t, 0, len(splitPathExt("")))
}
func TestApp_executableCandidates_Good(t *T) {
// A name already carrying a listed extension is tried as itself
// first, then with each extension appended; a bare name only with
// the extensions; POSIX (empty list) is the path alone.
withExt := executableCandidates("dir/git.exe", ".COM;.EXE")
AssertEqual(t, 3, len(withExt))
AssertEqual(t, "dir/git.exe", withExt[0])

bare := executableCandidates("dir/git", ".COM;.EXE")
AssertEqual(t, 2, len(bare))
AssertEqual(t, "dir/git.com", bare[0])
AssertEqual(t, "dir/git.exe", bare[1])

posix := executableCandidates("dir/git", "")
AssertEqual(t, 1, len(posix))
AssertEqual(t, "dir/git", posix[0])
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the mandatory coverage states.

splitPathExt has no _Ugly test. executableCandidates has no _Bad or _Ugly test. Add the required test functions with boundary and invalid-input cases.

As per coding guidelines, “All three coverage states are mandatory: _Good for happy path, _Bad for expected failures, and _Ugly for edge cases”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app_internal_test.go` around lines 76 - 103, Add the missing splitPathExt
_Ugly test and executableCandidates _Bad and _Ugly tests alongside the existing
coverage. Exercise boundary and invalid inputs, including whitespace/empty
extension entries and unusual or empty executable names, while preserving the
current _Good and splitPathExt _Bad expectations.

Source: Coding guidelines

Comment thread app.go
Comment on lines +50 to +55
pathExt := Env("PATHEXT")
if pathExt == "" && OS() == "windows" {
// Go's own default when the variable is unset.
pathExt = ".COM;.EXE;.BAT;.CMD"
}
return a.findWith(filename, name, Env("PATH"), pathExt)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict PATHEXT processing to Windows.

A populated PATHEXT on POSIX changes these methods from execute-bit checks to extension checks. This breaks the stated unchanged POSIX behaviour.

  • app.go#L50-L55: initialise pathExt only when OS() == "windows".
  • app.go#L157-L161: initialise pathExt only when OS() == "windows".
📍 Affects 1 file
  • app.go#L50-L55 (this comment)
  • app.go#L157-L161
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.go` around lines 50 - 55, The PATHEXT lookup currently affects POSIX
behavior; update both path-resolution sites in app.go at lines 50-55 and 157-161
to read or initialize pathExt only when OS() == "windows", leaving it empty on
POSIX so execute-bit checks remain unchanged. Apply the change consistently in
both methods using the existing findWith flow.

Comment thread app.go
// as a separator on Windows too, so both spellings mean "path" —
// matching only the platform separator sent "bin/tool" on a PATH
// hunt there instead of probing it.
if Contains(filename, string(PathSeparator)) || Contains(filename, "/") {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat drive-relative Windows paths as direct paths.

A valid Windows path such as C:git contains neither \ nor /. Line 67 therefore searches PATH instead of resolving the path relative to drive C:. When extension-based Windows mode applies, detect drive-prefixed paths and use the direct-path branch. Add a regression test for this case.

Proposed fix
-	if Contains(filename, string(PathSeparator)) || Contains(filename, "/") {
+	if Contains(filename, string(PathSeparator)) || Contains(filename, "/") ||
+		(len(splitPathExt(pathExt)) != 0 && Contains(filename, ":")) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.go` at line 67, Update the path classification condition in the relevant
filename-resolution function to recognize drive-prefixed Windows paths such as
C:git as direct paths, including when extension-based Windows mode is active,
rather than searching PATH. Add a regression test covering this drive-relative
input and asserting direct-path resolution.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant