Skip to content

fix(tests): make Windows unit-test job green#1427

Merged
danielmeppiel merged 3 commits into
mainfrom
danielmeppiel/stunning-meme
May 21, 2026
Merged

fix(tests): make Windows unit-test job green#1427
danielmeppiel merged 3 commits into
mainfrom
danielmeppiel/stunning-meme

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

fix(tests): make Windows unit-test job green

TL;DR

The Build & Test (windows-latest) job in run #26188855664 failed on 14 unit tests across 5 distinct root causes: a Linux-only fcntl import, os.fchmod not existing on Windows, a real atomic_write_text fd-leak that prevents tmp-file cleanup on Windows, POSIX-only path assertions, and Rich Console() truncating table columns under the narrow CI terminal width. This PR fixes each root cause at the right layer (one production fix in atomic_io.py, the rest in tests) so the Windows job goes green without weakening Linux/macOS coverage.

Problem (WHY)

The Windows runner failed with five distinct, simultaneous symptoms in tests/unit:

  • fcntl import errors in TestCloneLinux (test_reflink_filesystem_support.py, test_reflink_phase3w5.py) -- fcntl is a POSIX-only stdlib module; on Windows the import itself raises ModuleNotFoundError, so the whole class is uncollectable.
  • os.fchmod AttributeError in test_atomic_io.py::TestAtomicWriteText::test_fchmod_* -- unittest.mock.patch requires the target attribute to exist; on Windows os.fchmod does not exist, so the patch itself raises before the test body runs.
  • atomic_write_text leaks the tmp file on Windows when os.fdopen raises -- the raw fd from mkstemp is never closed, so Windows file locking blocks the subsequent os.unlink(tmp_name). test_tmp_file_cleaned_up_on_write_failure correctly caught this; the bug is in the production code, not the test.
  • POSIX-only result.startswith("/") assertions in test_paths.py::TestPortableRelpath -- absolute paths on Windows look like C:/Users/..., not /Users/....
  • Rich Console() truncating "constitution" to "constitutio…" in test_output_formatters_{phase3,rendering}.py::test_constitution_row_colored -- the Windows CI runner detects a narrow terminal width (~80 cols), so Rich shrinks the 100+ char-wide table and the assertion "constitution" in text fails.

Per the repo's contract that "agents pattern-match well against concrete structures", each failure deserves a targeted fix at the layer it actually lives in -- not a blanket skipif win32 over the whole file.

Approach (WHAT)

Failure Layer Fix
TestCloneLinux import-time fcntl failure test @pytest.mark.skipif(sys.platform == "win32", ...) on the class
patch("...os.fchmod") AttributeError test Add create=True to the three patch calls
atomic_write_text tmp-file leak on Windows production Close the raw fd in the failure path before unlinking
result.startswith("/") on Windows drive paths test Accept Path(result).is_absolute() or drive-letter
Rich table truncates "constitution" under narrow CI width test Pin Console(width=200) in the _color_formatter helper

Implementation (HOW)

  • src/apm_cli/utils/atomic_io.py -- track fd_wrapped = False and flip it to True only after os.fdopen(fd, ...) returns; in the except branch, close the fd first when it was never wrapped, then unlink. Preserves the exception-propagation contract; eliminates the Windows tmp-leak.
  • tests/unit/test_reflink_filesystem_support.py, tests/unit/test_reflink_phase3w5.py -- @pytest.mark.skipif(sys.platform == "win32", reason="Linux-only: fcntl module not available on Windows") on TestCloneLinux. The import fcntl lives inside the class body, so a class-level skip is the minimum-scope fix.
  • tests/unit/utils/test_atomic_io.py -- three call sites: patch("apm_cli.utils.atomic_io.os.fchmod", create=True). Behavior on POSIX is unchanged (the attribute exists; create=True is a no-op).
  • tests/unit/utils/test_paths.py -- two assertions changed from result.startswith("/") to Path(result).is_absolute() or (len(result) >= 2 and result[1] == ":"). Forward-slash-only check is preserved.
  • tests/unit/test_output_formatters_phase3.py, tests/unit/test_output_formatters_rendering.py -- _color_formatter() helper now constructs a Console(width=200, force_terminal=False) and assigns it to f.console, so Rich's auto-detect cannot shrink columns below content length.

Diagram -- atomic_write_text failure path before/after

Legend: shows why closing the fd before unlink is required on Windows but is a no-op on POSIX.

flowchart LR
  A[mkstemp returns fd, tmp_name] --> B{os.fdopen fd}
  B -- success --> C[write + os.replace]
  B -- raises --> D{"before: skip os.close(fd)"}
  D --> E["os.unlink(tmp_name)<br/>POSIX: succeeds<br/>Windows: blocked by open fd"]
  B -- raises --> F["after: os.close(fd) first"]
  F --> G["os.unlink(tmp_name)<br/>succeeds on every platform"]
Loading

Trade-offs

  • Skip TestCloneLinux on Windows rather than mock fcntl. Mocking a missing stdlib module is fragile (sys.modules['fcntl'] = MagicMock()) and would test the mock, not the code. The reflink code path that uses fcntl is itself gated on Linux, so Windows has no production code to exercise here. "Add what the agent lacks, omit what it knows" -- omit the test on the platform that does not run the code.
  • Pin Rich Console(width=200) in the test helper rather than parametrize production code. Production formatters intentionally honor the user's actual terminal width; threading a width through CompilationFormatter.__init__ just to satisfy tests would leak test concerns into the production API.

Benefits

  1. Windows Build & Test job goes from 14 failures to 0 on the targeted suite.
  2. Real production bug fixed in atomic_write_text: tmp files no longer leak on Windows when fdopen raises.
  3. macOS/Linux suite still passes locally (107/107 in the affected files; full tests/unit shows only 4 pre-existing failures on main, none introduced here).
  4. No new platform-specific dependencies or runtime branches introduced.

Validation

Local run on macOS against the directly-affected files:

$ uv run --extra dev pytest tests/unit/utils/test_atomic_io.py tests/unit/utils/test_paths.py \
    tests/unit/test_reflink_filesystem_support.py tests/unit/test_reflink_phase3w5.py \
    tests/unit/test_output_formatters_phase3.py::TestFormatOptimizationProgressColorBranches \
    tests/unit/test_output_formatters_rendering.py::TestFormatOptimizationProgressColorBranches
============================= 107 passed in 3.66s ==============================

Lint contract (mirrors CI Lint job per .apm/instructions/linting.instructions.md):

$ uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/
All checks passed!
1033 files already formatted
Pre-existing unrelated failures on main (verified by stashing this PR's diff)
FAILED tests/unit/test_mcp_integrator_install_hermetic.py::TestRunMcpInstallNoRuntimes::test_all_excluded_warns_and_returns_zero
FAILED tests/unit/test_mcp_integrator_install_phase3w4.py::TestRunMcpInstallNoRuntimes::test_all_excluded_warns_and_returns_zero
FAILED tests/unit/test_runtime_windows.py::TestScriptRunnerWindowsParsing::test_execute_runtime_command_uses_shlex_on_unix
FAILED tests/unit/test_runtime_factory.py::TestRuntimeFactory::test_get_available_runtimes_real_system

These reproduce on main with the PR diff stashed and are out of scope.

Scenario evidence

User-promise scenario Test that proves it APM principle
atomic_write_text never leaves a half-written or leaked tmp file when fdopen fails (Windows + POSIX) tests/unit/utils/test_atomic_io.py::TestAtomicWriteText::test_tmp_file_cleaned_up_on_write_failure Reliability of integration primitives
portable_relpath falls back to a forward-slash absolute path on every platform when path is not under base tests/unit/utils/test_paths.py::TestPortableRelpath::test_path_not_under_base_returns_absolute Cross-platform path correctness
CompilationFormatter color path renders the constitution row legibly regardless of detected terminal width tests/unit/test_output_formatters_*.py::TestFormatOptimizationProgressColorBranches::test_constitution_row_colored Deterministic CLI output

How to test

    • Pull the branch: git fetch origin danielmeppiel/stunning-meme && git checkout danielmeppiel/stunning-meme
    • On macOS or Linux, run the affected suite: uv run --extra dev pytest tests/unit/utils/test_atomic_io.py tests/unit/utils/test_paths.py tests/unit/test_reflink_filesystem_support.py tests/unit/test_reflink_phase3w5.py tests/unit/test_output_formatters_phase3.py tests/unit/test_output_formatters_rendering.py -- expect green.
    • Verify the Windows Build & Test job goes green on this PR's CI run.
    • Confirm lint stays silent: uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings May 21, 2026 11:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes Windows CI unit test failures by addressing platform-specific assumptions in tests and fixing a real temp-file FD leak in atomic_write_text.

Changes:

  • Fix atomic_write_text cleanup so temp files aren’t left locked on Windows when os.fdopen fails.
  • Make affected tests Windows-compatible via targeted skipif, patch(..., create=True), and portable path assertions.
  • Stabilize Rich table rendering assertions by forcing a wider Console in color-formatter test helpers.
Show a summary per file
File Description
src/apm_cli/utils/atomic_io.py Closes the raw mkstemp fd on error before unlinking, avoiding Windows temp-file lock/leak.
tests/unit/utils/test_atomic_io.py Uses patch(..., create=True) so Windows can patch missing os.fchmod.
tests/unit/utils/test_paths.py Updates absolute-path assertions to be Windows-compatible.
tests/unit/test_reflink_phase3w5.py Skips Linux-only reflink tests on Windows to avoid fcntl import-time failure.
tests/unit/test_reflink_filesystem_support.py Same Windows skip for Linux-only reflink tests.
tests/unit/test_output_formatters_phase3.py Forces wide Rich Console to prevent truncation-driven assertion failures.
tests/unit/test_output_formatters_rendering.py Same console-width forcing for deterministic Rich output in tests.

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 3

Comment thread tests/unit/utils/test_paths.py Outdated
# Must be absolute (starts with /)
assert result.startswith("/")
# Must be absolute (POSIX leading-slash, or Windows drive like "C:/")
assert Path(result).is_absolute() or (len(result) >= 2 and result[1] == ":")
Comment thread tests/unit/utils/test_paths.py Outdated
Comment on lines +113 to +114
# Falls back to resolved absolute posix (POSIX leading-slash or Windows drive)
assert Path(result).is_absolute() or (len(result) >= 2 and result[1] == ":")
Comment on lines +1388 to +1392
# Force a wide console so Rich tables don't truncate column contents
# under narrow terminals (Windows CI runners default to ~80 cols).
from rich.console import Console

f.console = Console(width=200, force_terminal=False)
@danielmeppiel danielmeppiel force-pushed the danielmeppiel/stunning-meme branch from bcc5c8b to daf99e0 Compare May 21, 2026 11:27
danielmeppiel pushed a commit that referenced this pull request May 21, 2026
Two findings from the Copilot reviewer, both accepted:

1. test_paths.py path assertions -- the previous fallback
   '(len(result) >= 2 and result[1] == ":")' would incorrectly accept
   drive-RELATIVE paths like 'C:foo' as absolute. Path(result).is_absolute()
   alone is correct on both POSIX and Windows because pathlib instantiates
   the platform's Path subclass; drop the redundant drive-letter clause.

2. _color_formatter helper duplicated across test_output_formatters_phase3.py
   and test_output_formatters_rendering.py -- extract the Console(width=200)
   construction into tests/unit/_formatter_helpers.py so the two modules
   cannot drift on Rich settings. Both call sites now delegate to
   make_color_formatter().

No behavior change in production code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Daniel Meppiel and others added 3 commits May 21, 2026 16:09
Address all unit-test failures observed on the windows-latest runner
(run https://github.com/microsoft/apm/actions/runs/26188855664):

- tests/unit/test_reflink_{filesystem_support,phase3w5}.py: skip
  TestCloneLinux on win32; it does 'import fcntl' which is
  Linux-only.
- tests/unit/utils/test_atomic_io.py: patch os.fchmod with
  create=True so the patch works on Windows where os.fchmod does
  not exist.
- src/apm_cli/utils/atomic_io.py: close the raw fd in the failure
  path before unlinking the tmp file, so Windows can release its
  lock and the cleanup succeeds (was leaking the tmp file when
  os.fdopen raised).
- tests/unit/utils/test_paths.py: accept Windows drive-letter
  absolute paths (e.g. 'C:/...') in addition to POSIX leading
  slashes when asserting portable_relpath() fell back to an
  absolute path.
- tests/unit/test_output_formatters_{phase3,rendering}.py: pin
  Rich Console width=200 in the _color_formatter test helper so
  table columns are not truncated under narrow Windows CI
  terminal widths (the 'constitution' assertion was failing
  because Rich shortened the cell to 'constitutio\u2026').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ckage to base

The codex and copilot client adapters carried identical copies of
_select_remote_with_url and _select_best_package (priority order
[npm, docker, pypi, homebrew]). After the recent #1277 merge the
two chunks crossed the pylint R0801 --min-similarity-lines=10
threshold, breaking the 'Code duplication guardrail' lint step on
every open PR:

    src/apm_cli/marketplace/version_check.py:1:0: R0801: Similar
    lines in 2 files
    ==apm_cli.adapters.client.codex:[545:573]
    ==apm_cli.adapters.client.copilot:[1003:1049]

Move both helpers onto MCPClientAdapter (where _infer_registry_name
they depend on already lives) and delete the duplicates from codex
and copilot. VSCodeAdapter keeps its own _select_best_package because
it uses a different priority order ([npm, pypi, docker]) with extra
runtime_hint fallback, so leaving its override in place is correct.

No behavior change: the moved bodies are byte-identical to the
removed copies. 26 existing best_package / remote_with_url unit
tests still pass; lint pipeline (ruff + pylint R0801) is green
locally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two findings from the Copilot reviewer, both accepted:

1. test_paths.py path assertions -- the previous fallback
   '(len(result) >= 2 and result[1] == ":")' would incorrectly accept
   drive-RELATIVE paths like 'C:foo' as absolute. Path(result).is_absolute()
   alone is correct on both POSIX and Windows because pathlib instantiates
   the platform's Path subclass; drop the redundant drive-letter clause.

2. _color_formatter helper duplicated across test_output_formatters_phase3.py
   and test_output_formatters_rendering.py -- extract the Console(width=200)
   construction into tests/unit/_formatter_helpers.py so the two modules
   cannot drift on Rich settings. Both call sites now delegate to
   make_color_formatter().

No behavior change in production code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel force-pushed the danielmeppiel/stunning-meme branch from 32991fb to c6587a6 Compare May 21, 2026 14:11
@danielmeppiel danielmeppiel merged commit a245f94 into main May 21, 2026
9 checks passed
@danielmeppiel danielmeppiel deleted the danielmeppiel/stunning-meme branch May 21, 2026 14:21
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