Skip to content

fix: cross-platform test failures on Windows (34 tests)#41

Merged
samtuckerdavis merged 1 commit into
mainfrom
fix/windows-test-compat-2026-05-14
May 13, 2026
Merged

fix: cross-platform test failures on Windows (34 tests)#41
samtuckerdavis merged 1 commit into
mainfrom
fix/windows-test-compat-2026-05-14

Conversation

@samtuckerdavis
Copy link
Copy Markdown

@samtuckerdavis samtuckerdavis commented May 13, 2026

Summary

After #40, running the full test suite on Windows surfaced 34 failures that weren't visible in CI (Ubuntu Linux 3.11). All were Windows-specific quirks; none affect Linux. Full suite is now 0 failed, 6830 passed, 6 skipped on Windows Python 3.12.

Each failure was traced to root cause and fixed — no skipif-on-windows shortcuts except where the test genuinely targets a POSIX-only code path.

Categories of fix

Path-separator normalization (7 production files)

Several modules built prefix strings with os.sep (\ on Windows) then compared against POSIX-style paths emitted by other tools or test fixtures. Mismatched separators caused silent miss-matches. Each fix normalizes both sides to / for comparison while preserving native separators in output where production callers expect them.

File Fix
engine/detectors/coupling.py::_rel_to_root Strip drive letter when Path.resolve() produced C:/... but caller still passes POSIX-rooted prefix
engine/detectors/coverage/mapping_analysis.py::_build_prod_by_module Normalize prod_file before prefix-strip; was producing module names like .repo.pkg.util instead of pkg.util
engine/detectors/test_coverage/discovery.py::_normalize_graph_paths,_discover_scorable_and_tests Same; kept native-separator output for callers that round-trip native paths
engine/policy/zones.py::_match_pattern Normalize both pattern and rel_path to forward slashes. Without this, absolute Windows path patterns hit the "exact basename" branch and returned False, causing the security detector to scan files in TEST zones it should have skipped
languages/_framework/treesitter/imports/resolvers_backend.py os.path.normpath around every returned candidate (go/rust/java/kotlin/cxx/csharp/dart/scala/swift)
languages/_framework/treesitter/imports/resolvers_scripts.py::resolve_js_import normpath each candidate before isfile check
languages/javascript/test_coverage.py::resolve_import_spec Normalize candidate for set-membership against POSIX-keyed production files

Test path/encoding fixes (3 tests)

  • test_state_path_from_explicit_state_argPath.as_posix() for cross-platform string equality
  • test_external_adapters.py::TestCollectExcludeDirs::*os.path.basename instead of rsplit("/") which split on the wrong separator
  • test_writes_to_file (holistic_review remediation) — explicit encoding="utf-8" on the read; locale default (cp1252 on Windows) mangled the middle-dot unicode

Skip Unix-only test paths (1 test)

  • test_timeout_kills_jscpd_process_grouppytest.mark.skipif(not hasattr(os, 'getpgid')). The kill-by-process-group code path is POSIX only; Windows uses TerminateProcess (covered by separate test)

Platform-gate chmod assert (1 test)

  • test_orchestrator_pipeline_writes_exact_cli_helper — skip the st_mode & 0o111 exec-bit assert on Windows. Windows uses ACLs; the helper is invoked via bash/PYTHONPATH and doesn't need the exec bit set on Windows

cmd /c wrapping (5 unit tests + 12 do_run_batches integration tests)

On Windows when a CLI binary isn't on PATH, _resolve_executable returns ["cmd", "/c", name], then _wrap_cmd_c collapses everything after /c into a single string for cmd.exe's tokenizer. Correct production behavior, but obscures the per-arg list shape several tests assert on.

  • 3 rovodev tests + 1 codex_batch test: per-test monkeypatch.setattr(..., "_resolve_executable", lambda name: [name])
  • 12 TestCmdReviewPrepare::test_do_run_batches_* tests: class-level autouse fixture doing the same
  • Plus **kwargs added to all 7 fake_subprocess_run signatures (and fake_run in test_run_codex_batch_writes_live_status_before_completion). The runner adds input=stdin_text to subprocess.run kwargs whenever _prompt_via_stdin(prompt) is true — and _prompt_via_stdin returns True on Windows unconditionally (return sys.platform == "win32" or len(prompt) > _PROMPT_ARG_MAX_CHARS). Without **kwargs, the fakes raised TypeError, hit _run_via_subprocess's (RuntimeError, ValueError, TypeError) except branch, and returned exit_code=1 — which is why the batches appeared to "exit 1" while the mock was set up correctly

Verification

Before After
Failed 34 0
Passed 6796 6830
Skipped 6 6

(Skipped count differs because test_timeout_kills_jscpd_process_group newly skips on Windows.)

Test plan

  • CI green (Linux 3.11) — proves no regression on the production target
  • CodeRabbit pass

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved cross-platform path handling consistency, particularly on Windows
    • Enhanced file discovery and module detection reliability across different operating systems
    • Fixed path normalization issues affecting coverage analysis and import resolution
  • Tests

    • Improved test reliability and compatibility across Windows and POSIX platforms

Review Change Stack

After PR #40, running the full suite on Windows surfaced 34 failures that
weren't visible in CI (Linux). All were Windows-specific quirks falling
into five categories. Each was root-caused and fixed without changing
Linux behavior. Full suite now green on Windows Python 3.12:
  Before: 34 failed, 6796 passed
  After:  0 failed, 6830 passed, 6 skipped

## Path-separator normalization (production code, 7 files)

Several modules built prefix strings with `os.sep` ("\\" on Windows) and
then compared against POSIX-style paths emitted by other tools or test
fixtures. The mismatched separators caused silent miss-matches. Each fix
normalizes both sides to "/" for comparison while preserving native
separators in the output where production callers expect them.

  - `engine/detectors/coupling.py::_rel_to_root` — also strip drive letter
    when root resolved to `C:/...` but the caller still passes POSIX-rooted
    prefix strings (e.g. tests using `/project/...`).
  - `engine/detectors/coverage/mapping_analysis.py::_build_prod_by_module` —
    normalize prod_file before prefix-strip; on Windows the mixed-slash
    inputs were missing the project-root prefix and producing module names
    like `.repo.pkg.util` instead of `pkg.util`.
  - `engine/detectors/test_coverage/discovery.py::_normalize_graph_paths,
    _discover_scorable_and_tests` — same pattern; also kept native-
    separator output for callers that round-trip native paths
    (test_normalize_graph_paths_converts_absolute_to_relative).
  - `engine/policy/zones.py::_match_pattern` — normalize both pattern and
    rel_path to forward slashes. Without this, an absolute Windows path
    pattern (`C:\...\test_creds.py`) hit the "exact basename" branch and
    returned False, causing the security detector to scan files in TEST
    zones it should have skipped.
  - `languages/_framework/treesitter/imports/resolvers_backend.py` —
    `os.path.normpath` around every returned candidate (go/rust/java/
    kotlin/cxx/csharp/dart/scala/swift). Forward-slashed src-root strings
    like "src/main/java" combined with native scan_path produced mixed
    separators ("C:\...\src/main/java\com\acme\App.java") that didn't
    match the platform-native expected paths.
  - `languages/_framework/treesitter/imports/resolvers_scripts.py::
    resolve_js_import` — normpath each candidate before isfile check.
    The "/index.js" suffix on a Windows path produced mixed separators.
  - `languages/javascript/test_coverage.py::resolve_import_spec` —
    normalize candidate after `os.path.normpath` for set-membership check
    against POSIX-keyed production files.

## Test path/separator fixes (3 tests)

  - `test_state_path_from_explicit_state_arg` — `Path.as_posix()` for
    cross-platform string equality.
  - `test_external_adapters.py::TestCollectExcludeDirs::test_includes_*,
    test_deduplicates` — use `os.path.basename` instead of rsplit("/")
    which split on the wrong separator on Windows.
  - `test_writes_to_file` (holistic_review remediation) — explicit
    `encoding="utf-8"` on the read; locale default (cp1252 on Windows)
    mangled the middle-dot unicode the writer emits as UTF-8.

## Skip Unix-only test path

  - `test_timeout_kills_jscpd_process_group` — `pytest.mark.skipif` on
    `hasattr(os, 'getpgid')`. The kill-by-process-group path is POSIX
    only; on Windows the timeout handling uses TerminateProcess, which
    has its own test.

## chmod assertion gated by platform

  - `test_orchestrator_pipeline_writes_exact_cli_helper` — skip the
    `st_mode & 0o111` exec-bit assert on Windows. Windows uses ACLs,
    not POSIX mode bits; the helper is invoked via bash/PYTHONPATH and
    doesn't need the exec bit set on Windows.

## cmd /c wrapping (5 tests, 1 cross-cutting fixture)

  - On Windows when an executable (acli, codex) isn't on PATH,
    `_resolve_executable` returns `["cmd", "/c", name]`, then
    `_wrap_cmd_c` collapses everything after `/c` into a single string
    for cmd.exe's tokenizer. Correct production behavior — but it
    obscures the per-arg list shape several tests assert on.
  - For 3 rovodev tests + 1 codex_batch test: per-test monkeypatch of
    `_resolve_executable` to return `[name]` directly.
  - For 12 `TestCmdReviewPrepare::test_do_run_batches_*` tests: added a
    class-level autouse fixture that does the same. Plus added
    `**kwargs` to all 7 `fake_subprocess_run` signatures + `fake_run` in
    `test_run_codex_batch_writes_live_status_before_completion`. The
    runner adds `input=stdin_text` to `subprocess.run` kwargs whenever
    `_prompt_via_stdin(prompt)` is true — which is always on Windows
    (`return sys.platform == "win32" or len(prompt) > _PROMPT_ARG_MAX_CHARS`).
    Without **kwargs, the fakes raised TypeError, hit
    `_run_via_subprocess`'s except branch, and returned exit_code=1.

None of these touch Linux behavior. CI on Linux 3.11 was already green;
this brings Windows 3.12 to parity.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: faf5dac4-7a85-4782-a1fc-faa56bbdd902

📥 Commits

Reviewing files that changed from the base of the PR and between 73b2e22 and 9f082aa.

📒 Files selected for processing (14)
  • desloppify/engine/detectors/coupling.py
  • desloppify/engine/detectors/coverage/mapping_analysis.py
  • desloppify/engine/detectors/test_coverage/discovery.py
  • desloppify/engine/policy/zones.py
  • desloppify/languages/_framework/treesitter/imports/resolvers_backend.py
  • desloppify/languages/_framework/treesitter/imports/resolvers_scripts.py
  • desloppify/languages/javascript/test_coverage.py
  • desloppify/tests/commands/plan/test_triage_split_modules_direct.py
  • desloppify/tests/commands/review/test_runner_rovodev_direct.py
  • desloppify/tests/commands/test_helpers.py
  • desloppify/tests/commands/test_runner_modules_direct.py
  • desloppify/tests/detectors/test_external_adapters.py
  • desloppify/tests/review/context/test_holistic_review_dimensions_and_structure.py
  • desloppify/tests/review/review_commands_cases.py

📝 Walkthrough

Walkthrough

This PR executes a cross-platform path normalization campaign across detection, resolution, and test infrastructure. Engine detectors and language import resolvers normalize Windows backslashes to forward slashes during prefix matching and relative path computation. Tests are updated to skip platform-specific assertions (POSIX exec bits), use platform-agnostic basename lookups, and monkeypatch executable resolution to prevent command-wrapping variability.

Changes

Cross-platform path consistency

Layer / File(s) Summary
Engine detector path normalization
desloppify/engine/detectors/coupling.py, desloppify/engine/detectors/coverage/mapping_analysis.py, desloppify/engine/detectors/test_coverage/discovery.py, desloppify/engine/policy/zones.py
_rel_to_root() adds Windows drive-letter fallback matching; _build_prod_by_module() normalizes project_root and prod_file to POSIX style before prefix slicing; _normalize_graph_paths() applies mixed-slash fallback when converting to relative paths; _match_pattern() strips backslashes before pattern checks.
Language import resolver standardization
desloppify/languages/_framework/treesitter/imports/resolvers_backend.py, desloppify/languages/_framework/treesitter/imports/resolvers_scripts.py
Go, Rust, Java, Kotlin, C/C++, C#, Dart, Scala, and Swift resolvers normalize returned file paths via os.path.normpath(); JavaScript resolver normalizes each candidate path within the extension loop.
JavaScript test coverage integration
desloppify/languages/javascript/test_coverage.py
resolve_import_spec() normalizes computed import target to POSIX separators before production_files lookup, ensuring consistent matching across platform-specific os.path.normpath() behavior.
Cross-platform test compatibility
desloppify/tests/commands/plan/test_triage_split_modules_direct.py, desloppify/tests/commands/review/test_runner_rovodev_direct.py, desloppify/tests/commands/test_helpers.py, desloppify/tests/commands/test_runner_modules_direct.py, desloppify/tests/detectors/test_external_adapters.py, desloppify/tests/review/context/test_holistic_review_dimensions_and_structure.py, desloppify/tests/review/review_commands_cases.py
Exec-bit assertions conditionally skipped on Windows; basenames computed via os.path.basename() instead of slash splitting; subprocess stubs accept **kwargs for prompt-via-stdin compatibility; _resolve_executable monkeypatched to prevent cmd /c wrapping; POSIX path assertions validate .as_posix() form; UTF-8 file reads specified explicitly.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the campaign's main objective: fixing cross-platform test failures on Windows, with the concrete metric (34 tests) that anchors scope.
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.
No Hardcoded Secrets Or Credentials ✅ Passed No hardcoded secrets, API keys, tokens, passwords, or credentials found in any non-test production files. All production changes are path normalization logic for cross-platform compatibility.
No Speciesist Idioms ✅ Passed No speciesist idioms found. Only technical "kill" in process management context.
No Tier 3 Data Committed ✅ Passed PR contains only technical path normalization and test compatibility fixes for Windows. No Tier 3 sensitive data found: no activist identities, legal strategy, operational security, or credentials.

✏️ 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 fix/windows-test-compat-2026-05-14
  • 🛠️ fix NAV violations

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

@samtuckerdavis samtuckerdavis merged commit 4239b06 into main May 13, 2026
12 checks passed
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