Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
0ea50e1803 fix(vscode): respect bun.test.filePattern setting in isTestFile
The isTestFile() method was using a hardcoded regex that only matched
.test. and .spec. patterns, ignoring the user's bun.test.filePattern
configuration. This caused files that don't match the custom pattern
to still be added to the Test Explorer when manually opened.

Now isTestFile() uses the customFilePattern() method to respect the
user's configuration. This allows users to configure patterns like
**/*.bun.test.{js,ts} to avoid conflicts with other test runners
like Vitest in monorepo setups.

Also adds a length check on glob patterns to prevent potential ReDoS
attacks from pathological patterns.

Fixes #26067

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 14:10:56 +00:00
2 changed files with 131 additions and 5 deletions

View File

@@ -573,6 +573,95 @@ describe("BunTestController - Test Discovery and Management", () => {
});
});
describe("matchesGlobPattern", () => {
test("should match default test file patterns", () => {
const defaultPattern = "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts,cjs,mjs}";
// Should match .test. files
expect(internal.matchesGlobPattern("/path/to/file.test.ts", defaultPattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/to/file.test.js", defaultPattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/to/file.test.tsx", defaultPattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/to/file.test.jsx", defaultPattern)).toBe(true);
// Should match .spec. files
expect(internal.matchesGlobPattern("/path/to/file.spec.ts", defaultPattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/to/component.spec.js", defaultPattern)).toBe(true);
// Should match _test_ files (pattern is _test_{extension} with no dot before extension)
expect(internal.matchesGlobPattern("/path/to/file_test_ts", defaultPattern)).toBe(true);
// Should match _spec_ files (pattern is _spec_{extension} with no dot before extension)
expect(internal.matchesGlobPattern("/path/to/file_spec_js", defaultPattern)).toBe(true);
});
test("should not match non-test files", () => {
const defaultPattern = "**/*{.test.,.spec.,_test_,_spec_}{js,ts,tsx,jsx,mts,cts,cjs,mjs}";
// Regular source files should not match
expect(internal.matchesGlobPattern("/path/to/component.ts", defaultPattern)).toBe(false);
expect(internal.matchesGlobPattern("/path/to/index.js", defaultPattern)).toBe(false);
expect(internal.matchesGlobPattern("/path/to/utils.tsx", defaultPattern)).toBe(false);
// Files without proper extension should not match
expect(internal.matchesGlobPattern("/path/to/test.txt", defaultPattern)).toBe(false);
expect(internal.matchesGlobPattern("/path/to/test", defaultPattern)).toBe(false);
});
test("should match custom bun test patterns (fixes #26067)", () => {
// Custom pattern for Bun-specific tests to avoid conflicts with Vitest
const bunPattern = "**/*.bun.test.{js,ts,tsx,jsx}";
// Should match Bun-specific test files
expect(internal.matchesGlobPattern("/backend/user.bun.test.ts", bunPattern)).toBe(true);
expect(internal.matchesGlobPattern("/src/api.bun.test.js", bunPattern)).toBe(true);
expect(internal.matchesGlobPattern("/components/Button.bun.test.tsx", bunPattern)).toBe(true);
// Should NOT match regular test files (Vitest files)
expect(internal.matchesGlobPattern("/frontend/component.test.ts", bunPattern)).toBe(false);
expect(internal.matchesGlobPattern("/src/utils.spec.js", bunPattern)).toBe(false);
});
test("should handle patterns with single wildcard", () => {
const pattern = "*.test.ts";
expect(internal.matchesGlobPattern("file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/to/file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("file.spec.ts", pattern)).toBe(false);
});
test("should handle patterns with double wildcard for nested paths", () => {
const pattern = "**/tests/**/*.test.ts";
expect(internal.matchesGlobPattern("/project/tests/unit/file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/project/tests/integration/deep/file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/project/src/file.test.ts", pattern)).toBe(false);
});
test("should handle patterns with question mark wildcard", () => {
const pattern = "**/*.test?.ts";
expect(internal.matchesGlobPattern("/path/file.test1.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/file.testA.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/file.test.ts", pattern)).toBe(false);
});
test("should handle case-insensitive matching", () => {
const pattern = "**/*.TEST.ts";
// Pattern matching should be case-insensitive
expect(internal.matchesGlobPattern("/path/file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/file.TEST.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("/path/file.Test.ts", pattern)).toBe(true);
});
test("should handle Windows-style paths", () => {
const pattern = "**/*.test.ts";
expect(internal.matchesGlobPattern("C:\\project\\src\\file.test.ts", pattern)).toBe(true);
expect(internal.matchesGlobPattern("C:\\Users\\dev\\tests\\unit.test.ts", pattern)).toBe(true);
});
});
describe("getBunExecutionConfig", () => {
test("should return bun execution configuration", () => {
const config = internal.getBunExecutionConfig();
@@ -1004,6 +1093,7 @@ describe("BunTestController - Integration and Coverage", () => {
expect(internal).toHaveProperty("isTestFile");
expect(internal).toHaveProperty("customFilePattern");
expect(internal).toHaveProperty("matchesGlobPattern");
expect(internal).toHaveProperty("getBunExecutionConfig");
expect(internal).toHaveProperty("findTestByPath");
@@ -1026,6 +1116,7 @@ describe("BunTestController - Integration and Coverage", () => {
expect(typeof internal.shouldUseTestNamePattern).toBe("function");
expect(typeof internal.isTestFile).toBe("function");
expect(typeof internal.customFilePattern).toBe("function");
expect(typeof internal.matchesGlobPattern).toBe("function");
expect(typeof internal.getBunExecutionConfig).toBe("function");
expect(typeof internal.findTestByPath).toBe("function");
expect(typeof internal.findTestByName).toBe("function");
@@ -1039,7 +1130,7 @@ describe("BunTestController - Integration and Coverage", () => {
const functionCount = methodNames.filter(name => typeof internal[name] === "function").length;
expect(functionCount).toBe(methodNames.length);
expect(methodNames.length).toBeGreaterThanOrEqual(16);
expect(methodNames.length).toBeGreaterThanOrEqual(17);
});
});
@@ -1057,6 +1148,7 @@ describe("BunTestController - Integration and Coverage", () => {
expect(typeof internal.shouldUseTestNamePattern).toBe("function");
expect(typeof internal.isTestFile).toBe("function");
expect(typeof internal.customFilePattern).toBe("function");
expect(typeof internal.matchesGlobPattern).toBe("function");
expect(typeof internal.getBunExecutionConfig).toBe("function");
expect(typeof internal.findTestByPath).toBe("function");
expect(typeof internal.findTestByName).toBe("function");
@@ -1068,7 +1160,7 @@ describe("BunTestController - Integration and Coverage", () => {
expect(controller._internal).toBeDefined();
const internalMethods = Object.keys(internal);
expect(internalMethods.length).toBeGreaterThanOrEqual(16);
expect(internalMethods.length).toBeGreaterThanOrEqual(17);
});
test("should handle controller disposal", () => {

View File

@@ -136,9 +136,42 @@ export class BunTestController implements vscode.Disposable {
}
private isTestFile(document: vscode.TextDocument): boolean {
return (
document?.uri?.scheme === "file" && /\.(test|spec)\.(js|jsx|ts|tsx|cjs|mjs|mts|cts)$/.test(document.uri.fsPath)
);
if (document?.uri?.scheme !== "file") {
return false;
}
const pattern = this.customFilePattern();
return this.matchesGlobPattern(document.uri.fsPath, pattern);
}
private matchesGlobPattern(filePath: string, globPattern: string): boolean {
// Basic sanity check to prevent pathological patterns
if (globPattern.length > 500) {
debug.appendLine(`Warning: glob pattern too long (${globPattern.length} chars), skipping match`);
return false;
}
// Normalize file path: convert Windows backslashes to forward slashes
const normalizedPath = filePath.replace(/\\/g, "/");
// Convert glob pattern to regex
// Handle common glob patterns like **/*.test.{js,ts}
let regexPattern = globPattern
// Escape special regex characters (except those used in glob patterns)
.replace(/[.+^$|\\]/g, "\\$&")
// Convert ** to match any path
.replace(/\*\*/g, "<<<GLOBSTAR>>>")
// Convert * to match any filename characters (not path separators)
.replace(/\*/g, "[^/]*")
// Restore ** as .* to match anything including path separators
.replace(/<<<GLOBSTAR>>>/g, ".*")
// Convert ? to match single character
.replace(/\?/g, ".")
// Convert {a,b,c} to (a|b|c)
.replace(/\{([^}]+)\}/g, (_, group) => `(${group.split(",").join("|")})`);
// The pattern should match the end of the file path
const regex = new RegExp(`(^|/)${regexPattern}$`, "i");
return regex.test(normalizedPath);
}
private async discoverInitialTests(
@@ -1480,6 +1513,7 @@ export class BunTestController implements vscode.Disposable {
isTestFile: this.isTestFile.bind(this),
customFilePattern: this.customFilePattern.bind(this),
matchesGlobPattern: this.matchesGlobPattern.bind(this),
getBunExecutionConfig: this.getBunExecutionConfig.bind(this),
findTestByPath: this.findTestByPath.bind(this),