@@ -11,6 +11,8 @@ import {
1111 findLongestPrefixMatch ,
1212 getCommandDecision ,
1313 getSingleCommandDecision ,
14+ CommandValidator ,
15+ createCommandValidator ,
1416} from "../command-validation"
1517
1618describe ( "Command Validation" , ( ) => {
@@ -759,4 +761,313 @@ describe("Unified Command Decision Functions", () => {
759761 } )
760762 } )
761763 } )
764+
765+ describe ( "CommandValidator Integration Tests" , ( ) => {
766+ describe ( "CommandValidator Class" , ( ) => {
767+ let validator : CommandValidator
768+
769+ beforeEach ( ( ) => {
770+ validator = new CommandValidator ( [ "npm" , "echo" , "git" ] , [ "npm test" , "git push" ] )
771+ } )
772+
773+ describe ( "Basic validation methods" , ( ) => {
774+ it ( "validates commands correctly" , ( ) => {
775+ expect ( validator . validateCommand ( "npm install" ) ) . toBe ( "auto_approve" )
776+ expect ( validator . validateCommand ( "npm test" ) ) . toBe ( "auto_deny" )
777+ expect ( validator . validateCommand ( "dangerous" ) ) . toBe ( "ask_user" )
778+ } )
779+
780+ it ( "provides convenience methods" , ( ) => {
781+ expect ( validator . isAutoApproved ( "npm install" ) ) . toBe ( true )
782+ expect ( validator . isAutoApproved ( "npm test" ) ) . toBe ( false )
783+ expect ( validator . isAutoApproved ( "dangerous" ) ) . toBe ( false )
784+
785+ expect ( validator . isAutoDenied ( "npm install" ) ) . toBe ( false )
786+ expect ( validator . isAutoDenied ( "npm test" ) ) . toBe ( true )
787+ expect ( validator . isAutoDenied ( "dangerous" ) ) . toBe ( false )
788+
789+ expect ( validator . requiresUserInput ( "npm install" ) ) . toBe ( false )
790+ expect ( validator . requiresUserInput ( "npm test" ) ) . toBe ( false )
791+ expect ( validator . requiresUserInput ( "dangerous" ) ) . toBe ( true )
792+ } )
793+ } )
794+
795+ describe ( "Configuration management" , ( ) => {
796+ it ( "updates command lists" , ( ) => {
797+ validator . updateCommandLists ( [ "echo" ] , [ "echo hello" ] )
798+
799+ expect ( validator . validateCommand ( "npm install" ) ) . toBe ( "ask_user" )
800+ expect ( validator . validateCommand ( "echo world" ) ) . toBe ( "auto_approve" )
801+ expect ( validator . validateCommand ( "echo hello" ) ) . toBe ( "auto_deny" )
802+ } )
803+
804+ it ( "gets current command lists" , ( ) => {
805+ const lists = validator . getCommandLists ( )
806+ expect ( lists . allowedCommands ) . toEqual ( [ "npm" , "echo" , "git" ] )
807+ expect ( lists . deniedCommands ) . toEqual ( [ "npm test" , "git push" ] )
808+ } )
809+
810+ it ( "handles undefined denied commands" , ( ) => {
811+ const validatorNoDeny = new CommandValidator ( [ "npm" ] )
812+ const lists = validatorNoDeny . getCommandLists ( )
813+ expect ( lists . allowedCommands ) . toEqual ( [ "npm" ] )
814+ expect ( lists . deniedCommands ) . toBeUndefined ( )
815+ } )
816+ } )
817+
818+ describe ( "Detailed validation information" , ( ) => {
819+ it ( "provides comprehensive validation details" , ( ) => {
820+ const details = validator . getValidationDetails ( "npm install && echo done" )
821+
822+ expect ( details . decision ) . toBe ( "auto_approve" )
823+ expect ( details . subCommands ) . toEqual ( [ "npm install" , "echo done" ] )
824+ expect ( details . hasSubshells ) . toBe ( false )
825+ expect ( details . allowedMatches ) . toHaveLength ( 2 )
826+ expect ( details . deniedMatches ) . toHaveLength ( 2 )
827+
828+ // Check specific matches
829+ expect ( details . allowedMatches [ 0 ] ) . toEqual ( { command : "npm install" , match : "npm" } )
830+ expect ( details . allowedMatches [ 1 ] ) . toEqual ( { command : "echo done" , match : "echo" } )
831+ expect ( details . deniedMatches [ 0 ] ) . toEqual ( { command : "npm install" , match : null } )
832+ expect ( details . deniedMatches [ 1 ] ) . toEqual ( { command : "echo done" , match : null } )
833+ } )
834+
835+ it ( "detects subshells correctly" , ( ) => {
836+ const details = validator . getValidationDetails ( "npm install $(echo test)" )
837+ expect ( details . hasSubshells ) . toBe ( true )
838+ expect ( details . decision ) . toBe ( "auto_deny" ) // blocked due to subshells with denylist
839+ } )
840+
841+ it ( "handles complex command chains" , ( ) => {
842+ const details = validator . getValidationDetails ( "npm test && git push origin" )
843+
844+ expect ( details . decision ) . toBe ( "auto_deny" )
845+ expect ( details . subCommands ) . toEqual ( [ "npm test" , "git push origin" ] )
846+ expect ( details . deniedMatches [ 0 ] ) . toEqual ( { command : "npm test" , match : "npm test" } )
847+ expect ( details . deniedMatches [ 1 ] ) . toEqual ( { command : "git push origin" , match : "git push" } )
848+ } )
849+ } )
850+
851+ describe ( "Batch validation" , ( ) => {
852+ it ( "validates multiple commands at once" , ( ) => {
853+ const commands = [ "npm install" , "npm test" , "dangerous" , "echo hello" ]
854+ const results = validator . validateCommands ( commands )
855+
856+ expect ( results . get ( "npm install" ) ) . toBe ( "auto_approve" )
857+ expect ( results . get ( "npm test" ) ) . toBe ( "auto_deny" )
858+ expect ( results . get ( "dangerous" ) ) . toBe ( "ask_user" )
859+ expect ( results . get ( "echo hello" ) ) . toBe ( "auto_approve" )
860+ expect ( results . size ) . toBe ( 4 )
861+ } )
862+
863+ it ( "handles empty command list" , ( ) => {
864+ const results = validator . validateCommands ( [ ] )
865+ expect ( results . size ) . toBe ( 0 )
866+ } )
867+ } )
868+
869+ describe ( "Configuration analysis" , ( ) => {
870+ it ( "detects if rules are configured" , ( ) => {
871+ expect ( validator . hasRules ( ) ) . toBe ( true )
872+
873+ const emptyValidator = new CommandValidator ( [ ] , [ ] )
874+ expect ( emptyValidator . hasRules ( ) ) . toBe ( false )
875+
876+ const allowOnlyValidator = new CommandValidator ( [ "npm" ] , [ ] )
877+ expect ( allowOnlyValidator . hasRules ( ) ) . toBe ( true )
878+
879+ const denyOnlyValidator = new CommandValidator ( [ ] , [ "rm" ] )
880+ expect ( denyOnlyValidator . hasRules ( ) ) . toBe ( true )
881+ } )
882+
883+ it ( "provides configuration statistics" , ( ) => {
884+ const stats = validator . getStats ( )
885+ expect ( stats . allowedCount ) . toBe ( 3 )
886+ expect ( stats . deniedCount ) . toBe ( 2 )
887+ expect ( stats . hasWildcard ) . toBe ( false )
888+ expect ( stats . hasRules ) . toBe ( true )
889+ } )
890+
891+ it ( "detects wildcard configuration" , ( ) => {
892+ const wildcardValidator = new CommandValidator ( [ "*" , "npm" ] , [ "rm" ] )
893+ const stats = wildcardValidator . getStats ( )
894+ expect ( stats . hasWildcard ) . toBe ( true )
895+ } )
896+ } )
897+
898+ describe ( "Edge cases and error handling" , ( ) => {
899+ it ( "handles empty commands gracefully" , ( ) => {
900+ expect ( validator . validateCommand ( "" ) ) . toBe ( "auto_approve" )
901+ expect ( validator . validateCommand ( " " ) ) . toBe ( "auto_approve" )
902+ } )
903+
904+ it ( "handles commands with only whitespace" , ( ) => {
905+ const details = validator . getValidationDetails ( " " )
906+ expect ( details . decision ) . toBe ( "auto_approve" )
907+ expect ( details . subCommands ) . toEqual ( [ ] )
908+ } )
909+
910+ it ( "handles malformed commands" , ( ) => {
911+ // Commands with unmatched quotes or brackets should not crash
912+ expect ( ( ) => validator . validateCommand ( 'npm test "unclosed quote' ) ) . not . toThrow ( )
913+ expect ( ( ) => validator . validateCommand ( "npm test $(unclosed" ) ) . not . toThrow ( )
914+ } )
915+ } )
916+ } )
917+
918+ describe ( "Factory function" , ( ) => {
919+ it ( "creates validator instances correctly" , ( ) => {
920+ const validator = createCommandValidator ( [ "npm" ] , [ "rm" ] )
921+ expect ( validator ) . toBeInstanceOf ( CommandValidator )
922+ expect ( validator . validateCommand ( "npm test" ) ) . toBe ( "auto_approve" )
923+ expect ( validator . validateCommand ( "rm file" ) ) . toBe ( "auto_deny" )
924+ } )
925+
926+ it ( "handles optional denied commands" , ( ) => {
927+ const validator = createCommandValidator ( [ "npm" ] )
928+ expect ( validator . validateCommand ( "npm test" ) ) . toBe ( "auto_approve" )
929+ expect ( validator . validateCommand ( "dangerous" ) ) . toBe ( "ask_user" )
930+ } )
931+ } )
932+
933+ describe ( "Real-world integration scenarios" , ( ) => {
934+ describe ( "Development workflow validation" , ( ) => {
935+ let devValidator : CommandValidator
936+
937+ beforeEach ( ( ) => {
938+ devValidator = createCommandValidator (
939+ [ "npm" , "git" , "echo" , "ls" , "cat" ] ,
940+ [ "git push" , "rm" , "sudo" , "npm publish" ] ,
941+ )
942+ } )
943+
944+ it ( "allows common development commands" , ( ) => {
945+ const commonCommands = [
946+ "npm install" ,
947+ "npm test" ,
948+ "npm run build" ,
949+ "git status" ,
950+ "git add ." ,
951+ "git commit -m 'fix'" ,
952+ "echo 'done'" ,
953+ "ls -la" ,
954+ "cat package.json" ,
955+ ]
956+
957+ commonCommands . forEach ( ( cmd ) => {
958+ expect ( devValidator . isAutoApproved ( cmd ) ) . toBe ( true )
959+ } )
960+ } )
961+
962+ it ( "blocks dangerous commands" , ( ) => {
963+ const dangerousCommands = [
964+ "git push origin main" ,
965+ "rm -rf node_modules" ,
966+ "sudo apt install" ,
967+ "npm publish" ,
968+ ]
969+
970+ dangerousCommands . forEach ( ( cmd ) => {
971+ expect ( devValidator . isAutoDenied ( cmd ) ) . toBe ( true )
972+ } )
973+ } )
974+
975+ it ( "requires user input for unknown commands" , ( ) => {
976+ const unknownCommands = [ "docker run" , "python script.py" , "curl https://api.example.com" ]
977+
978+ unknownCommands . forEach ( ( cmd ) => {
979+ expect ( devValidator . requiresUserInput ( cmd ) ) . toBe ( true )
980+ } )
981+ } )
982+ } )
983+
984+ describe ( "Production environment validation" , ( ) => {
985+ let prodValidator : CommandValidator
986+
987+ beforeEach ( ( ) => {
988+ prodValidator = createCommandValidator (
989+ [ "ls" , "cat" , "grep" , "tail" ] ,
990+ [ "*" ] , // Deny everything by default
991+ )
992+ } )
993+
994+ it ( "allows only read-only commands" , ( ) => {
995+ expect ( prodValidator . isAutoApproved ( "ls -la" ) ) . toBe ( true )
996+ expect ( prodValidator . isAutoApproved ( "cat /var/log/app.log" ) ) . toBe ( true )
997+ expect ( prodValidator . isAutoApproved ( "grep ERROR /var/log/app.log" ) ) . toBe ( true )
998+ expect ( prodValidator . isAutoApproved ( "tail -f /var/log/app.log" ) ) . toBe ( true )
999+ } )
1000+
1001+ it ( "blocks all other commands due to wildcard deny" , ( ) => {
1002+ const blockedCommands = [ "npm install" , "git push" , "rm file" , "echo hello" , "mkdir test" ]
1003+
1004+ blockedCommands . forEach ( ( cmd ) => {
1005+ expect ( prodValidator . isAutoDenied ( cmd ) ) . toBe ( true )
1006+ } )
1007+ } )
1008+ } )
1009+
1010+ describe ( "Longest prefix match in complex scenarios" , ( ) => {
1011+ let complexValidator : CommandValidator
1012+
1013+ beforeEach ( ( ) => {
1014+ complexValidator = createCommandValidator (
1015+ [ "git" , "git push" , "git push --dry-run" , "npm" , "npm test" ] ,
1016+ [ "git push" , "npm test --coverage" ] ,
1017+ )
1018+ } )
1019+
1020+ it ( "demonstrates longest prefix match resolution" , ( ) => {
1021+ // git push --dry-run (allowed, 18 chars) vs git push (denied, 8 chars) -> allow
1022+ expect ( complexValidator . isAutoApproved ( "git push --dry-run origin main" ) ) . toBe ( true )
1023+
1024+ // git push origin (denied, 8 chars) vs git (allowed, 3 chars) -> deny
1025+ expect ( complexValidator . isAutoDenied ( "git push origin main" ) ) . toBe ( true )
1026+
1027+ // npm test (allowed, 8 chars) vs npm test --coverage (denied, 19 chars) -> deny
1028+ expect ( complexValidator . isAutoDenied ( "npm test --coverage --watch" ) ) . toBe ( true )
1029+
1030+ // npm test basic (allowed, 8 chars) vs no deny match -> allow
1031+ expect ( complexValidator . isAutoApproved ( "npm test basic" ) ) . toBe ( true )
1032+ } )
1033+
1034+ it ( "handles command chains with mixed decisions" , ( ) => {
1035+ // One command denied -> whole chain denied
1036+ expect ( complexValidator . isAutoDenied ( "git status && git push origin" ) ) . toBe ( true )
1037+
1038+ // All commands approved -> whole chain approved
1039+ expect ( complexValidator . isAutoApproved ( "git status && git push --dry-run" ) ) . toBe ( true )
1040+
1041+ // Mixed with unknown -> ask user
1042+ expect ( complexValidator . requiresUserInput ( "git status && unknown-command" ) ) . toBe ( true )
1043+ } )
1044+ } )
1045+
1046+ describe ( "Performance and scalability" , ( ) => {
1047+ it ( "handles large command lists efficiently" , ( ) => {
1048+ const largeAllowList = Array . from ( { length : 1000 } , ( _ , i ) => `command${ i } ` )
1049+ const largeDenyList = Array . from ( { length : 500 } , ( _ , i ) => `dangerous${ i } ` )
1050+
1051+ const largeValidator = createCommandValidator ( largeAllowList , largeDenyList )
1052+
1053+ // Should still work efficiently
1054+ expect ( largeValidator . isAutoApproved ( "command500 --flag" ) ) . toBe ( true )
1055+ expect ( largeValidator . isAutoDenied ( "dangerous250 --flag" ) ) . toBe ( true )
1056+ expect ( largeValidator . requiresUserInput ( "unknown" ) ) . toBe ( true )
1057+ } )
1058+
1059+ it ( "handles batch validation efficiently" , ( ) => {
1060+ const batchValidator = createCommandValidator ( [ "npm" ] , [ "rm" ] )
1061+ const commands = Array . from ( { length : 100 } , ( _ , i ) => `npm test${ i } ` )
1062+ const results = batchValidator . validateCommands ( commands )
1063+
1064+ expect ( results . size ) . toBe ( 100 )
1065+ // All should be auto-approved since they match "npm" allowlist
1066+ Array . from ( results . values ( ) ) . forEach ( ( decision ) => {
1067+ expect ( decision ) . toBe ( "auto_approve" )
1068+ } )
1069+ } )
1070+ } )
1071+ } )
1072+ } )
7621073} )
0 commit comments