Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

#### Symbols, tests and the viewer

- `codegraph affected` now recognises every ecosystem's test files — Go `foo_test.go`, Python `test_foo.py`, JVM `FooTest.kt` and the rest — instead of only `.test.`/`.spec.` names, so it stops reporting "no tests affected" for projects that have them. (#1507)
- 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.
Expand Down
66 changes: 66 additions & 0 deletions __tests__/cli-affected-test-conventions.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
17 changes: 7 additions & 10 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { getTelemetry, TELEMETRY_DOCS, recordIndexEvent } from '../telemetry';
// server itself is loaded lazily inside the `ui` action. See ui-server/constants.
import { BROWSER_ENV, DEFAULT_UI_PORT } from '../ui-server/constants';
import type { UiServerHandle } from '../ui-server';
import { isTestPath } from '../search/query-utils';
import { lookupSymbolNodes, describeSymbolNode, groupDefinitions } from '../graph/symbol-lookup';
import type { Node, Edge } from '../types';

Expand Down Expand Up @@ -2452,15 +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;
Expand All @@ -2474,9 +2466,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
Expand Down