@@ -659,3 +659,168 @@ fn test_index_multi_file_cross_queries() {
659659 "Should have tasks from at least 4 different files"
660660 ) ;
661661}
662+
663+ #[ test]
664+ fn test_file_level_label_filtering ( ) {
665+ // Test scenario: Verify file-level labels are indexed and filterable
666+ // This test explicitly verifies that @labels from file metadata are:
667+ // 1. Indexed into the labels and file_labels tables
668+ // 2. Queryable via TaskFilter
669+ // 3. Properly associated with all tasks in the file
670+ let project = create_multi_file_project ( ) ;
671+ let root = project. path ( ) . to_path_buf ( ) ;
672+ let db = TestDatabase :: file_based ( ) ;
673+ let conn = db. connection ( ) ;
674+
675+ // Index the project
676+ let config = IndexerConfig :: new ( root. clone ( ) )
677+ . with_incremental ( false )
678+ . with_progress ( false ) ;
679+ let parser_config = LashConfig :: default ( ) ;
680+ let mut indexer = Indexer :: new ( & conn, config, & parser_config) ;
681+ indexer. index_project ( ) . expect ( "Index should succeed" ) ;
682+
683+ // Verify file-level labels are in the labels table
684+ let inspector = DbInspector :: new ( & conn) ;
685+ let labels = inspector. get_labels ( ) ;
686+ assert ! (
687+ !labels. is_empty( ) ,
688+ "File-level labels should be indexed into labels table"
689+ ) ;
690+
691+ // Verify specific labels exist
692+ assert ! (
693+ labels. contains( & "backend" . to_string( ) ) ,
694+ "Should have 'backend' label from auth.md and database.md"
695+ ) ;
696+ assert ! (
697+ labels. contains( & "frontend" . to_string( ) ) ,
698+ "Should have 'frontend' label from ui.md"
699+ ) ;
700+ assert ! (
701+ labels. contains( & "security" . to_string( ) ) ,
702+ "Should have 'security' label from auth.md"
703+ ) ;
704+
705+ // Test 1: Filter by file-level label "backend"
706+ let task_repo = TaskRepository :: new ( & conn) ;
707+ let backend_tasks = task_repo
708+ . find ( & TaskFilter {
709+ labels : vec ! [ "backend" . to_string( ) ] ,
710+ ..Default :: default ( )
711+ } )
712+ . expect ( "Should find tasks by file-level label" ) ;
713+
714+ // Should get ALL tasks from auth.md (5 tasks) and database.md (3 tasks) = 8 tasks
715+ assert ! (
716+ backend_tasks. len( ) >= 5 ,
717+ "Should have at least 5 tasks from backend-labeled files, got {}" ,
718+ backend_tasks. len( )
719+ ) ;
720+
721+ // Verify all tasks are from backend-labeled files
722+ let file_repo = FileRepository :: new ( & conn) ;
723+ for task in & backend_tasks {
724+ let file = file_repo
725+ . get_by_db_id ( task. file_id )
726+ . expect ( "Should get file" )
727+ . expect ( "File should exist" ) ;
728+ let path = file. path . to_string_lossy ( ) ;
729+ assert ! (
730+ path. contains( "auth.md" ) || path. contains( "database.md" ) ,
731+ "Backend task should be from auth.md or database.md, got: {path}"
732+ ) ;
733+ }
734+
735+ // Test 2: Filter by file-level label "frontend"
736+ let frontend_tasks = task_repo
737+ . find ( & TaskFilter {
738+ labels : vec ! [ "frontend" . to_string( ) ] ,
739+ ..Default :: default ( )
740+ } )
741+ . expect ( "Should find tasks by file-level label" ) ;
742+
743+ // Should get ALL tasks from ui.md (3 tasks)
744+ assert_eq ! (
745+ frontend_tasks. len( ) ,
746+ 3 ,
747+ "Should have exactly 3 tasks from ui.md"
748+ ) ;
749+
750+ // Verify all tasks are from ui.md
751+ for task in & frontend_tasks {
752+ let file = file_repo
753+ . get_by_db_id ( task. file_id )
754+ . expect ( "Should get file" )
755+ . expect ( "File should exist" ) ;
756+ assert ! (
757+ file. path. to_string_lossy( ) . contains( "ui.md" ) ,
758+ "Frontend task should be from ui.md"
759+ ) ;
760+ }
761+
762+ // Test 3: Filter by file-level label "bug"
763+ let bug_tasks = task_repo
764+ . find ( & TaskFilter {
765+ labels : vec ! [ "bug" . to_string( ) ] ,
766+ ..Default :: default ( )
767+ } )
768+ . expect ( "Should find tasks by file-level label" ) ;
769+
770+ // Should get ALL tasks from bugs.md (3 tasks)
771+ assert_eq ! (
772+ bug_tasks. len( ) ,
773+ 3 ,
774+ "Should have exactly 3 tasks from bugs.md"
775+ ) ;
776+
777+ // Test 4: Filter by file-level label "security" (only on auth.md)
778+ let security_tasks = task_repo
779+ . find ( & TaskFilter {
780+ labels : vec ! [ "security" . to_string( ) ] ,
781+ ..Default :: default ( )
782+ } )
783+ . expect ( "Should find tasks by file-level label" ) ;
784+
785+ // Should get ALL tasks from auth.md (5 tasks)
786+ assert_eq ! (
787+ security_tasks. len( ) ,
788+ 5 ,
789+ "Should have exactly 5 tasks from auth.md"
790+ ) ;
791+
792+ // Test 5: Combine file-level label with status filter
793+ let open_backend_tasks = task_repo
794+ . find ( & TaskFilter {
795+ labels : vec ! [ "backend" . to_string( ) ] ,
796+ status : Some ( TaskStatus :: Open ) ,
797+ ..Default :: default ( )
798+ } )
799+ . expect ( "Should find tasks with combined filters" ) ;
800+
801+ // Verify all results match both criteria
802+ for task in & open_backend_tasks {
803+ assert_eq ! ( task. status, TaskStatus :: Open , "Should only be open tasks" ) ;
804+ let file = file_repo
805+ . get_by_db_id ( task. file_id )
806+ . expect ( "Should get file" )
807+ . expect ( "File should exist" ) ;
808+ let path = file. path . to_string_lossy ( ) ;
809+ assert ! (
810+ path. contains( "auth.md" ) || path. contains( "database.md" ) ,
811+ "Should be from backend-labeled file"
812+ ) ;
813+ }
814+
815+ // Test 6: Verify no cross-contamination between different file-level labels
816+ let backend_ids: std:: collections:: HashSet < _ > =
817+ backend_tasks. iter ( ) . map ( |t| & t. full_id ) . collect ( ) ;
818+ let frontend_ids: std:: collections:: HashSet < _ > =
819+ frontend_tasks. iter ( ) . map ( |t| & t. full_id ) . collect ( ) ;
820+
821+ let overlap: Vec < _ > = backend_ids. intersection ( & frontend_ids) . collect ( ) ;
822+ assert ! (
823+ overlap. is_empty( ) ,
824+ "Backend and frontend tasks should not overlap (different files), found: {overlap:?}"
825+ ) ;
826+ }
0 commit comments