diff --git a/CHANGELOG.md b/CHANGELOG.md index b190092..8b7af93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **JS/TS test-file excludes suggested by wizard + config help** (GH #165). + Unexcluded co-located test files (`*.spec.ts` / `*.test.ts` / `__tests__`) + were the upstream root cause of graph-export edge pollution (lh-enterprise: + 77% of edges from test files, mocks all unresolved). `codeindex init`'s + wizard now suggests the test exclude patterns when such files exist (same + conditional style as `vendor/`/`target/`), and `config explain exclude` + finally lists them — graph-export's high-unresolved warning already pointed + there. Fixed at the config seam (single source of truth for both `scan-all` + and `graph-export`); a separate `--exclude-tests` flag was rejected as it + would let the two see different trees. - **Characterization tests immune to local fixture pollution** (GH #135 residual). `test_graphbuffer_baseline` copies the fixture including any on-disk `README_AI.md` left by a repo-root `scan-all` (gitignored by diff --git a/src/codeindex/config_help.py b/src/codeindex/config_help.py index 1808c0e..e8771a8 100644 --- a/src/codeindex/config_help.py +++ b/src/codeindex/config_help.py @@ -86,10 +86,20 @@ "type": "list[string]", "default": "(standard exclusions)", "description": "Directory patterns to exclude from scanning", + "recommendations": """ + • Always: __pycache__, node_modules, .git + • JS/TS projects: exclude co-located test files — they are mock-heavy + and pollute graph-export edges (GH #165): + - "**/*.spec.ts" / "**/*.spec.tsx" / "**/*.spec.js" + - "**/*.test.ts" / "**/*.test.tsx" / "**/*.test.js" + - "**/__tests__/**\"""", "example": """exclude: - "**/__pycache__/**" - "**/node_modules/**" - - "**/.git/**\"""", + - "**/.git/**" + - "**/*.spec.ts" + - "**/*.test.ts" + - "**/__tests__/**\"""", }, "output_file": { "name": "output_file", diff --git a/src/codeindex/init_wizard.py b/src/codeindex/init_wizard.py index 6983184..6550758 100644 --- a/src/codeindex/init_wizard.py +++ b/src/codeindex/init_wizard.py @@ -269,6 +269,18 @@ def infer_include_patterns(project_dir: Path) -> List[str]: return sorted(includes) +def _has_js_test_files(project_dir: Path) -> bool: + """True if the project contains JS/TS co-located test files or a __tests__ dir.""" + suffixes = (".spec.ts", ".spec.tsx", ".spec.js", ".test.ts", ".test.tsx", ".test.js") + for root, dirs, files in os.walk(project_dir): + dirs[:] = [d for d in dirs if d not in ("node_modules", ".git", "__pycache__")] + if "__tests__" in dirs: + return True + if any(f.endswith(suffixes) for f in files): + return True + return False + + def infer_exclude_patterns(project_dir: Path) -> List[str]: """Infer exclude patterns based on common artifacts. @@ -309,6 +321,22 @@ def infer_exclude_patterns(project_dir: Path) -> List[str]: if (project_dir / dir_name).exists(): excludes.append(pattern) + # GH #165: JS/TS co-located test files are mock-heavy and near-zero nav + # value; unexcluded they polluted graph-export with 77% test edges. + # Suggested only when present — same conditional style as above. + if _has_js_test_files(project_dir): + excludes.extend( + [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.test.js", + "**/__tests__/**", + ] + ) + return sorted(set(excludes)) diff --git a/tests/test_init_wizard_exclude.py b/tests/test_init_wizard_exclude.py new file mode 100644 index 0000000..273e203 --- /dev/null +++ b/tests/test_init_wizard_exclude.py @@ -0,0 +1,53 @@ +"""JS/TS test-file exclude suggestions (GH #165). + +Root cause of graph-export edge pollution (77% of edges from test files, +mocks all unresolved): the wizard/config template never suggested JS/TS +test patterns, so `.codeindex.yaml` shipped without them. Fixed at the +config seam — a `--exclude-tests` flag would let scan-all and +graph-export see different trees. +""" + +from pathlib import Path + +from codeindex.config_help import CONFIG_PARAMS +from codeindex.init_wizard import infer_exclude_patterns + +JS_TS_TEST_PATTERNS = [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.test.js", + "**/__tests__/**", +] + + +def test_suggests_test_excludes_when_spec_files_exist(tmp_path: Path) -> None: + (tmp_path / "src").mkdir() + (tmp_path / "src" / "foo.service.ts").write_text("export class Foo {}\n") + (tmp_path / "src" / "foo.service.spec.ts").write_text("describe('Foo', () => {});\n") + + excludes = infer_exclude_patterns(tmp_path) + + for pattern in JS_TS_TEST_PATTERNS: + assert pattern in excludes, f"missing {pattern}" + + +def test_no_test_excludes_for_pure_python_project(tmp_path: Path) -> None: + (tmp_path / "src").mkdir() + (tmp_path / "src" / "main.py").write_text("x = 1\n") + + excludes = infer_exclude_patterns(tmp_path) + + assert not any(p in excludes for p in JS_TS_TEST_PATTERNS) + + +def test_explain_exclude_lists_test_patterns() -> None: + """graph-export's warning says 'run: codeindex config explain exclude' — + that output must actually contain the test patterns it points to.""" + exclude_help = CONFIG_PARAMS["exclude"] + text = exclude_help.get("recommendations", "") + exclude_help.get("example", "") + + assert "**/*.spec.ts" in text + assert "**/__tests__/**" in text