Skip to content

tests: fix seven Windows-only failures, one POSIX assumption at a time - #120

Merged
9 commits merged into
mainfrom
w56/windows-test-isolation
Sep 8, 2026
Merged

9 commits merged into
mainfrom
w56/windows-test-isolation

Conversation

@jothimani-rajendran

Copy link
Copy Markdown
Collaborator

Seven tests fail on a clean main on Windows and pass on the CI runners. Each rests on a
POSIX assumption that was never true off POSIX. None is skipped, deleted, or weakened; one
commit per failure, each named for the assumption it removes.

Full suite on Windows: 1534 passed, 4 skipped, 0 failed (was 7 failed).

The three assumptions

os.path.expanduser reads HOME — five failures. It does on POSIX. ntpath.expanduser
reads USERPROFILE, then HOMEDRIVE + HOMEPATH, and never HOME. conftest.py's
autouse isolated_home fixture already sets all four, and its docstring says exactly this.
The five tests set HOME again by hand and then asserted against a path derived from that
one variable, so on Windows they measured a directory nothing had written to.

The failure modes were worth reading rather than just fixing:

test what it actually reported
test_a_user_scoped_config_path_is_not_nested_under_the_repo install wrote the right file; the assertion looked one level up
test_install_never_destroys_a_config_it_cannot_parse the BOM case passed vacuously; the corrupt cases raised JSONDecodeError from the test's own json.loads, not ConfigUnreadableError from install
test_a_query_never_raises_on_an_unparseable_config installed() returned False because nothing was there — right answer, wrong reason — and uninstall() had no file to fail on, hence DID NOT RAISE
test_install_appends_a_block_...untouched install wrote a correct config; the test read the untouched original it had seeded and reported guard.py missing
test_reinstalling_replaces_our_block_... FileNotFoundError before any assertion about stacked blocks ran

Each now takes isolated_home as a parameter. tmp_path stays where it means the repo
root, which is a different argument and still belongs there.

The platform default text encoding is UTF-8 — one failure. The generated pages carry em
dashes; both readers that compare a committed page to a freshly built one used a bare
open(). Under cp1252 every em dash read back as three characters, so all twelve pages
reported stale — and could not be made to pass, because regenerating wrote the same bytes
it had just failed to read. Fixed in the test and in examples/generate.py's --check
path, which the pre-commit hook and the examples CI job both run and which carried the
identical latent bug. The writer now pins encoding="utf-8", newline="\n" too: a Windows
regeneration was rewriting all thirteen pages with CRLF, so the same run that fixed one
page dirtied twelve others.

The filesystem carries a POSIX execute bit — one failure. st_mode & 0o111 is 0 for
every file on Windows, including one git records as 100755 and checks out executable on
Linux and macOS. The test now asserts on the mode git records for the path: that is what
travels with a clone, it decides whether the hook runs at all, and it is the same answer on
every platform. Confirmed non-vacuous — git currently records 100755, and the assertion
names the mode it found when it is anything else. An untracked hook now fails with its own
message rather than an index error.

One extra commit, not one of the seven

test_a_query_never_raises_on_an_undecodable_toml_config carries the same hand-set HOME
as the three install tests, but it passed on Windows. That is the worse outcome:
installed() returned False because the utf-16 file was not on the path it reads, not
because the TOML branch had decoded it safely. The test asserted its own setup and never
entered the branch it is named for.

Fixed the same way, in its own commit, and verified non-vacuous by writing a valid
installed config to that path (installed()True), then overwriting it with the utf-16
body (installed()False, no raise). Drop that commit if you would rather keep this PR
to the seven.

Checks

ruff check ., ruff format --check . and tests/check_stdlib_only.py clean. No blanket
ruff --fix was run; no import reordering was needed.

🤖 Generated with Claude Code

…ed path test

test_a_user_scoped_config_path_is_not_nested_under_the_repo set HOME to
tmp_path itself and then asserted the config landed in tmp_path/.junie.
Only posixpath.expanduser reads HOME. ntpath reads USERPROFILE, then
HOMEDRIVE + HOMEPATH, and conftest's autouse isolated_home fixture already
points all four at a `home` directory beneath tmp_path -- so on Windows the
install correctly wrote its config under the fixture's home while the
assertion looked one level up, in tmp_path itself.

The fixture is the platform-independent answer and the test now takes it as
a parameter instead of re-deriving home from an env var that three quarters
of the supported platforms ignore.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… test

test_install_never_destroys_a_config_it_cannot_parse seeded a config at
tmp_path/.junie/config.json after setting HOME to tmp_path, then called
install() and checked the user's settings survived.

On Windows expanduser resolved ~ to the fixture's tmp_path/home, so install
never saw the seeded file at all: it created a fresh config elsewhere, and
every assertion here was reading a file nothing had touched. The BOM case
passed vacuously and the unparseable cases raised JSONDecodeError from the
test's own json.loads rather than ConfigUnreadableError from install.

Home now comes from the isolated_home fixture, which sets every variable
expanduser actually consults on both platform families.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… query test

test_a_query_never_raises_on_an_unparseable_config wrote a corrupt config
under a HOME it had set by hand, then asserted installed() returns False and
uninstall() raises ConfigUnreadableError.

On Windows the corrupt file was not on the path either call reads, so
installed() returned False because nothing was there -- the right answer for
the wrong reason -- and uninstall() found no file to fail on, which is why
the test reported DID NOT RAISE.

Home now comes from the isolated_home fixture. All three install tests that
seeded a user-scoped config by hand are now consistent with it.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…all test

kimi_code's CONFIG_PATH starts with ~/, so its config is user-scoped by
design -- there is a test in this same file asserting exactly that. The
install test still seeded the file at tmp_path/.kimi-code/config.toml after
setting HOME by hand, which resolves nowhere on Windows.

The result was the sharpest form of the failure: install() wrote a correct
config under the fixture's home, the test read the untouched original it had
seeded, and reported that guard.py was missing from a file install had never
been asked to write.

Home now comes from the isolated_home fixture. tmp_path stays as the repo
root argument, which is a different thing and still belongs there.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stall test

test_reinstalling_replaces_our_block_rather_than_stacking_them read the
config back from tmp_path/.kimi-code/config.toml after two installs. Both
installs wrote to the home expanduser actually resolves, so on Windows the
read raised FileNotFoundError before any assertion about stacked blocks ran.

Home now comes from the isolated_home fixture, so the file the test reads is
the file the installs wrote. `Path` is still used elsewhere in the module, so
the import stays.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generated pages carry em dashes. Both readers that compare a committed
page against a freshly built one opened it with no encoding argument, so on
a cp1252 host every em dash decoded to three characters that could never
match. test_committed_pages_match_what_the_library_produces reported all
twelve pages stale on Windows -- with no way to make it pass, because
regenerating wrote the same bytes it had just failed to read back.

Both readers now say encoding="utf-8": the test's, extracted into a
_committed() helper that can carry the reason, and generate.py's --check
path, which the pre-commit hook and the examples CI job both run and which
had the identical latent bug.

The writer says encoding="utf-8", newline="\n" for the same reason from the
other side: a regeneration on Windows was writing CRLF into files the repo
stores with LF, so every page showed as modified until git normalised it
away. Same assumption, same fix, and it keeps a Windows regeneration
byte-identical to a CI one.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_hook_is_executable read HOOK.stat().st_mode & 0o111. Windows has no
POSIX execute bit, so that is 0 for every file in the tree -- including this
one, which git records as 100755 and checks out executable on Linux and
macOS. The test could not pass on Windows and told the truth about nothing
there.

It now reads the mode git records for the path. That is the mode a clone
gets, which is what decides whether the hook runs, and it is the same
answer on every platform. A hook that is not tracked at all now fails with
its own message rather than an index error.

Verified real rather than vacuous: git currently records 100755 for
.githooks/pre-commit, and the assertion names the mode it found when it is
anything else.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssing

test_a_query_never_raises_on_an_undecodable_toml_config carried the same
hand-set HOME as the three install tests fixed earlier on this branch, but
it did not fail on Windows. That is the worse outcome: installed() returned
False because the utf-16 file was not on the path it reads, not because the
TOML branch had decoded it safely. The test asserted its own setup.

Not one of the seven reported failures. It is the same defect in the same
file, found while fixing them, and it was reporting a pass for a branch it
never entered.

With home taken from the fixture, the file is on the path installed() reads:
verified by writing a valid installed config to that path and watching
installed() return True, then overwriting it with the utf-16 body and
watching it return False without raising.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… behind each

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (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.

2 participants