diff --git a/README.md b/README.md index e36fcf7..559e884 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![npm version](https://img.shields.io/npm/v/@colbymchenry/codegraph.svg)](https://www.npmjs.com/package/@colbymchenry/codegraph) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/) +[![Node.js](https://img.shields.io/badge/Node.js-20--24-green.svg)](https://nodejs.org/) [![Windows](https://img.shields.io/badge/Windows-supported-blue.svg)](#) [![macOS](https://img.shields.io/badge/macOS-supported-blue.svg)](#) diff --git a/__tests__/is-test-file.test.ts b/__tests__/is-test-file.test.ts new file mode 100644 index 0000000..e3fc6d0 --- /dev/null +++ b/__tests__/is-test-file.test.ts @@ -0,0 +1,53 @@ +/** + * isTestFile heuristic — test-file detection used to deprioritize test code in + * search/explore ranking. + * + * Regression coverage for the cold-query fix: the heuristic previously only + * knew Java/JS/Python conventions, so Kotlin (`*Test.kt`, `jvmTest/`), Swift + * (`*Tests.swift`), and camelCase test source-set dirs slipped through — which + * let OkHttp's tests flood `codegraph_explore` results on a plain-language + * query. The false-positive guards matter just as much: `latest.kt` / + * `manifest.kt` / a `RealCall.kt` production file must NOT be flagged. + */ +import { describe, it, expect } from 'vitest'; +import { isTestFile } from '../src/search/query-utils'; + +describe('isTestFile', () => { + it('flags Kotlin test files and source sets', () => { + expect(isTestFile('okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt')).toBe(true); + expect(isTestFile('okhttp/src/commonTest/kotlin/okhttp3/CompressionInterceptorTest.kt')).toBe(true); + expect(isTestFile('app/src/androidTest/java/com/example/FooTest.kt')).toBe(true); + expect(isTestFile('module/src/integrationTest/kotlin/BarSpec.kt')).toBe(true); + }); + + it('flags Swift test files', () => { + expect(isTestFile('Tests/SessionTests.swift')).toBe(true); + expect(isTestFile('Sources/FooTest.swift')).toBe(true); + }); + + it('still flags the previously-supported conventions', () => { + expect(isTestFile('foo/test_bar.py')).toBe(true); + expect(isTestFile('pkg/bar_test.go')).toBe(true); + expect(isTestFile('src/foo.test.ts')).toBe(true); + expect(isTestFile('src/foo.spec.ts')).toBe(true); + expect(isTestFile('com/example/FooTest.java')).toBe(true); + expect(isTestFile('com/example/FooTestCase.java')).toBe(true); + expect(isTestFile('project/__tests__/foo.ts')).toBe(true); + expect(isTestFile('project/tests/foo.rb')).toBe(true); + }); + + it('does NOT flag production files that merely contain "test" lowercase', () => { + // The fix is capital-led so camelCase boundaries distinguish these. + expect(isTestFile('src/latest/loader.kt')).toBe(false); + expect(isTestFile('lib/manifest.kt')).toBe(false); + expect(isTestFile('okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/RealCall.kt')).toBe(false); + expect(isTestFile('src/contestEntry.ts')).toBe(false); + expect(isTestFile('pkg/greatest.go')).toBe(false); + }); + + it('does NOT flag ordinary production source', () => { + expect(isTestFile('src/flask/app.py')).toBe(false); + expect(isTestFile('src/vs/workbench/api/common/extensionHostMain.ts')).toBe(false); + expect(isTestFile('okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt')).toBe(false); + }); +}); diff --git a/src/search/query-utils.ts b/src/search/query-utils.ts index 9a61aca..da0645f 100644 --- a/src/search/query-utils.ts +++ b/src/search/query-utils.ts @@ -207,36 +207,43 @@ export function scorePathRelevance(filePath: string, query: string): number { */ export function isTestFile(filePath: string): boolean { const lower = filePath.toLowerCase(); - const fileName = path.basename(lower); + const fileName = path.basename(filePath); // original case — needed for camelCase boundaries + const lowerName = fileName.toLowerCase(); - // Common test file patterns - return ( - fileName.startsWith('test_') || - fileName.startsWith('test.') || - fileName.endsWith('.test.ts') || - fileName.endsWith('.test.js') || - fileName.endsWith('.test.tsx') || - fileName.endsWith('.test.jsx') || - fileName.endsWith('.spec.ts') || - fileName.endsWith('.spec.js') || - fileName.endsWith('_test.go') || - fileName.endsWith('_test.py') || - fileName.endsWith('_test.rs') || - fileName.endsWith('Tests.java') || - fileName.endsWith('Test.java') || - fileName.endsWith('Tester.java') || - fileName.endsWith('TestCase.java') || - lower.includes('/tests/') || - lower.includes('/test/') || - lower.includes('/__tests__/') || - lower.includes('/spec/') || - lower.includes('/testlib/') || + // --- Filename patterns --- + if ( + lowerName.startsWith('test_') || // python: test_foo.py + lowerName.startsWith('test.') || + // separator-delimited: foo_test.go, foo.test.ts, foo-spec.rb, bar_spec.py + /[._-](test|tests|spec|specs)\.[a-z0-9]+$/.test(lowerName) || + // CamelCase suffix (Java/Kotlin/Swift/C#/Scala): FooTest.kt, BarTests.swift, + // BazSpec.scala, QuxTestCase.java. Capital-led so "latest.kt"/"manifest.kt" + // (lowercase "test") are NOT matched. + /(?:Test|Tests|TestCase|Tester|Spec|Specs)\.[A-Za-z0-9]+$/.test(fileName) + ) { + return true; + } + + // --- Directory patterns --- + if ( + lower.includes('/tests/') || lower.includes('/test/') || + lower.includes('/__tests__/') || lower.includes('/spec/') || + lower.includes('/specs/') || lower.includes('/testlib/') || lower.includes('/testing/') || - // Non-production directories: examples, samples, benchmarks, fixtures, demos. - // Check both mid-path (/integration/) and start-of-path (integration/) since - // file paths may be stored as relative paths without a leading slash. - matchesNonProductionDir(lower) - ); + lower.startsWith('test/') || lower.startsWith('tests/') || + lower.startsWith('spec/') || lower.startsWith('specs/') || + // CamelCase test source-set dirs (Kotlin Multiplatform / Gradle / Xcode): + // jvmTest/, commonTest/, androidTest/, iosTest/, integrationTest/. Capital-led + // so "latest/" / "manifest/" are not matched. + /(?:^|\/)[A-Za-z0-9]*(?:Test|Tests|Spec)\//.test(filePath) + ) { + return true; + } + + // Non-production directories: examples, samples, benchmarks, fixtures, demos. + // Check both mid-path (/integration/) and start-of-path (integration/) since + // file paths may be stored as relative paths without a leading slash. + return matchesNonProductionDir(lower); } /**