From 88a0ab5af98842d590af86ee15f3c58e9b9d646c Mon Sep 17 00:00:00 2001 From: realDiligent Date: Tue, 7 Jul 2026 14:39:05 +0800 Subject: [PATCH] fix(mcp): sync score-preview isCodeFile mirrors with isSourcePath (.kts) Add .kts to the MCP local-branch and score-preview classifiers so Gradle Kotlin-script source counts as code in local score previews, matching merged #3889 server isSourcePath behavior. Co-authored-by: Cursor --- packages/gittensory-mcp/lib/local-branch.js | 2 +- .../scripts/gittensor-score-preview.mjs | 2 +- .../scripts/gittensor-score-preview.py | 2 +- test/unit/local-branch.test.ts | 4 ++++ test/unit/path-matchers.test.ts | 20 +++++++++++++++++ test/unit/score-preview-script.test.ts | 22 +++++++++++++++++++ 6 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/gittensory-mcp/lib/local-branch.js b/packages/gittensory-mcp/lib/local-branch.js index 492e8b4dcf..81f68acfa3 100644 --- a/packages/gittensory-mcp/lib/local-branch.js +++ b/packages/gittensory-mcp/lib/local-branch.js @@ -614,7 +614,7 @@ function isGeneratedCodeFile(file) { } export function isCodeFile(file) { - return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); + return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|kts|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); } function numberValue(value) { diff --git a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs index ed2c55a7bd..b2ea3ab928 100644 --- a/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs +++ b/packages/gittensory-mcp/scripts/gittensor-score-preview.mjs @@ -23,7 +23,7 @@ function isGeneratedCodeFile(file) { } function isCodeFile(file) { - return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); + return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|kts|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file); } function lineCount(file) { diff --git a/packages/gittensory-mcp/scripts/gittensor-score-preview.py b/packages/gittensory-mcp/scripts/gittensor-score-preview.py index 6c94982dbd..4cc8d1d364 100644 --- a/packages/gittensory-mcp/scripts/gittensor-score-preview.py +++ b/packages/gittensory-mcp/scripts/gittensor-score-preview.py @@ -146,7 +146,7 @@ def metadata_fallback(metadata: dict) -> dict: lines = max(int(entry.get("additions") or 0) + int(entry.get("deletions") or 0), 0) if is_test_file(path): tests += lines - elif lower_path.endswith((".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".rs", ".go", ".java", ".kt", ".scala", ".sql", ".cs", ".swift", ".groovy", ".php", ".cpp", ".cc", ".c", ".h", ".hpp", ".m", ".vue", ".svelte", ".astro", ".dart")) and not is_generated_code_file(lower_path): + elif lower_path.endswith((".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".rs", ".go", ".java", ".kt", ".kts", ".scala", ".sql", ".cs", ".swift", ".groovy", ".php", ".cpp", ".cc", ".c", ".h", ".hpp", ".m", ".vue", ".svelte", ".astro", ".dart")) and not is_generated_code_file(lower_path): source += lines else: non_code += lines diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index dc93def9b8..fca4efba1e 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1792,6 +1792,10 @@ describe("local MCP git metadata collection", () => { expect(isTestFile(file)).toBe(false); expect(isCodeFile(file)).toBe(false); } + // #3889 follow-up: .kts source must count as code in the MCP copy now that isSourcePath knows it. + expect(isCodeFile("app/Build.kts")).toBe(true); + expect(isTestFile("build/SettingsTests.kts")).toBe(true); + expect(isCodeFile("build/SettingsTests.kts")).toBe(false); }); it("extracts linked issues only from standalone closing keywords, not keyword substrings", async () => { diff --git a/test/unit/path-matchers.test.ts b/test/unit/path-matchers.test.ts index 45caf65722..8550de7fa5 100644 --- a/test/unit/path-matchers.test.ts +++ b/test/unit/path-matchers.test.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { classifyChangedFile, + isCodeFile, isDependencyManifestFile, isConfigFile, isDocsFile, @@ -26,6 +27,25 @@ describe("path-matchers.ts never imports from local-branch.ts", () => { }); }); +describe("isCodeFile extension sets", () => { + it("keeps isSourcePath core extensions and EXTENDED_SOURCE_EXTENSION disjoint", () => { + const testEvidenceSource = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../src/signals/test-evidence.ts"), "utf8"); + const pathMatchersSource = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../src/signals/path-matchers.ts"), "utf8"); + const coreMatch = testEvidenceSource.match(/SOURCE_FILE_EXTENSION = \/\\\.\(([^)]+)\)\$\/i/); + const extendedMatch = pathMatchersSource.match(/EXTENDED_SOURCE_EXTENSION = \/\\\.\(([^)]+)\)\$\/i/); + expect(coreMatch).not.toBeNull(); + expect(extendedMatch).not.toBeNull(); + const coreExts = new Set(coreMatch![1]!.split("|")); + const extendedExts = new Set(extendedMatch![1]!.split("|")); + expect([...coreExts].filter((ext) => extendedExts.has(ext))).toEqual([]); + }); + + it("classifies .kts Gradle Kotlin-script source as code via the core matcher", () => { + expect(isCodeFile("app/Build.kts")).toBe(true); + expect(isCodeFile("build/SettingsTests.kts")).toBe(false); + }); +}); + describe("isGeneratedFile", () => { it("matches generated output by directory, suffix, codegen, and source maps", () => { for (const path of [ diff --git a/test/unit/score-preview-script.test.ts b/test/unit/score-preview-script.test.ts index fe216d8277..a89120e85d 100644 --- a/test/unit/score-preview-script.test.ts +++ b/test/unit/score-preview-script.test.ts @@ -213,6 +213,28 @@ describe("gittensor-score-preview.mjs classifier parity with the server", () => expect(py.nonCodeTokenScore).toBe(0); }); + it("classifies Kotlin-script source and class-suffix .kts tests in both .mjs and .py previews", () => { + const files = [ + { path: "app/Build.kts", additions: 9, deletions: 0 }, + { path: "build/SettingsTests.kts", additions: 4, deletions: 0 }, + ]; + const mjs = runPreview(files); + expect(mjs.sourceTokenScore).toBe(9); + expect(mjs.testTokenScore).toBe(4); + expect(mjs.nonCodeTokenScore).toBe(0); + + const python = findPython(); + if (!python) return; + const env = { ...process.env }; + delete env.GITTENSOR_ROOT; + const res = spawnSync(python, [scriptPy], { input: JSON.stringify({ changedFiles: files }), encoding: "utf8", env }); + expect(res.status, res.stderr).toBe(0); + const py = JSON.parse(res.stdout); + expect(py.sourceTokenScore).toBe(9); + expect(py.testTokenScore).toBe(4); + expect(py.nonCodeTokenScore).toBe(0); + }); + it("does not misclassify a *.test.mjs.map source-map as a test (extension anchored to end-of-path, matching the server)", () => { // A substring match on ".test.mjs" wrongly flagged non-tests like dist/widget.test.mjs.map; the rule must be // end-anchored like isTestPath. It's a source-map — neither test nor code — so it counts as non-code.