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., from process.cwd() vs pathToFileURL vs resolve). Because file.startsWith(cwd) performs a case-sensitive string check, c:/project/test.js does NOT start with C:/project. The path is not stripped and is returned as a full absolute path, causing matches(file, filePatterns) to fail and test files to be silently ignored.
- Drive Root Path Corruption: The function assumes
cwd does not end in a slash and computes file.slice(cwd.length + 1). When running from a drive root (e.g. C:/), cwd.length + 1 is 4, which slices off the first character of the filename (C:/test.js becomes est.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) is false. Returns "c:/Users/dev/project/test/app.test.js".
classify() passes this to picomatch('test/**/*.js'), which returns false. 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
isLikeSelector checks:
if (isPrimitive(selector) || (!Array.isArray(selector) && Reflect.getPrototypeOf(selector) !== Object.prototype)) {
return false;
}
Null-prototype objects (Object.create(null)) have a prototype of null (null !== Object.prototype), causing isLikeSelector to reject clean dictionaries as invalid selectors.
- In
selectComparable:
comparable[key] = Reflect.get(actual, key);
If selector contains the key '__proto__', assigning comparable['__proto__'] = ... modifies the prototype of comparable instead of setting an own property, leading to prototype mutation during comparison. Use Reflect.defineProperty to safely preserve own properties.
Issue :- (High Severity Cross-Platform Bug): Windows Test Discovery Failure via Case-Sensitive Prefix Matching in
normalizeFileForMatchingMetadata
[Bug] Windows test discovery fails due to drive letter case sensitivity and root drive slicing in normalizeFileForMatchinglib/glob-helpers.js)lib/glob-helpers.js(normalizeFileForMatching, lines 79–94)Problem Summary
normalizeFileForMatchingstrips 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:
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.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.jslines 79–94: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.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 withslash():Bonus Finding:
isLikeSelectorRejectsObject.create(null)& Mutates Prototype via__proto__Metadata
lib/like-selector.js)lib/like-selector.js(lines 3–15, 24–41)Problem Summary
isLikeSelectorchecks:Object.create(null)) have a prototype ofnull(null !== Object.prototype), causingisLikeSelectorto reject clean dictionaries as invalid selectors.selectComparable: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.