(High Severity Cross-Platform Bug): Windows Test Discovery Failure via Case-Sensitive Prefix Matching in `normalizeFileForMatching`
Author: codeCraft-RitikCreated Sep 15, 2026Updated Sep 15, 2026
Labelsneeds triage
Issue :- (High Severity Cross-Platform Bug): Windows Test Discovery Failure via Case-Sensitive Prefix Matching in normalizeFileForMatching
Metadata
- Title:
[Bug] Windows test discovery fails due to drive letter case sensitivity and root drive slicing in normalizeFileForMatching - Severity: High (Cross-Platform / Windows Compatibility)
- Component: Path Normalization & Glob Matching (
lib/glob-helpers.js) - Affected File:
lib/glob-helpers.js(normalizeFileForMatching, lines 79–94)
Problem Summary
normalizeFileForMatching strips the current working directory from absolute file paths so that picomatch can match them against relative test patterns (e.g. test/**/*.js).
On Windows, this implementation suffers from two major bugs:
- Drive Letter Case Sensitivity: Windows file paths are case-insensitive, and Node.js frequently mixes lowercase (
c:/...) and uppercase (C:/...) drive letters (e.g., fromprocess.cwd()vspathToFileURLvsresolve). Becausefile.startsWith(cwd)performs a case-sensitive string check,c:/project/test.jsdoes NOT start withC:/project. The path is not stripped and is returned as a full absolute path, causingmatches(file, filePatterns)to fail and test files to be silently ignored. - Drive Root Path Corruption: The function assumes
cwddoes not end in a slash and computesfile.slice(cwd.length + 1). When running from a drive root (e.g.C:/),cwd.length + 1is 4, which slices off the first character of the filename (C:/test.jsbecomesest.js).
Root Cause Analysis
In lib/glob-helpers.js lines 79–94:
export function normalizeFileForMatching(cwd, file) {
if (process.platform === 'win32') {
cwd = slash(cwd);
file = slash(file);
}
// Note that if `file` is outside `cwd` we can't normalize it. If this turns
// out to be a real-world scenario we may have to make changes in calling code
// to make sure the file isn't even selected for matching.
if (!file.startsWith(cwd)) {
return file;
}
// Assume `cwd` does *not* end in a slash.
return file.slice(cwd.length + 1);
}- Scenario 1:
cwd = "C:/Users/dev/project",file = "c:/Users/dev/project/test/app.test.js".file.startsWith(cwd)isfalse. Returns"c:/Users/dev/project/test/app.test.js".classify()passes this topicomatch('test/**/*.js'), which returnsfalse. The test file is discarded. - Scenario 2:
cwd = "C:/",file = "C:/test.js".file.slice(3 + 1)returns"est.js". The test file is corrupted.
Proposed Fix
Use standard path.relative (which natively handles Windows case-insensitivity, drive boundaries, and root slashes) combined with slash():
--- a/lib/glob-helpers.js
+++ b/lib/glob-helpers.js
@@ -79,16 +79,12 @@ export function matches(file, patterns) {
export function normalizeFileForMatching(cwd, file) {
- if (process.platform === 'win32') {
- cwd = slash(cwd);
- file = slash(file);
- }
-
- // Note that if `file` is outside `cwd` we can't normalize it. If this turns
- // out to be a real-world scenario we may have to make changes in calling code
- // to make sure the file isn't even selected for matching.
- if (!file.startsWith(cwd)) {
- return file;
+ const rel = path.relative(cwd, file);
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
+ return slash(file);
}
- // Assume `cwd` does *not* end in a slash.
- return file.slice(cwd.length + 1);
+ return slash(rel);
}Bonus Finding: isLikeSelector Rejects Object.create(null) & Mutates Prototype via __proto__
Metadata
- Component: Assertion Library (
lib/like-selector.js) - Affected File:
lib/like-selector.js(lines 3–15, 24–41)
Problem Summary
isLikeSelectorchecks:Null-prototype objects (if (isPrimitive(selector) || (!Array.isArray(selector) && Reflect.getPrototypeOf(selector) !== Object.prototype)) { return false; }Object.create(null)) have a prototype ofnull(null !== Object.prototype), causingisLikeSelectorto reject clean dictionaries as invalid selectors.- In
selectComparable:Ifcomparable[key] = Reflect.get(actual, key);selectorcontains the key'__proto__', assigningcomparable['__proto__'] = ...modifies the prototype ofcomparableinstead of setting an own property, leading to prototype mutation during comparison. UseReflect.definePropertyto safely preserve own properties.
Source: avajs/ava