Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ 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)

- **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges.

- **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does.
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';

// Decided once, before `--color`/`--no-color` are stripped from argv below
// (#1281). Piped/redirected stdout, NO_COLOR, or --no-color -> plain output.
Expand Down Expand Up @@ -2434,15 +2435,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 @@ -2456,9 +2448,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