Skip to content

fix: resolve Windows path separator inconsistency and make tests OS-independent#1

Merged
flanny7 merged 13 commits intomainfrom
fix/path-backslash
Apr 2, 2026
Merged

fix: resolve Windows path separator inconsistency and make tests OS-independent#1
flanny7 merged 13 commits intomainfrom
fix/path-backslash

Conversation

@flanny7
Copy link
Copy Markdown
Owner

@flanny7 flanny7 commented Mar 26, 2026

Hello,
I always appreciate your work on rulesync.

Following up on Issue dyoshikawa#1251 and related Windows path behaviors, I have created a Pull Request to resolve the path separator inconsistency and make the tests OS-independent.

Description

Closes dyoshikawa#1251

This PR fixes the Windows path separator inconsistency that caused backslashes (\) to appear in contexts requiring forward slashes (/), and implements OS-independent parameterized tests to ensure cross-platform reliability.

Root Cause

path.join() from node:path uses the platform separator — backslash on Windows. The codebase used path.join() in four non-filesystem contexts that require forward slashes:

  1. .gitignore entries — The gitignore spec requires forward slashes. On Windows, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH resolved to .rulesync\skills\.curated, producing an invalid gitignore entry.
  2. GitHub API pathsfetch.ts used path.join(basePath, featurePath) to build repository paths sent to the GitHub Contents API. On Windows this produced paths like packages\shared\rules, which the API cannot resolve.
  3. Rule file @-referencesAiFile.getRelativePathFromCwd() used path.join() to produce display paths that are written verbatim into generated rule files (e.g., @.cursor/rules/sub/my-rule.md in TOON format and Claude Code legacy format). On Windows, when a rule file is in a subdirectory, path.relative() returns sub\my-rule.md, causing getRelativePathFromCwd() to produce @.cursor/rules/sub\my-rule.md — broken syntax.
  4. Directory display pathsAiDir.getRelativePathFromCwd() had the same issue (used for dry-run logging and changed-path reporting).

Affected Components

Component Impact
gitignore command Generated .gitignore contained backslash entries on Windows
fetch command GitHub API requests failed with 404 Not Found on Windows
Non-root rules @-references in generated root rule files contained backslashes on Windows

Changes Made

  • src/constants/rulesync-paths.ts: Changed path.join to path.posix.join so constants always use forward slashes. When these constants are later used in filesystem operations (e.g., path.join(process.cwd(), constant)), path.join normalizes them to the platform separator automatically.
  • src/lib/fetch.ts: Changed join(basePath, featurePath) to posix.join(basePath, featurePath) for the GitHub API path construction.
  • src/types/ai-file.ts & src/types/ai-dir.ts: Added .replace(/\\/g, "/") to getRelativePathFromCwd() so paths written into rule file content always use forward slashes, regardless of what path.join() produces on the host OS.

Testing Strategy & Added Tests

To prevent false positives on macOS/Linux CI runners (where path.join natively returns /), the tests have been designed to be strictly OS-independent:

  • Parameterized Input Testing (src/types/tool-file.test.ts, src/types/ai-dir.test.ts, src/lib/fetch.test.ts): Instead of destructively mocking node:path, we use it.each to explicitly inject both Windows-style (\\) and POSIX-style (/) path strings. This guarantees that the path sanitization logic works consistently across all platforms without breaking native OS testing capabilities.
  • Static Constants & Gitignore (src/constants/rulesync-paths.test.ts, src/cli/commands/gitignore.test.ts): Added assertions to verify that dynamically generated constants and gitignore entries never contain backslashes (.not.toContain("\\")). (Any regressions in base paths will be caught by Windows runners in the CI matrix).

Please let me know if there are any formatting issues or if I missed anything in the contribution guidelines. I would be happy to fix them.

There is no rush at all for the review. Your time and consideration are greatly appreciated.

@flanny7 flanny7 force-pushed the fix/path-backslash branch from 74eea7f to 02a51e4 Compare March 27, 2026 04:25
autaut03 and others added 13 commits March 27, 2026 17:49
GitHub API and .gitignore entries require forward slashes, but path.join()
produces backslashes on Windows. This caused fetch command failures with
subdirectory paths and gitignore entries with mixed separators.

Changes:
- src/constants/rulesync-paths.ts: Use path.posix.join to ensure
  constants always contain forward slashes. When used in filesystem
  operations (e.g., join(cwd, constant)), path.join automatically
  normalizes to platform separators.
- src/lib/fetch.ts: Use posix.join for GitHub API path construction
  instead of platform-dependent join.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add tests verifying that:
- rulesync-paths constants always use forward slashes
- .gitignore entries use only forward slashes
- GitHub API paths never contain backslashes

These tests catch Windows path separator issues that would break fetch
and gitignore commands on Windows systems.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Normalize backslashes to forward slashes in AiFile.getRelativePathFromCwd() and
AiDir.getRelativePathFromCwd(). These methods produce paths that are written
into generated rule file content (e.g., @.cursor/rules/my-rule.md) via
rules-processor.ts. On Windows, when rules are in subdirectories, path.relative()
returns backslash-separated paths, causing getRelativePathFromCwd() to produce
paths with backslashes—invalid syntax for rule files.

The fix applies .replace(/\\/g, "/") to normalize the output, ensuring
cross-platform consistency regardless of the host OS.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add tests to verify that getRelativePathFromCwd() normalizes backslashes to
forward slashes:
- tool-file.test.ts: Test AiFile.getRelativePathFromCwd() with backslash in
  relativeFilePath (simulating Windows path.relative() output)
- ai-dir.test.ts (new): Test AiDir.getRelativePathFromCwd() with backslash in
  relativeDirPath

Both tests verify that the return value never contains backslashes, ensuring
paths written into rule file content are always cross-platform compatible.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Replaced host-OS-dependent path tests with `it.each` blocks that explicitly test both Windows-style and POSIX-style path inputs. This prevents false positives on macOS/Linux CI runners and ensures path sanitization logic is robust without destructively mocking `node:path`.
Fix it.each test in tool-file.test.ts that declared relativeDirPath and
relativeFilePath parameters but hardcoded values in the TestToolFile constructor,
making the Windows-style path case never actually exercised.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@flanny7 flanny7 force-pushed the fix/path-backslash branch from 697d957 to 28a23fe Compare March 29, 2026 13:02
@flanny7 flanny7 merged commit d0a3241 into main Apr 2, 2026
8 checks passed
flanny7 pushed a commit that referenced this pull request Apr 2, 2026
…arator fix

- Add explanatory comments in ai-file.ts and ai-dir.ts for why
  .replace() is used instead of path.posix.join (#1)
- Improve fetch.test.ts with parameterized Windows-style backslash
  path test inputs via it.each (#2)
- Normalize backslashes in fetch.ts resolvedPath for API compatibility
- Update coding-guidelines.md to distinguish filesystem paths
  (path.join) from semantic/API paths (path.posix.join) (dyoshikawa#3)

Closes dyoshikawa#1394

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
flanny7 pushed a commit that referenced this pull request Apr 2, 2026
…d gitignore sync

- Enforce both-or-neither opts in buildDeletionRulesFromPaths (#1)
- Rename destructured param in fromRootFile for clarity (#2)
- Remove redundant optional chaining after nonRoot guard (dyoshikawa#3)
- Remove duplicate **/.rovodev/ gitignore entry (#4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
flanny7 pushed a commit that referenced this pull request Apr 22, 2026
- #1: use static import for resetDeprecationWarningForTests in tests
- #2: document why mutual-exclusivity is runtime-enforced, not a discriminated union
- dyoshikawa#3: stop emitting the deprecation warning from the Config constructor;
  the ConfigResolver is now the single emission point
- #4: cache validated ToolTarget[] for object-form targets in the constructor
  so getTargets() no longer rebuilds the ALL_TOOL_TARGETS set per call
- #5: fix misleading schema comment that claimed unknown-target warnings
  (the runtime path actually throws)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
flanny7 pushed a commit that referenced this pull request Apr 22, 2026
…nd match-all bypasses

- Reject imported rules whose toolName maps to __proto__, constructor,
  or prototype to prevent prototype pollution when round-tripping
  untrusted TOML; use Object.hasOwn for lookups to avoid hitting
  inherited accessors. (Sec #1)
- Stop translating glob character classes to regex classes; emit '['
  and ']' as literals so that negated ([^a]) or wide-range ([!-~])
  classes cannot bypass the JSON field-boundary guard. (Sec #2)
- Skip empty patterns ('') with a warning (would match every bash
  invocation or nothing for other tools). Skip bash '*' and '**'
  with allow/deny decisions because they would silently grant or
  revoke every shell command; 'ask' remains supported. (Sec dyoshikawa#3)
- Update docs to reflect the new guardrails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flanny7 pushed a commit that referenced this pull request Apr 22, 2026
Address Round 2 review findings for PR dyoshikawa#1526:

- HIGH-R2-#1: guard the stale-file cleanup loop in apm-install.ts
  against path traversal. Attacker-controlled deployed_files entries
  with ".." segments or absolute paths are now rejected by shape and
  via checkPathTraversal, with a warn log per offending entry, so a
  hostile lockfile cannot drive arbitrary removeFile calls.
- MID-R2-#2: make lockfile ordering deterministic for failed deps.
  The per-dep worker now returns the preserved prior entry via its
  result object, and the sequential post-loop pushes successes or
  preserved entries strictly in manifest order, not in
  promise-completion order.
- MID-R2-dyoshikawa#3: preserve top-level loose fields (mcp_servers and any
  looseObject extras) across lockfile rewrites by carrying forward
  existingLock through createEmptyApmLock.
- MID-R2-#4: relax the content_hash schema to accept arbitrary
  strings on parse so a lockfile produced by the upstream apm CLI
  does not break readApmLock. The --frozen integrity check now only
  compares hashes whose shape matches RULESYNC_CONTENT_HASH_REGEX
  and skips comparison otherwise (commit SHA pin still enforces
  integrity).

Tests added for each finding, including a two-dep ordering
regression and a frozen-mode interop check with a legacy
content_hash value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Incorrect path separators generated for .rulesync/skills/.curated/ by rulesync gitigno

3 participants