Browse Source

fix(cli): affected shares the tool's one notion of a test file (#1507) (#1803)

`codegraph affected` kept six regexes of its own — `.test.`, `.spec.`,
`/tests/`… — so a Go `foo_test.go`, a Python `test_foo.py` or a JVM
`FooTest.kt` beside the changed file was never reported, and "no tests
affected" read as "no coverage". Use isTestPath from search/query-utils,
the same predicate search and the MCP tools already rank by.

Cherry-picked from danusha2345's upstream PR #1688
(commit 995b6f1f262564cafa6eb0b026b84fa4122f2cb0).
Preserve main's CLI imports and Unreleased entries, and credit the
contributor in the changelog. The default affected depth remains 5.

Fixes #1507.
Supersedes #1688.

Verified on Linux with Node 22.19.0: npm run build; the Go fixture
changes from no affected tests to math_test.go; matching and nonmatching
custom filters still override. Vitest: 3 files, 36 tests passed, including
the upstream Go/Python/Kotlin suite and affected path/dependency coverage.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Colby Mchenry 13 hours ago
parent
commit
64bd45cd2a
3 changed files with 75 additions and 11 deletions
  1. 2 0
      CHANGELOG.md
  2. 66 0
      __tests__/cli-affected-test-conventions.test.ts
  3. 7 11
      src/bin/codegraph.ts

+ 2 - 0
CHANGELOG.md

@@ -227,6 +227,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 #### Symbols, tests and the viewer
 
+- `codegraph affected` now finds Go, Python and JVM test files that previously went unreported, while preserving custom `--filter` behavior (thanks @danusha2345; #1507, #1688).
+
 - Calls inside declaration initializers in Kotlin, Java, TypeScript, JavaScript, Scala, Rust and Python now appear under the declaration that owns them, making callers and impact results more accurate after re-indexing with `codegraph index -f` (thanks @danusha2345; #1510, #1511).
 - Java fields initialized with anonymous classes now expose their methods and calls in the graph.
 - Kotlin property accessors, initialization blocks and destructuring declarations now retain their calls with the correct owner.

+ 66 - 0
__tests__/cli-affected-test-conventions.test.ts

@@ -0,0 +1,66 @@
+/**
+ * `codegraph affected` recognises every ecosystem's test-file convention (#1507).
+ *
+ * The command used to carry its own six regexes — `.test.`, `.spec.`,
+ * `/tests/`… — so a Go `foo_test.go`, a Python `test_foo.py` or a JVM
+ * `FooTest.kt` beside the changed file was never reported, and "no tests
+ * affected" read as "no coverage". It now shares `isTestPath` with search and
+ * the MCP tools. Exercised end-to-end against the built binary.
+ */
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { CodeGraph } from '../src';
+
+const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
+
+function affected(cwd: string, args: string[]): string[] {
+  const out = execFileSync(process.execPath, [BIN, 'affected', ...args, '--quiet', '-p', cwd], {
+    encoding: 'utf-8',
+    env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
+    stdio: ['ignore', 'pipe', 'pipe'],
+  });
+  return out.split('\n').map((s) => s.trim()).filter(Boolean);
+}
+
+describe('codegraph affected — test-file conventions (#1507)', () => {
+  let dir: string;
+
+  beforeAll(async () => {
+    dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-affected-conv-'));
+    const w = (rel: string, body: string) => {
+      fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
+      fs.writeFileSync(path.join(dir, rel), body);
+    };
+    w('go.mod', 'module example.com/demo\n\ngo 1.22\n');
+    w('math.go', 'package demo\n\nfunc Add(a, b int) int { return a + b }\n');
+    w('math_test.go', 'package demo\n\nimport "testing"\n\nfunc TestAdd(t *testing.T) { if Add(1, 2) != 3 { t.Fatal("boom") } }\n');
+    w('pkg/calc.py', 'def add(a, b):\n    return a + b\n');
+    w('pkg/test_calc.py', 'from pkg.calc import add\n\ndef test_add():\n    assert add(1, 2) == 3\n');
+    w('src/main/kotlin/app/Calc.kt', 'package app\n\nclass Calc {\n    fun add(a: Int, b: Int): Int = a + b\n}\n');
+    w('src/test/kotlin/app/CalcTest.kt', 'package app\n\nclass CalcTest {\n    fun addsNumbers() { Calc().add(1, 2) }\n}\n');
+    const cg = CodeGraph.initSync(dir);
+    await cg.indexAll();
+    cg.close();
+  });
+
+  afterAll(() => {
+    fs.rmSync(dir, { recursive: true, force: true });
+  });
+
+  it('reports the sibling Go _test.go file', () => {
+    expect(affected(dir, ['math.go'])).toEqual(['math_test.go']);
+  });
+
+  it('reports the Python test_ module and the JVM FooTest class', () => {
+    expect(affected(dir, ['pkg/calc.py'])).toEqual(['pkg/test_calc.py']);
+    expect(affected(dir, ['src/main/kotlin/app/Calc.kt'])).toEqual(['src/test/kotlin/app/CalcTest.kt']);
+  });
+
+  it('still honours an explicit --filter glob', () => {
+    expect(affected(dir, ['math.go', '--filter', '*_test.go'])).toEqual(['math_test.go']);
+    expect(affected(dir, ['math.go', '--filter', '*.spec.ts'])).toEqual([]);
+  });
+});

+ 7 - 11
src/bin/codegraph.ts

@@ -61,6 +61,7 @@ import { BROWSER_ENV, DEFAULT_UI_PORT } from '../ui-server/constants';
 import type { UiServerHandle } from '../ui-server';
 import { lookupSymbolNodes, describeSymbolNode, groupDefinitions } from '../graph/symbol-lookup';
 import type { Node, Edge } from '../types';
+import { isTestPath } from '../search/query-utils';
 
 // Decided once, before `--color`/`--no-color` are stripped from argv below
 // (#1281). Piped/redirected stdout, NO_COLOR, or --no-color -> plain output.
@@ -2452,16 +2453,6 @@ program
       const cg = await CodeGraph.open(projectPath);
       const maxDepth = parseInt(options.depth || '5', 10);
 
-      // Common test file patterns
-      const defaultTestPatterns = [
-        /\.spec\./,
-        /\.test\./,
-        /\/__tests__\//,
-        /\/tests?\//,
-        /\/e2e\//,
-        /\/spec\//,
-      ];
-
       // Custom filter pattern
       let customFilter: RegExp | null = null;
       if (options.filter) {
@@ -2474,9 +2465,14 @@ program
         customFilter = new RegExp(regex);
       }
 
+      // One notion of "a test" for the whole tool (#1507): the CLI used to keep
+      // its own six regexes here, which knew `.test.` and `/tests/` but not Go's
+      // `_test.go`, Python's `test_x.py` or the JVM's `FooTest.kt` — so
+      // `affected` reported "no tests" for whole ecosystems while `search` and
+      // the MCP tools counted those very files as tests.
       function isTestFile(filePath: string): boolean {
         if (customFilter) return customFilter.test(filePath);
-        return defaultTestPatterns.some(p => p.test(filePath));
+        return isTestPath(filePath);
       }
 
       // BFS to find all transitive dependents of changed files, filtered to test files