@@ -621,3 +621,208 @@ def test_ts_no_workspace_arg_uses_cwd(self, ts_workspace):
621621 assert result ["workspace" ] == os .path .abspath (ts_workspace )
622622 assert "auth/google-auth-cache.ts" in result ["changed_files" ]
623623 assert result ["unresolved" ] == []
624+
625+
626+ # ─── Issue #189: BFS never starts when import parser misses TS patterns ──
627+ #
628+ # PR #184 fixed the workspace-as-first-arg heuristic so the existing
629+ # ts_workspace fixture (single-line `import { x } from './y'`) works. But
630+ # `_parse_js_imports` still missed three common TS/JS import shapes:
631+ #
632+ # 1. Multi-line imports — `import {\n foo,\n} from './bar'`
633+ # Root cause: regex used `.*?` which doesn't cross newlines.
634+ # 2. Side-effect imports — `import './polyfills'` (no `from` clause).
635+ # Root cause: regex required `from`.
636+ # 3. Re-exports — `export { x } from './bar'`, `export * from './bar'`.
637+ # Root cause: regex only matched `import`, not `export ... from`.
638+ #
639+ # When the parser misses an import, the reverse_graph loses an edge. If the
640+ # changed file's only inbound edge was the missed import, BFS finds no
641+ # dependents and `affected_count` stays at 0 — even though real dependents
642+ # exist. This is the "BFS never starts" symptom described in issue #189.
643+
644+
645+ class TestIssue189ImportParserPatterns :
646+ """Regression tests for issue #189: `_parse_js_imports` must handle
647+ multi-line imports, side-effect imports, and re-exports — otherwise
648+ `_build_import_graph` produces an incomplete reverse_graph and
649+ `codelens affected` returns ``affected_count: 0`` even when real
650+ dependents exist.
651+ """
652+
653+ @pytest .fixture
654+ def ts_workspace_multiline (self , tmp_path ):
655+ """TS workspace where login.ts uses a multi-line import statement.
656+
657+ Mirrors the existing `ts_workspace` fixture but exercises the
658+ multi-line import shape that real-world TS code uses pervasively
659+ (Prettier default, ESLint `multi-line` rule, etc.).
660+ """
661+ ws = tmp_path / "ws_multiline"
662+ ws .mkdir ()
663+ (ws / "auth" ).mkdir ()
664+ (ws / "tests" ).mkdir ()
665+ (ws / "auth" / "google-auth-cache.ts" ).write_text (
666+ "export const cache = new Map<string, string>();\n "
667+ "export function getCachedToken(key: string): string | null {\n "
668+ " return cache.get(key) || null;\n "
669+ "}\n " ,
670+ encoding = "utf-8" ,
671+ )
672+ (ws / "auth" / "login.ts" ).write_text (
673+ "import {\n "
674+ " getCachedToken,\n "
675+ "} from './google-auth-cache';\n "
676+ "export function login(user: string): boolean {\n "
677+ " const token = getCachedToken(user);\n "
678+ " return token !== null;\n "
679+ "}\n " ,
680+ encoding = "utf-8" ,
681+ )
682+ (ws / "tests" / "login.test.ts" ).write_text (
683+ "import { login } from '../auth/login';\n "
684+ "test('login returns false for unknown user', () => {\n "
685+ " expect(login('unknown')).toBe(false);\n "
686+ "});\n " ,
687+ encoding = "utf-8" ,
688+ )
689+ return str (ws )
690+
691+ @pytest .fixture
692+ def ts_workspace_side_effect (self , tmp_path ):
693+ """TS workspace where app.ts has a side-effect import (no `from`)."""
694+ ws = tmp_path / "ws_side_effect"
695+ ws .mkdir ()
696+ (ws / "auth" ).mkdir ()
697+ (ws / "tests" ).mkdir ()
698+ (ws / "auth" / "polyfills.ts" ).write_text (
699+ "export const POLYFILL_VERSION = '1.0.0';\n " ,
700+ encoding = "utf-8" ,
701+ )
702+ (ws / "auth" / "app.ts" ).write_text (
703+ "import './polyfills';\n "
704+ "export function app(): string { return 'ok'; }\n " ,
705+ encoding = "utf-8" ,
706+ )
707+ (ws / "tests" / "app.test.ts" ).write_text (
708+ "import { app } from '../auth/app';\n "
709+ "test('app works', () => { expect(app()).toBe('ok'); });\n " ,
710+ encoding = "utf-8" ,
711+ )
712+ return str (ws )
713+
714+ @pytest .fixture
715+ def ts_workspace_reexport (self , tmp_path ):
716+ """TS workspace using `export { x } from './core'` (barrel file)."""
717+ ws = tmp_path / "ws_reexport"
718+ ws .mkdir ()
719+ (ws / "auth" ).mkdir ()
720+ (ws / "tests" ).mkdir ()
721+ (ws / "auth" / "core.ts" ).write_text (
722+ "export const TOKEN_KEY = 'cl_token';\n " ,
723+ encoding = "utf-8" ,
724+ )
725+ (ws / "auth" / "index.ts" ).write_text (
726+ "export { TOKEN_KEY } from './core';\n " ,
727+ encoding = "utf-8" ,
728+ )
729+ (ws / "tests" / "index.test.ts" ).write_text (
730+ "import { TOKEN_KEY } from '../auth/index';\n "
731+ "test('key is string', () => { expect(typeof TOKEN_KEY).toBe('string'); });\n " ,
732+ encoding = "utf-8" ,
733+ )
734+ return str (ws )
735+
736+ def test_multiline_import_resolves_and_bfs_finds_dependents (
737+ self , ts_workspace_multiline ,
738+ ):
739+ """Multi-line `import {\n foo \n } from './bar'` must be parsed.
740+
741+ Without the fix, `_parse_js_imports` skipped this import (regex
742+ `.*?` did not cross newlines), `reverse_graph` had no edge from
743+ `google-auth-cache.ts` to `login.ts`, and `affected_count` was 0.
744+ """
745+ result = get_affected_files (
746+ changed_files = ["auth/google-auth-cache.ts" ],
747+ workspace = ts_workspace_multiline ,
748+ depth = 5 ,
749+ )
750+ assert result ["status" ] == "ok"
751+ assert result ["unresolved" ] == [], (
752+ "changed file must resolve; unresolved means graph keys are "
753+ "incomplete (the BFS-never-starts symptom from issue #189)"
754+ )
755+ assert result ["stats" ]["affected_count" ] >= 1 , (
756+ "BFS must find the transitive test dependent via the multi-line "
757+ "import in login.ts"
758+ )
759+ assert "tests/login.test.ts" in result ["affected" ]
760+
761+ def test_side_effect_import_resolves_and_bfs_finds_dependents (
762+ self , ts_workspace_side_effect ,
763+ ):
764+ """`import './polyfills'` (no `from`) must be parsed as an edge."""
765+ result = get_affected_files (
766+ changed_files = ["auth/polyfills.ts" ],
767+ workspace = ts_workspace_side_effect ,
768+ depth = 5 ,
769+ )
770+ assert result ["status" ] == "ok"
771+ assert result ["unresolved" ] == []
772+ assert result ["stats" ]["affected_count" ] >= 1 , (
773+ "side-effect imports create a real dependency edge — BFS must "
774+ "find the transitive test dependent"
775+ )
776+ assert "tests/app.test.ts" in result ["affected" ]
777+
778+ def test_reexport_creates_dependency_edge (
779+ self , ts_workspace_reexport ,
780+ ):
781+ """`export { x } from './core'` must add core.ts → index.ts edge."""
782+ result = get_affected_files (
783+ changed_files = ["auth/core.ts" ],
784+ workspace = ts_workspace_reexport ,
785+ depth = 5 ,
786+ )
787+ assert result ["status" ] == "ok"
788+ assert result ["unresolved" ] == []
789+ assert result ["stats" ]["affected_count" ] >= 1 , (
790+ "re-exports create a real dependency edge — BFS must find the "
791+ "transitive test dependent via the barrel file"
792+ )
793+ assert "tests/index.test.ts" in result ["affected" ]
794+
795+ def test_existing_ts_workspace_fixture_still_finds_affected (self , ts_workspace ):
796+ """DoD sanity check: the existing fixture from PR #184 must still
797+ return ``affected_count >= 1`` after the parser changes.
798+
799+ This is the explicit Definition-of-Done assertion from issue #189:
800+ "Use existing ts_workspace fixture in tests/test_affected_command.py
801+ (from PR #184) and assert affected_count >= 1".
802+ """
803+ result = get_affected_files (
804+ changed_files = ["auth/google-auth-cache.ts" ],
805+ workspace = ts_workspace ,
806+ depth = 5 ,
807+ )
808+ assert result ["status" ] == "ok"
809+ assert result ["unresolved" ] == []
810+ assert result ["stats" ]["affected_count" ] >= 1
811+ assert "tests/login.test.ts" in result ["affected" ]
812+
813+ def test_multiline_include_source_returns_all_dependents (
814+ self , ts_workspace_multiline ,
815+ ):
816+ """`--include-source` must also surface non-test dependents reached
817+ via multi-line imports (cross-check against the existing
818+ `test_ts_include_source_returns_all_dependents`).
819+ """
820+ result = get_affected_files (
821+ changed_files = ["auth/google-auth-cache.ts" ],
822+ workspace = ts_workspace_multiline ,
823+ depth = 5 ,
824+ include_source = True ,
825+ )
826+ affected = set (result ["affected" ])
827+ assert "auth/login.ts" in affected
828+ assert "tests/login.test.ts" in affected
0 commit comments