diff --git a/packages/gittensory-mcp/lib/local-branch.js b/packages/gittensory-mcp/lib/local-branch.js index 949e07a181..5a4a034145 100644 --- a/packages/gittensory-mcp/lib/local-branch.js +++ b/packages/gittensory-mcp/lib/local-branch.js @@ -597,9 +597,14 @@ export function isTestFile(file) { /(^|\/)(test|tests|spec|__tests__)\//i.test(file) || /(^|\/)src\/test\//i.test(file) || /(^|\/)[^/]+_test\.(go|py|rb)$/i.test(file) || + /(^|\/)test_[^/]*\.py$/i.test(file) || // pytest's default `test_*.py` prefix convention (the suffix rule above only catches `*_test.py`) /(^|\/)[^/]+_spec\.rb$/i.test(file) || /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs)$/i.test(file) || /(^|\/)[^/]+\.(cy|e2e)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(file) || + // JVM / C# / Swift `SomethingTest(s)`/`SomethingSpec` class-suffix convention (JUnit, Kotlin/ScalaTest, + // Spock, xUnit/NUnit, XCTest). Case-sensitive on the PascalCase suffix so it can't false-positive on words + // that merely end in "test"/"spec" (Latest.java, Contest.cs, manifest.scala). + /(^|\/)\w*(Tests?|Spec)\.(java|kt|kts|scala|cs|swift|groovy)$/.test(file) || /(^|\/)__snapshots__\//i.test(file) ); } diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 5f62a051b8..e538e334ac 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1762,6 +1762,18 @@ describe("local MCP git metadata collection", () => { expect(isTestFile(file)).toBe(true); expect(isCodeFile(file)).toBe(false); } + // #2666 + #2743 parity: the pytest `test_*.py` prefix and the JVM/C#/Swift `SomethingTest(s)`/`Spec` + // class-suffix conventions were added to the server isTestPath but not this MCP copy — so the local + // predictor wrongly counted Java/Kotlin/Scala/C#/Swift tests and pytest-prefixed files as SOURCE. + for (const file of ["tests/test_utils.py", "test_api.py", "app/FooTests.java", "src/BarSpec.kt", "core/BazTest.scala", "svc/QuuxTests.cs", "ios/CorgeSpec.swift", "build/GraultTest.groovy"]) { + expect(isTestFile(file)).toBe(true); + expect(isCodeFile(file)).toBe(false); + } + // Case-sensitive on the PascalCase suffix: a JVM source merely ENDING in "test"/"spec" stays source. + for (const file of ["src/Latest.java", "core/manifest.scala", "app/MyService.kt"]) { + expect(isTestFile(file)).toBe(false); + expect(isCodeFile(file)).toBe(true); + } }); it("extracts linked issues only from standalone closing keywords, not keyword substrings", async () => {