GitNexus/gitnexus/test/unit/test-file-path.test.ts
Carter LaSalle c283b21e9a
fix(mcp): honor includeTests for C#, Java, Swift and PHP test paths (#2866)
* fix(mcp): honor includeTests for C#, Java, Swift and PHP test paths

Test-file classification had two hand-maintained implementations that had drifted:

  core/ingestion/entry-point-scoring.ts  isTestFile      — excludes tests from
                                                           process entry points
  mcp/local/local-backend.ts             isTestFilePath  — backs `includeTests`
                                                           on impact/trace/context

The MCP copy recognized no C#, Java or Swift test convention and neither PHP
form, so `includeTests: false` silently failed to filter them — a C# project's
`*.Tests/` and a Maven project's `src/test/` landed in blast-radius output as
though they were production callers. The scoring copy missed `/fixtures/` and
`/conftest.`, so those could be selected as process entry points.

Both now delegate to one predicate carrying the union of the two pattern sets.
Public names are unchanged, so importers are unaffected.

The duplication was not gratuitous: `entry-point-scoring.ts` imports the
language-provider registry, and #2802 deliberately cut that closure out of MCP
server startup — importing it from `local-backend.ts` would put it back. The
shared predicate therefore lives in its own module with NO imports, and a test
asserts it declares none, so the startup cost cannot be reintroduced by a future
import added there.

19 new tests: the previously-missed paths per language, nullish and
Windows-separator handling, production paths that must NOT match, and a
cross-check that both public names agree on every case — the regression guard for
the drift itself.

Behavior-neutral on a Python/Go/TypeScript repository (204,336 nodes / 299,580
edges / 813 flows before and after), since the newly-recognized patterns are
languages it does not contain. `npx tsc --noEmit` clean; 728 tests pass across
the entry-point, process, impact and test-file suites.

* Address PR review feedback (#2866)

Tighten the shared test-path predicate so includeTests filtering no longer
treats Contest.swift/Latest.php as tests, unanchored uitests/ as a substring,
or production /fixtures/ trees as test code.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify the shared test-path matcher

Drop substring needles already covered by /test/, /tests/, and /spec/,
and inline the one-off slash-prefix helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#2866)

Correct the module header: scoring already matched /test/, so
/test/fixtures/ was never the scoring gap — only /conftest. was.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#2866)

Classify Xcode *UITests path segments without restoring the
unanchored uitests/ substring that also matches fruitests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Carter LaSalle <carterlasalle@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 17:43:53 +01:00

106 lines
3.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { isTestFilePath } from '../../src/core/ingestion/utils/test-file-path.js';
import { isTestFile } from '../../src/core/ingestion/entry-point-scoring.js';
import { isTestFilePath as backendIsTestFilePath } from '../../src/mcp/local/local-backend.js';
describe('isTestFilePath — shared predicate', () => {
it('returns false for nullish input rather than throwing', () => {
expect(isTestFilePath(undefined)).toBe(false);
expect(isTestFilePath(null)).toBe(false);
expect(isTestFilePath('')).toBe(false);
});
it('normalizes Windows separators and casing', () => {
expect(isTestFilePath('SRC\\Test\\FooTests.cs')).toBe(true);
expect(isTestFilePath('pkg\\thing_test.go')).toBe(true);
expect(isTestFilePath('src\\Widgets.Tests\\WidgetTests.cs')).toBe(true);
});
// These were recognized by entry-point scoring but NOT by the MCP copy, so
// `includeTests: false` silently failed to filter them.
for (const p of [
'app/src/test/java/com/x/FooTest.java',
'ios/MyAppTests/LoginTests.swift',
'ios/MyAppUITests/FlowTest.swift',
'ios/MyAppUITests/Flow.swift',
'ios/MyAppUITests/README.swift',
'src/Widgets.Tests/WidgetTests.cs',
'src/Widgets.UnitTests/Thing.cs',
'src/Widgets.IntegrationTests/Thing.cs',
'tests/Feature/LoginTest.php',
'tests/Unit/ThingSpec.php',
'tests/Feature/Support/FakeGateway.php',
]) {
it(`detects a test path the MCP copy used to miss: ${p}`, () => {
expect(isTestFilePath(p)).toBe(true);
});
}
// These were recognized by the MCP copy but NOT by entry-point scoring, so
// they could be selected as process entry points.
for (const p of ['tests/fixtures/sample.py', 'tests/conftest.py']) {
it(`detects a test path entry-point scoring used to miss: ${p}`, () => {
expect(isTestFilePath(p)).toBe(true);
});
}
for (const p of [
'src/app/widgets.ts',
'src/core/ingestion/utils/test-file-path.ts',
'pkg/service/handler.go',
'app/models/user.rb',
'Contest.swift',
'Contest.cs',
'Protest.cs',
'Latest.php',
'src/fixtures/schema.ts',
'src/fruitests/helpers.swift',
]) {
it(`does not classify production code as test: ${p}`, () => {
expect(isTestFilePath(p)).toBe(false);
});
}
});
describe('test-file classification has exactly one implementation', () => {
// Regression guard: two hand-maintained copies drifted apart once already.
// Both public names must delegate to the same predicate.
const paths = [
'src/app/widgets.ts',
'app/src/test/java/com/x/FooTest.java',
'ios/MyAppTests/LoginTests.swift',
'src/Widgets.Tests/WidgetTests.cs',
'tests/conftest.py',
'tests/fixtures/sample.py',
'src/fixtures/schema.ts',
'Contest.swift',
'spec/models/user_spec.rb',
'pkg/thing_test.go',
'tests/Feature/LoginTest.php',
];
it('entry-point-scoring isTestFile agrees with the shared predicate', () => {
for (const p of paths) expect(isTestFile(p)).toBe(isTestFilePath(p));
});
it('local-backend isTestFilePath agrees with the shared predicate', () => {
for (const p of paths) expect(backendIsTestFilePath(p)).toBe(isTestFilePath(p));
});
});
describe('shared predicate stays dependency-free', () => {
// Poka-yoke for #2802: `local-backend.ts` imports this module, so anything
// imported here lands in MCP server startup. The duplication this module
// replaced existed precisely because `entry-point-scoring.ts` pulls in the
// language-provider registry. An import added here would silently reintroduce
// that startup cost.
it('declares no imports', () => {
const src = readFileSync(
new URL('../../src/core/ingestion/utils/test-file-path.ts', import.meta.url),
'utf8',
);
const imports = src.split('\n').filter((l) => /^\s*(import\b|export\s.*\sfrom\s)/.test(l));
expect(imports).toEqual([]);
});
});