@@ -525,3 +525,293 @@ def test_new_hooks_version_gated(server: Server, test_case: HookTestCase) -> Non
525525
526526 # Cleanup
527527 session .unset_hook (f"{ test_case .hook } [0]" )
528+
529+
530+ # =============================================================================
531+ # Bulk Operations API Tests
532+ # =============================================================================
533+
534+
535+ class BulkOpTestCase (t .NamedTuple ):
536+ """Test case for bulk hook operations."""
537+
538+ test_id : str
539+ operation : str # "get_indices", "get_values", "set_bulk", "clear", "append"
540+ hook : str # Hook name to test
541+ setup_hooks : dict [int , str ] # Initial hooks to set (index -> value)
542+ operation_args : dict [str , t .Any ] # Args for operation
543+ expected_indices : list [int ] # Expected indices after operation
544+ expected_contains : list [str ] | None = None # Strings expected in values
545+
546+
547+ # --- get_hook_indices tests ---
548+ GET_INDICES_TESTS : list [BulkOpTestCase ] = [
549+ BulkOpTestCase (
550+ "get_indices_empty" ,
551+ "get_indices" ,
552+ "session-renamed" ,
553+ {},
554+ {},
555+ [],
556+ ),
557+ BulkOpTestCase (
558+ "get_indices_sequential" ,
559+ "get_indices" ,
560+ "session-renamed" ,
561+ {
562+ 0 : "display-message 'hook 0'" ,
563+ 1 : "display-message 'hook 1'" ,
564+ 2 : "display-message 'hook 2'" ,
565+ },
566+ {},
567+ [0 , 1 , 2 ],
568+ ),
569+ BulkOpTestCase (
570+ "get_indices_sparse" ,
571+ "get_indices" ,
572+ "session-renamed" ,
573+ {
574+ 0 : "display-message 'hook 0'" ,
575+ 5 : "display-message 'hook 5'" ,
576+ 10 : "display-message 'hook 10'" ,
577+ },
578+ {},
579+ [0 , 5 , 10 ],
580+ ),
581+ ]
582+
583+ # --- get_hook_values tests ---
584+ GET_VALUES_TESTS : list [BulkOpTestCase ] = [
585+ BulkOpTestCase (
586+ "get_values_empty" ,
587+ "get_values" ,
588+ "session-renamed" ,
589+ {},
590+ {},
591+ [],
592+ ),
593+ BulkOpTestCase (
594+ "get_values_sparse" ,
595+ "get_values" ,
596+ "session-renamed" ,
597+ {0 : "display-message 'hook 0'" , 5 : "display-message 'hook 5'" },
598+ {},
599+ [0 , 5 ],
600+ ["display-message" ],
601+ ),
602+ ]
603+
604+ # --- set_hooks_bulk tests ---
605+ SET_BULK_TESTS : list [BulkOpTestCase ] = [
606+ BulkOpTestCase (
607+ "set_bulk_with_dict" ,
608+ "set_bulk" ,
609+ "session-renamed" ,
610+ {},
611+ {
612+ "values" : {
613+ 0 : "display-message 'hook 0'" ,
614+ 1 : "display-message 'hook 1'" ,
615+ 5 : "display-message 'hook 5'" ,
616+ },
617+ },
618+ [0 , 1 , 5 ],
619+ ["hook 0" , "hook 1" , "hook 5" ],
620+ ),
621+ BulkOpTestCase (
622+ "set_bulk_with_list" ,
623+ "set_bulk" ,
624+ "session-renamed" ,
625+ {},
626+ {
627+ "values" : [
628+ "display-message 'hook 0'" ,
629+ "display-message 'hook 1'" ,
630+ "display-message 'hook 2'" ,
631+ ],
632+ },
633+ [0 , 1 , 2 ],
634+ ),
635+ BulkOpTestCase (
636+ "set_bulk_clear_existing" ,
637+ "set_bulk" ,
638+ "session-renamed" ,
639+ {0 : "display-message 'old 0'" , 1 : "display-message 'old 1'" },
640+ {"values" : {0 : "display-message 'new 0'" }, "clear_existing" : True },
641+ [0 ],
642+ ["new 0" ],
643+ ),
644+ ]
645+
646+ # --- clear_hook tests ---
647+ CLEAR_TESTS : list [BulkOpTestCase ] = [
648+ BulkOpTestCase (
649+ "clear_hook" ,
650+ "clear" ,
651+ "session-renamed" ,
652+ {0 : "display-message 'hook 0'" , 5 : "display-message 'hook 5'" },
653+ {},
654+ [],
655+ ),
656+ ]
657+
658+ # --- append_hook tests ---
659+ APPEND_TESTS : list [BulkOpTestCase ] = [
660+ BulkOpTestCase (
661+ "append_to_empty" ,
662+ "append" ,
663+ "session-renamed" ,
664+ {},
665+ {"value" : "display-message 'appended'" },
666+ [0 ],
667+ ["appended" ],
668+ ),
669+ BulkOpTestCase (
670+ "append_sequential" ,
671+ "append" ,
672+ "session-renamed" ,
673+ {0 : "display-message 'initial'" },
674+ {"value" : "display-message 'appended'" },
675+ [0 , 1 ],
676+ ),
677+ BulkOpTestCase (
678+ "append_after_sparse" ,
679+ "append" ,
680+ "session-renamed" ,
681+ {0 : "display-message 'at 0'" , 10 : "display-message 'at 10'" },
682+ {"value" : "display-message 'appended'" },
683+ [0 , 10 , 11 ],
684+ ["appended" ],
685+ ),
686+ ]
687+
688+ # Combine all bulk operation test cases
689+ ALL_BULK_OP_TESTS : list [BulkOpTestCase ] = (
690+ GET_INDICES_TESTS + GET_VALUES_TESTS + SET_BULK_TESTS + CLEAR_TESTS + APPEND_TESTS
691+ )
692+
693+
694+ def _build_bulk_op_params () -> list [t .Any ]:
695+ """Build pytest params for bulk operation tests."""
696+ return [pytest .param (tc , id = tc .test_id ) for tc in ALL_BULK_OP_TESTS ]
697+
698+
699+ @pytest .mark .parametrize ("test_case" , _build_bulk_op_params ())
700+ def test_bulk_hook_operation (server : Server , test_case : BulkOpTestCase ) -> None :
701+ """Test bulk hook operations.
702+
703+ This parametrized test ensures all bulk operations work correctly:
704+ - get_hook_indices: returns sorted list of existing indices
705+ - get_hook_values: returns SparseArray with values
706+ - set_hooks_bulk: sets multiple hooks at once
707+ - clear_hook: removes all indexed values
708+ - append_hook: appends at next available index
709+ """
710+ session = server .new_session (session_name = "test_bulk_ops" )
711+
712+ # Setup initial hooks
713+ for idx , val in test_case .setup_hooks .items ():
714+ session .set_hook (f"{ test_case .hook } [{ idx } ]" , val )
715+
716+ # Perform operation based on type
717+ if test_case .operation == "get_indices" :
718+ result = session .get_hook_indices (test_case .hook )
719+ assert result == test_case .expected_indices
720+
721+ elif test_case .operation == "get_values" :
722+ values = session .get_hook_values (test_case .hook )
723+ assert isinstance (values , SparseArray )
724+ assert sorted (values .keys ()) == test_case .expected_indices
725+ if test_case .expected_contains :
726+ for expected_str in test_case .expected_contains :
727+ assert any (expected_str in v for v in values .values ())
728+
729+ elif test_case .operation == "set_bulk" :
730+ session .set_hooks_bulk (test_case .hook , ** test_case .operation_args )
731+ indices = session .get_hook_indices (test_case .hook )
732+ assert indices == test_case .expected_indices
733+ if test_case .expected_contains :
734+ values = session .get_hook_values (test_case .hook )
735+ for expected_str in test_case .expected_contains :
736+ assert any (expected_str in v for v in values .values ())
737+
738+ elif test_case .operation == "clear" :
739+ session .clear_hook (test_case .hook )
740+ indices = session .get_hook_indices (test_case .hook )
741+ assert indices == test_case .expected_indices
742+
743+ elif test_case .operation == "append" :
744+ session .append_hook (test_case .hook , test_case .operation_args ["value" ])
745+ indices = session .get_hook_indices (test_case .hook )
746+ assert indices == test_case .expected_indices
747+ if test_case .expected_contains :
748+ values = session .get_hook_values (test_case .hook )
749+ for expected_str in test_case .expected_contains :
750+ assert any (expected_str in v for v in values .values ())
751+
752+ # Cleanup
753+ session .clear_hook (test_case .hook )
754+
755+
756+ def test_bulk_hook_values_iteration (server : Server ) -> None :
757+ """Test iterating over hook values in sorted order."""
758+ session = server .new_session (session_name = "test_bulk_ops" )
759+
760+ # Set hooks at sparse indices (out of order)
761+ session .set_hook ("session-renamed[5]" , "display-message 'fifth'" )
762+ session .set_hook ("session-renamed[0]" , "display-message 'zeroth'" )
763+ session .set_hook ("session-renamed[2]" , "display-message 'second'" )
764+
765+ values = session .get_hook_values ("session-renamed" )
766+ value_list = list (values .iter_values ())
767+
768+ # Values should be in sorted index order
769+ assert len (value_list ) == 3
770+ assert "zeroth" in value_list [0 ]
771+ assert "second" in value_list [1 ]
772+ assert "fifth" in value_list [2 ]
773+
774+ # Cleanup
775+ session .clear_hook ("session-renamed" )
776+
777+
778+ def test_bulk_hook_set_with_sparse_array (server : Server ) -> None :
779+ """Test set_hooks_bulk with SparseArray input."""
780+ session = server .new_session (session_name = "test_bulk_ops" )
781+
782+ sparse : SparseArray [str ] = SparseArray ()
783+ sparse .add (0 , "display-message 'from sparse 0'" )
784+ sparse .add (10 , "display-message 'from sparse 10'" )
785+
786+ session .set_hooks_bulk ("session-renamed" , sparse )
787+
788+ indices = session .get_hook_indices ("session-renamed" )
789+ assert indices == [0 , 10 ]
790+
791+ # Cleanup
792+ session .clear_hook ("session-renamed" )
793+
794+
795+ def test_bulk_hook_method_chaining (server : Server ) -> None :
796+ """Test that bulk operations support method chaining."""
797+ session = server .new_session (session_name = "test_bulk_ops" )
798+
799+ # Chain operations
800+ result = (
801+ session .set_hooks_bulk (
802+ "session-renamed" ,
803+ ["display-message 'hook 0'" ],
804+ )
805+ .append_hook ("session-renamed" , "display-message 'hook 1'" )
806+ .append_hook ("session-renamed" , "display-message 'hook 2'" )
807+ )
808+
809+ # Should return the session
810+ assert result is session
811+
812+ # Verify all hooks set
813+ indices = session .get_hook_indices ("session-renamed" )
814+ assert indices == [0 , 1 , 2 ]
815+
816+ # Cleanup
817+ session .clear_hook ("session-renamed" )
0 commit comments