Skip to content

fix: prefer native realpath for canonical paths#972

Merged
TabishB merged 1 commit intomainfrom
codex/fix-native-realpath
Apr 14, 2026
Merged

fix: prefer native realpath for canonical paths#972
TabishB merged 1 commit intomainfrom
codex/fix-native-realpath

Conversation

@TabishB
Copy link
Copy Markdown
Contributor

@TabishB TabishB commented Apr 14, 2026

Summary

  • prefer Node's native realpath resolver when canonicalizing existing paths
  • ensure Windows short-path aliases like RUNNER~1 are expanded before artifact workflow JSON emits file paths
  • add regression coverage for the canonicalization helper

Context

Testing

  • pnpm run build
  • pnpm exec vitest run test/utils/file-system.test.ts test/commands/artifact-workflow.test.ts

Summary by CodeRabbit

  • Refactor

    • Improved file path resolution with native filesystem APIs and fallback mechanisms.
  • Tests

    • Added test coverage for the enhanced path canonicalization functionality.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

The PR refactors filesystem API imports to use nodeFs as the primary source and updates canonicalizeExistingPath() to prefer native realpathSync with a two-level fallback strategy (realpathSync then path.resolve). Test coverage for the updated method is added.

Changes

Cohort / File(s) Summary
Filesystem Implementation
src/utils/file-system.ts
Consolidated filesystem imports to use nodeFs and restructured canonicalizeExistingPath() to attempt native realpath resolution first, with fallback to standard realpath and path resolution.
Test Coverage
test/utils/file-system.test.ts
Added test suite for canonicalizeExistingPath() that verifies native realpathSync is invoked and validates the returned path value.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • fix: canonicalize workflow artifact paths #971: Both PRs modify the canonicalizeExistingPath() implementation in src/utils/file-system.ts, with this PR updating filesystem imports and introducing a native realpath preference strategy.

Suggested reviewers

  • alfred-openspec

Poem

🐰 Hopping through paths with native grace,
We find the true canonical place,
Fallbacks stand ready, three levels deep,
Real paths to canonize, promises to keep!
Tests verify our burrow is sound,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: prefer native realpath for canonical paths' clearly and specifically describes the main change: adopting Node's native realpath resolver for path canonicalization.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-native-realpath

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 and usage tips.

Copy link
Copy Markdown
Collaborator

@alfred-openspec alfred-openspec left a comment

Choose a reason for hiding this comment

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

Looks good. Native realpath is the right fix here, and the regression test covers the intended behavior.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/utils/file-system.test.ts (1)

96-110: Add fallback-branch regression tests for full canonicalization contract.

This covers the native path, but the updated logic also depends on two fallbacks. Please add tests for:

  1. native throws → realpathSync is used,
  2. both throw → path.resolve is returned.
    That will lock in the behavior introduced in src/utils/file-system.ts Line 65-Line 69.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils/file-system.test.ts` around lines 96 - 110, Add two regression
tests for FileSystemUtils.canonicalizeExistingPath: one where
nodeFs.realpathSync.native is made to throw (spy to throw), assert that
nodeFs.realpathSync was called and its result returned; and one where both
nodeFs.realpathSync.native and nodeFs.realpathSync are made to throw, assert
that path.resolve was used and its value returned. In each test, create a
temporary file like the existing test, use vi.spyOn on
nodeFs.realpathSync.native and nodeFs.realpathSync to simulate throws or
returns, call FileSystemUtils.canonicalizeExistingPath(filePath), assert the
correct fallback behavior (calls and return values), and restore mocks after
each test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/utils/file-system.test.ts`:
- Around line 101-108: The test creates a spy on nodeFs.realpathSync.native and
manually restores it with nativeSpy.mockRestore, but restoration can be skipped
if assertions throw; wrap the spy usage around the call to
FileSystemUtils.canonicalizeExistingPath and the expect assertions in a
try/finally block and call nativeSpy.mockRestore() in the finally to guarantee
cleanup even on failure.

---

Nitpick comments:
In `@test/utils/file-system.test.ts`:
- Around line 96-110: Add two regression tests for
FileSystemUtils.canonicalizeExistingPath: one where nodeFs.realpathSync.native
is made to throw (spy to throw), assert that nodeFs.realpathSync was called and
its result returned; and one where both nodeFs.realpathSync.native and
nodeFs.realpathSync are made to throw, assert that path.resolve was used and its
value returned. In each test, create a temporary file like the existing test,
use vi.spyOn on nodeFs.realpathSync.native and nodeFs.realpathSync to simulate
throws or returns, call FileSystemUtils.canonicalizeExistingPath(filePath),
assert the correct fallback behavior (calls and return values), and restore
mocks after each test.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 61ab8a0d-371a-4b68-9447-d20ac3ea6f48

📥 Commits

Reviewing files that changed from the base of the PR and between 7d07101 and 4936057.

📒 Files selected for processing (2)
  • src/utils/file-system.ts
  • test/utils/file-system.test.ts

Comment on lines +101 to +108
const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');

const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);

expect(nativeSpy).toHaveBeenCalledWith(filePath);
expect(resolved).toBe(nodeFs.realpathSync.native(filePath));

nativeSpy.mockRestore();
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking Vitest config for automatic mock restoration..."
fd -i 'vitest.config.*' | while read -r file; do
  echo "---- ${file} ----"
  rg -n -C2 'restoreMocks|clearMocks|mockReset' "$file" || true
done

echo
echo "Checking test setup hooks for global mock cleanup..."
rg -n -C2 "afterEach\\(.*vi\\.(restoreAllMocks|resetAllMocks|clearAllMocks)" --type ts --type js || true

Repository: Fission-AI/OpenSpec

Length of output: 200


🏁 Script executed:

# Check the full vitest.config.ts file
cat vitest.config.ts

Repository: Fission-AI/OpenSpec

Length of output: 1419


🏁 Script executed:

# Search for any setup files referenced in config or at standard locations
fd -i 'setup.*\.(ts|js)$' | head -20

Repository: Fission-AI/OpenSpec

Length of output: 78


🏁 Script executed:

# Search for afterEach hooks in all test files to see if there's a pattern of manual cleanup
rg -n "afterEach\(" test/ --type ts | head -10

Repository: Fission-AI/OpenSpec

Length of output: 664


🏁 Script executed:

# Check the actual test file context around the code snippet
cat test/utils/file-system.test.ts | head -120 | tail -40

Repository: Fission-AI/OpenSpec

Length of output: 1436


🏁 Script executed:

# Check vitest.setup.ts content
cat vitest.setup.ts

Repository: Fission-AI/OpenSpec

Length of output: 542


🏁 Script executed:

# Get the afterEach hook at line 17 in the test file with context
sed -n '15,30p' test/utils/file-system.test.ts

Repository: Fission-AI/OpenSpec

Length of output: 520


🏁 Script executed:

# Get the full import section and context around the spy test
sed -n '1,25p' test/utils/file-system.test.ts

Repository: Fission-AI/OpenSpec

Length of output: 863


🏁 Script executed:

# Get the full test block around the spy test to see if there are multiple tests that could be affected
sed -n '96,125p' test/utils/file-system.test.ts

Repository: Fission-AI/OpenSpec

Length of output: 1161


Wrap spy cleanup in try/finally for guaranteed mock restoration.

Vitest auto-restore is not enabled in this project's configuration. The spy created on line 101 is manually restored on line 108, but if any assertion fails before then, the cleanup will not run, leaving the mock in a dirty state. Use try/finally to guarantee restoration:

Suggested patch
-      const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');
-
-      const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);
-
-      expect(nativeSpy).toHaveBeenCalledWith(filePath);
-      expect(resolved).toBe(nodeFs.realpathSync.native(filePath));
-
-      nativeSpy.mockRestore();
+      const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');
+      try {
+        const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);
+        expect(nativeSpy).toHaveBeenCalledWith(filePath);
+        expect(resolved).toBe(nodeFs.realpathSync.native(filePath));
+      } finally {
+        nativeSpy.mockRestore();
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');
const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);
expect(nativeSpy).toHaveBeenCalledWith(filePath);
expect(resolved).toBe(nodeFs.realpathSync.native(filePath));
nativeSpy.mockRestore();
const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');
try {
const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);
expect(nativeSpy).toHaveBeenCalledWith(filePath);
expect(resolved).toBe(nodeFs.realpathSync.native(filePath));
} finally {
nativeSpy.mockRestore();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/utils/file-system.test.ts` around lines 101 - 108, The test creates a
spy on nodeFs.realpathSync.native and manually restores it with
nativeSpy.mockRestore, but restoration can be skipped if assertions throw; wrap
the spy usage around the call to FileSystemUtils.canonicalizeExistingPath and
the expect assertions in a try/finally block and call nativeSpy.mockRestore() in
the finally to guarantee cleanup even on failure.

@TabishB TabishB added this pull request to the merge queue Apr 14, 2026
Merged via the queue into main with commit 93f7b79 Apr 14, 2026
9 checks passed
@TabishB TabishB deleted the codex/fix-native-realpath branch April 14, 2026 07:45
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.

2 participants