@@ -738,3 +738,270 @@ def test_top_level_help_lists_agents():
738738 result = runner .invoke (main , ["--help" ])
739739 assert result .exit_code == 0
740740 assert "agents" in result .output
741+
742+
743+ # --- payload_override enforcement flags (hosted PR #590) ---
744+
745+
746+ def test_create_require_payload_override_in_help ():
747+ result = runner .invoke (main , ["create" , "--help" ])
748+ assert result .exit_code == 0
749+ assert "--require-payload-override" in result .output
750+ assert "--no-require-payload-override" in result .output
751+
752+
753+ def test_create_required_keys_in_help ():
754+ result = runner .invoke (main , ["create" , "--help" ])
755+ assert result .exit_code == 0
756+ assert "--required-keys" in result .output
757+
758+
759+ def test_update_require_payload_override_in_help ():
760+ result = runner .invoke (main , ["update" , "--help" ])
761+ assert result .exit_code == 0
762+ assert "--require-payload-override" in result .output
763+ assert "--no-require-payload-override" in result .output
764+
765+
766+ def test_update_required_keys_in_help ():
767+ result = runner .invoke (main , ["update" , "--help" ])
768+ assert result .exit_code == 0
769+ assert "--required-keys" in result .output
770+
771+
772+ # --- body-construction unit tests for new flags ---
773+ #
774+ # These mock the HTTP layer (CueAPIClient.post / .patch) and assert that the
775+ # CLI body matches the hosted-API spec. Cheap insurance against flag-wiring
776+ # regressions when refactoring create/update body builders.
777+
778+
779+ class _FakeResp :
780+ def __init__ (self , status_code : int , payload : dict ):
781+ self .status_code = status_code
782+ self ._payload = payload
783+
784+ def json (self ):
785+ return self ._payload
786+
787+
788+ class _FakeClient :
789+ def __init__ (self ):
790+ self .posted : list = []
791+ self .patched : list = []
792+
793+ def __enter__ (self ):
794+ return self
795+
796+ def __exit__ (self , * _ ):
797+ pass
798+
799+ def post (self , path , json = None , ** _ ):
800+ self .posted .append ((path , json ))
801+ return _FakeResp (201 , {"id" : "cue_test" , "status" : "active" , "next_run" : None })
802+
803+ def patch (self , path , json = None , ** _ ):
804+ self .patched .append ((path , json ))
805+ return _FakeResp (200 , {"id" : "cue_test" , "name" : json .get ("name" , "x" ) if json else "x" })
806+
807+ def get (self , * _ , ** __ ):
808+ # Not used by these tests but defined so the context-manager surface
809+ # matches CueAPIClient.
810+ return _FakeResp (200 , {})
811+
812+
813+ def _patched_client (monkeypatch , client_holder ):
814+ """Patch CueAPIClient in cueapi.cli to return a captured FakeClient."""
815+ import cueapi .cli as cli_mod
816+
817+ def fake_factory (* _ , ** __ ):
818+ client_holder ["client" ] = _FakeClient ()
819+ return client_holder ["client" ]
820+
821+ monkeypatch .setattr (cli_mod , "CueAPIClient" , fake_factory )
822+
823+
824+ def test_create_require_payload_override_true_sends_field (monkeypatch ):
825+ holder : dict = {}
826+ _patched_client (monkeypatch , holder )
827+ result = runner .invoke (
828+ main ,
829+ [
830+ "create" ,
831+ "--name" , "team-comm" ,
832+ "--cron" , "0 9 * * *" ,
833+ "--worker" ,
834+ "--require-payload-override" ,
835+ ],
836+ )
837+ assert result .exit_code == 0 , result .output
838+ body = holder ["client" ].posted [- 1 ][1 ]
839+ assert body .get ("require_payload_override" ) is True
840+
841+
842+ def test_create_no_require_payload_override_sends_false (monkeypatch ):
843+ holder : dict = {}
844+ _patched_client (monkeypatch , holder )
845+ result = runner .invoke (
846+ main ,
847+ [
848+ "create" ,
849+ "--name" , "cron-cue" ,
850+ "--cron" , "0 9 * * *" ,
851+ "--worker" ,
852+ "--no-require-payload-override" ,
853+ ],
854+ )
855+ assert result .exit_code == 0 , result .output
856+ body = holder ["client" ].posted [- 1 ][1 ]
857+ assert body .get ("require_payload_override" ) is False
858+
859+
860+ def test_create_omits_require_payload_override_when_unset (monkeypatch ):
861+ # Default None must NOT appear in the body — server-side default is the
862+ # source of truth for "did the caller specify this?" Pinning this
863+ # behavior so a refactor can't silently start sending false.
864+ holder : dict = {}
865+ _patched_client (monkeypatch , holder )
866+ result = runner .invoke (
867+ main ,
868+ ["create" , "--name" , "x" , "--cron" , "0 9 * * *" , "--worker" ],
869+ )
870+ assert result .exit_code == 0 , result .output
871+ body = holder ["client" ].posted [- 1 ][1 ]
872+ assert "require_payload_override" not in body
873+
874+
875+ def test_create_required_keys_splits_and_trims (monkeypatch ):
876+ holder : dict = {}
877+ _patched_client (monkeypatch , holder )
878+ result = runner .invoke (
879+ main ,
880+ [
881+ "create" ,
882+ "--name" , "team-comm" ,
883+ "--cron" , "0 9 * * *" ,
884+ "--worker" ,
885+ "--required-keys" , " task ,message, token" ,
886+ ],
887+ )
888+ assert result .exit_code == 0 , result .output
889+ body = holder ["client" ].posted [- 1 ][1 ]
890+ assert body .get ("required_payload_keys" ) == ["task" , "message" , "token" ]
891+
892+
893+ def test_create_required_keys_empty_string_sends_empty_list (monkeypatch ):
894+ # Empty string is the "explicit clear" path. Pin so a future refactor
895+ # doesn't drop the body field entirely (which would mean "leave
896+ # unchanged" not "clear").
897+ holder : dict = {}
898+ _patched_client (monkeypatch , holder )
899+ result = runner .invoke (
900+ main ,
901+ [
902+ "create" ,
903+ "--name" , "x" ,
904+ "--cron" , "0 9 * * *" ,
905+ "--worker" ,
906+ "--required-keys" , "" ,
907+ ],
908+ )
909+ assert result .exit_code == 0 , result .output
910+ body = holder ["client" ].posted [- 1 ][1 ]
911+ assert body .get ("required_payload_keys" ) == []
912+
913+
914+ def test_update_require_payload_override_tri_state (monkeypatch ):
915+ holder : dict = {}
916+ _patched_client (monkeypatch , holder )
917+ result = runner .invoke (
918+ main ,
919+ ["update" , "cue_test" , "--require-payload-override" ],
920+ )
921+ assert result .exit_code == 0 , result .output
922+ body = holder ["client" ].patched [- 1 ][1 ]
923+ assert body .get ("require_payload_override" ) is True
924+
925+
926+ def test_update_no_require_payload_override_sends_false (monkeypatch ):
927+ holder : dict = {}
928+ _patched_client (monkeypatch , holder )
929+ result = runner .invoke (
930+ main ,
931+ ["update" , "cue_test" , "--no-require-payload-override" ],
932+ )
933+ assert result .exit_code == 0 , result .output
934+ body = holder ["client" ].patched [- 1 ][1 ]
935+ assert body .get ("require_payload_override" ) is False
936+
937+
938+ def test_update_required_keys_works (monkeypatch ):
939+ holder : dict = {}
940+ _patched_client (monkeypatch , holder )
941+ result = runner .invoke (
942+ main ,
943+ ["update" , "cue_test" , "--required-keys" , "a,b" ],
944+ )
945+ assert result .exit_code == 0 , result .output
946+ body = holder ["client" ].patched [- 1 ][1 ]
947+ assert body .get ("required_payload_keys" ) == ["a" , "b" ]
948+
949+
950+ # --- effective payload display on `executions get` (hosted PR #589) ---
951+
952+
953+ def test_executions_get_displays_payload_when_present (monkeypatch ):
954+ import cueapi .cli as cli_mod
955+
956+ fake_response = {
957+ "id" : "exec_abc" ,
958+ "cue_id" : "cue_xyz" ,
959+ "status" : "success" ,
960+ "scheduled_for" : "2026-05-04T10:00:00Z" ,
961+ "attempts" : 1 ,
962+ "payload" : {"task" : "demo" , "message" : "hello" },
963+ }
964+
965+ class _GetClient :
966+ def __enter__ (self ):
967+ return self
968+
969+ def __exit__ (self , * _ ):
970+ pass
971+
972+ def get (self , path , ** _ ):
973+ return _FakeResp (200 , fake_response )
974+
975+ monkeypatch .setattr (cli_mod , "CueAPIClient" , lambda * _ , ** __ : _GetClient ())
976+ result = runner .invoke (main , ["executions" , "get" , "exec_abc" ])
977+ assert result .exit_code == 0 , result .output
978+ assert "Payload:" in result .output
979+ # Pretty-print with indent=2, sort_keys=True — pin the keys are visible.
980+ assert "task" in result .output
981+ assert "demo" in result .output
982+
983+
984+ def test_executions_get_omits_payload_when_null (monkeypatch ):
985+ import cueapi .cli as cli_mod
986+
987+ fake_response = {
988+ "id" : "exec_abc" ,
989+ "cue_id" : "cue_xyz" ,
990+ "status" : "pending" ,
991+ "payload" : None ,
992+ }
993+
994+ class _GetClient :
995+ def __enter__ (self ):
996+ return self
997+
998+ def __exit__ (self , * _ ):
999+ pass
1000+
1001+ def get (self , path , ** _ ):
1002+ return _FakeResp (200 , fake_response )
1003+
1004+ monkeypatch .setattr (cli_mod , "CueAPIClient" , lambda * _ , ** __ : _GetClient ())
1005+ result = runner .invoke (main , ["executions" , "get" , "exec_abc" ])
1006+ assert result .exit_code == 0 , result .output
1007+ assert "Payload:" not in result .output
0 commit comments