Skip to content

Windows verification harnesses: an MSYS /d/ path literal silently redirected two tzdata sabotage cases into a duplicate of case E #2834

Description

@kyle-sexton

Summary

An untracked verification harness, D:\worktrees\_vfy\sabotage.sh, writes two of its generated test fixtures to an MSYS-form absolute path (/d/worktrees/...) that a Windows-native Python resolves against the current drive's root instead of the intended directory. The consequence is not primarily disk litter. Two of the harness's own test cases silently exercised a different scenario than their names claim, so a coverage gap the harness was written to probe is still open.

The defect that matters: two cases tested the wrong thing

sabotage.sh lines 34 and 35 define cases G-valid-zip-no-tzdata and H-valid-zip-garbage-tz. Each is a sabotage string evaled by run_case (lines 9-25). The execution sequence for those two cases is:

  1. Line 16 — run_case copies the real bundle into place: cp "$SRC/vendor/tzdata-zoneinfo.zip" "$dir/scripts/vendor/".
  2. Line 17 — ( cd "$dir" && eval "$sabotage" ) runs the case's sabotage string with $dir as the working directory.
  3. The sabotage string's first action is rm -f scripts/vendor/tzdata-zoneinfo.zip, which deletes that real bundle. This path is relative, so it resolves correctly, under $dir.
  4. The sabotage string's replacement archive is then written to an absolute MSYS-form literal, which does not land in $dir at all:
run_case "G-valid-zip-no-tzdata"   "rm -f scripts/vendor/tzdata-zoneinfo.zip && mkdir -p zz/notzdata && echo hi > zz/notzdata/x.txt && (cd zz && python -c \"import shutil;shutil.make_archive('/d/worktrees/_vfy/sab/G-valid-zip-no-tzdata/scripts/vendor/tzdata-zoneinfo','zip','.')\")"
  1. Lines 19-22 then run the script under test against $dir/scripts/, where the bundle file is now absent.

So cases G and H ran with no bundle present at all — mechanically identical to case E-missing-bundle (line 32), whose sabotage string is the same rm -f with nothing after it. The resulting scripts/ tree is in the same state in all three cases; G and H merely leave an extra zz/ sibling under $dir that the script under test never reads. Line 19 also sets TMPDIR/TEMP/TMP per case, so no warm extraction cache from A-baseline-valid could have leaked in and masked the difference.

Confirming this on disk today: D:\worktrees\_vfy\sab\G-valid-zip-no-tzdata\scripts\vendor\ exists and is empty, while zz\notzdata\x.txt sits as a sibling under the same $dir — the relative paths resolved correctly and only the absolute one went astray.

Neither case tested the scenario its name describes. Whoever relies on that matrix should re-run those two cases; the results recorded for G and H are results for E.

This matters more than a duplicated case, because these two shapes have no committed coverage. plugins/session-flow/skills/keep-going/scripts/check-usage-limit-reset.test.py (BundledTzdataDegradationTests) covers corrupt bytes, a truncated zip, and a directory at the bundle path. It does not cover a valid zip with no tzdata/ member (G), nor a valid zip with a garbage tzdata/ member (H). (For completeness: E-missing-bundle, F-empty-file, and I-vendor-dir-removed are also uncovered by the committed suite — a missing bundle is that suite's assumed baseline rather than an asserted case. G and H are the two the harness was specifically written to add, and are the two that silently degenerated.)

Mechanism

Git Bash spells D:\worktrees\... as /d/worktrees/.... Handed to a Windows-native Python, the leading / anchors to the root of the current drive, not to a POSIX root — so the literal becomes D:\d\worktrees\.... shutil.make_archive then creates that whole phantom directory chain before writing.

Confirmed four independent ways (all re-verified read-only on 2026-08-16 before filing):

  1. Path resolution. cd /d && python -c "import os;print(os.path.abspath('/d/x'))"D:\d\x. (Python 3.14.7, MSC v.1944 64-bit, Windows.)
  2. Directory creation. inspect.getsource(shutil._make_zipfile) contains archive_dir = os.path.dirname(base_name) followed by if archive_dir and not os.path.exists(archive_dir): ... os.makedirs(archive_dir) — Python creates the phantom tree itself rather than failing.
  3. Byte-exact payload match. The archive at D:\d\worktrees\_vfy\sab\G-valid-zip-no-tzdata\scripts\vendor\tzdata-zoneinfo.zip holds members notzdata/ (a 0-byte directory entry) and notzdata/x.txt at 3 bytes — matching line 34's echo hi. The archive under H-valid-zip-garbage-tz holds tzdata/ (directory entry), tzdata/junk.bin at 2 bytes and tzdata/__init__.py at 8 bytes — matching line 35's echo 'x' and echo 'garbage'. Member timestamps are 2026-08-15 04:45:58 and 04:46:24 (the source files' mtimes, seconds before each archive was written); sabotage.sh has mtime 2026-08-15 04:40:53. The artifacts sit inside that run's window.
  4. Misdirection, proven both ways. Test-Path on both intended destinations — D:\worktrees\_vfy\sab\G-valid-zip-no-tzdata\scripts\vendor\tzdata-zoneinfo.zip and the H-valid-zip-garbage-tz equivalent — returns False. The archives exist only under D:\d\.... (Test-Path establishes the current state; leg 3's timestamps are what tie the artifacts to that run.) Within D:\worktrees, make_archive appears in no other file — sabotage.sh is the sole emitter there.

Why a convention, not a one-line fix

sabotage.sh is untracked scratch. On current origin/main (1199240452ca411881c5defee293986f92ccf76a), git grep -l make_archive origin/main returns nothing; the only sabotag* hits are two docstring lines inside the tracked check-usage-limit-reset.test.py; and git log --all -S"_vfy" is empty. There is nothing here to patch — the defect lives in how throwaway Windows verification harnesses get written, so the next one repeats it.

Recurrence is observed, not hypothesized. melodic-software/dotfiles#486 records that a phantom D:\d tree was found and removed during a /disk-hygiene:clean --execute D: run on 2026-08-14. D:\d exists again today, and its CreationTime2026-08-15T04:46:04 — is identical to the moment case G's archive was written. It did not survive from before; this harness recreated it. The tree currently present is:

D:\d\worktrees\_vfy\sab\G-valid-zip-no-tzdata\scripts\vendor\tzdata-zoneinfo.zip
D:\d\worktrees\_vfy\sab\H-valid-zip-garbage-tz\scripts\vendor\tzdata-zoneinfo.zip

A rerun of the harness recreates it again.

Prior art in this repo — and what it does not cover

lib/hook-utils.sh on origin/main already treats OSTYPE-gated path handling as a first-class concern, which is the precedent worth citing. The two helpers closest to this problem are neither of them a drop-in fix:

  • hook::normalize_path (lines 281-294) gates on OSTYPE and folds a leading drive prefix (/c/... or c:/...) to an upper-case drive letter plus lower-cased remainder. It uses no cygpath, and its own comment is explicit that the result is used only for comparison — "the emitted path is always the caller's original." Emitting its return value would be a misuse of it.
  • hook::expand_8dot3 (lines 296-334) gates on OSTYPE and compares cygpath -m against cygpath -l -m, but for 8.3 short-name expansion, and only when the path contains ~.

cygpath appears nowhere else in the file. So the repo has the discipline and the cygpath dependency already established, but no helper that performs the MSYS→Windows emit-path conversion a harness actually needs. Closing that gap is part of the ask below.

Asks

  1. Fix the harness pattern. Build the destination in Windows form before it crosses into a native consumer — run it through cygpath -w / cygpath -m, or let Python compute the path from a relative base it already owns. The sabotage strings already cd into $dir, so a relative destination would have been both correct and simpler.
  2. Document the convention, and give it a helper to point at. Any path originating in Git Bash and handed to PowerShell, cmd, or a Windows-native interpreter must be converted first. Point harness authors at an emit-safe conversion helper rather than leaving each harness to rediscover the trap. lib/hook-utils.sh is the precedent for the OSTYPE-gating shape, but as noted above neither existing helper is safe to use for this purpose as-is.
  3. Consider a cheap detection net. Fail a run that leaves a directory at a drive root whose name is a single letter matching a drive — a reliable fingerprint of this exact bug. guardrails: nothing catches a POSIX /tmp path resolving to a Windows drive-root C:\tmp #2594 already argued for a drive-root detection guard for externally-produced C:\tmp residue; this is the same detector applied to a producer we do own.

Flag for the fix/2648-tzdata-degradation lane owner

Cases G and H need re-running against a correct destination path. Their recorded results are results for case E.

Note on locations, so the follow-up lands in the right place: D:\worktrees\_vfy is an untracked plain directory copy with no .git — it is not itself a registered worktree and does not appear in git worktree list. The locked worktree for that lane is a separate path, D:/worktrees/melodic-software-claude-code-plugins-fix-2648-tzdata-degradation at 4a196c90f [fix/2648-tzdata-degradation], whose lock reason reads: worktree-create.sh: lane active on melo-desk-001 since 2026-08-15T04:33:17Z; unlock when the owning lane is done.

This issue reports only what the script does. It makes no claim about what the lane concluded from the G and H results — that was not examined.

Related, not duplicated

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions