@@ -3558,3 +3558,185 @@ func TestExecuteNotificationAction_TypeCommand(t *testing.T) {
35583558 })
35593559 }
35603560}
3561+ func TestExecuteSessionEndAction_TypeOutput (t * testing.T ) {
3562+ tests := []struct {
3563+ name string
3564+ action Action
3565+ wantContinue bool
3566+ wantSystemMessage string
3567+ wantErr bool
3568+ }{
3569+ {
3570+ name : "Message only -> systemMessage set, continue=true" ,
3571+ action : Action {
3572+ Type : "output" ,
3573+ Message : "Session cleanup completed" ,
3574+ },
3575+ wantContinue : true ,
3576+ wantSystemMessage : "Session cleanup completed" ,
3577+ wantErr : false ,
3578+ },
3579+ {
3580+ name : "Empty message -> fail-safe (systemMessage=fixed message, continue=true)" ,
3581+ action : Action {
3582+ Type : "output" ,
3583+ Message : "" ,
3584+ },
3585+ wantContinue : true ,
3586+ wantSystemMessage : "Empty message in SessionEnd action" ,
3587+ wantErr : false ,
3588+ },
3589+ {
3590+ name : "exit_status specified -> ignore exit_status, emit warning" ,
3591+ action : Action {
3592+ Type : "output" ,
3593+ Message : "Test message" ,
3594+ ExitStatus : intPtr (2 ),
3595+ },
3596+ wantContinue : true ,
3597+ wantSystemMessage : "Test message" ,
3598+ wantErr : false ,
3599+ },
3600+ }
3601+
3602+ for _ , tt := range tests {
3603+ t .Run (tt .name , func (t * testing.T ) {
3604+ runner := & stubRunnerWithOutput {}
3605+ executor := & ActionExecutor {runner : runner }
3606+ input := & SessionEndInput {}
3607+
3608+ result , err := executor .ExecuteSessionEndAction (tt .action , input , map [string ]interface {}{})
3609+
3610+ if (err != nil ) != tt .wantErr {
3611+ t .Errorf ("ExecuteSessionEndAction() error = %v, wantErr %v" , err , tt .wantErr )
3612+ return
3613+ }
3614+
3615+ if result == nil {
3616+ t .Fatal ("Expected non-nil ActionOutput, got nil" )
3617+ }
3618+
3619+ if result .Continue != tt .wantContinue {
3620+ t .Errorf ("Continue = %v, want %v" , result .Continue , tt .wantContinue )
3621+ }
3622+
3623+ if result .SystemMessage != tt .wantSystemMessage {
3624+ t .Errorf ("SystemMessage = %q, want %q" , result .SystemMessage , tt .wantSystemMessage )
3625+ }
3626+ })
3627+ }
3628+ }
3629+
3630+ func TestExecuteSessionEndAction_TypeCommand (t * testing.T ) {
3631+ tests := []struct {
3632+ name string
3633+ stdout string
3634+ stderr string
3635+ exitCode int
3636+ cmdErr error
3637+ wantContinue bool
3638+ wantSystemMessage string
3639+ wantErr bool
3640+ }{
3641+ {
3642+ name : "Valid JSON output with all fields" ,
3643+ stdout : `{"continue": true, "stopReason": "cleanup done", "suppressOutput": false, "systemMessage": "Session ended"}` ,
3644+ exitCode : 0 ,
3645+ wantContinue : true ,
3646+ wantErr : false ,
3647+ },
3648+ {
3649+ name : "Valid JSON output with minimal fields" ,
3650+ stdout : `{"continue": true}` ,
3651+ exitCode : 0 ,
3652+ wantContinue : true ,
3653+ wantErr : false ,
3654+ },
3655+ {
3656+ name : "Empty stdout -> continue=true, no error" ,
3657+ stdout : "" ,
3658+ exitCode : 0 ,
3659+ wantContinue : true ,
3660+ wantSystemMessage : "" ,
3661+ wantErr : false ,
3662+ },
3663+ {
3664+ name : "Command failed (exit code 1) -> fail-safe (continue=true, systemMessage=error)" ,
3665+ stdout : "" ,
3666+ stderr : "command error" ,
3667+ exitCode : 1 ,
3668+ wantContinue : true ,
3669+ wantSystemMessage : "Command failed with exit code 1: command error" ,
3670+ wantErr : false ,
3671+ },
3672+ {
3673+ name : "Invalid JSON -> fail-safe (continue=true, systemMessage=error)" ,
3674+ stdout : `{"continue": "invalid"}` ,
3675+ exitCode : 0 ,
3676+ wantContinue : true ,
3677+ wantSystemMessage : "Command output is not valid JSON: {\" continue\" : \" invalid\" }" ,
3678+ wantErr : false ,
3679+ },
3680+ {
3681+ name : "Unsupported field in JSON -> warning to stderr, continue processing" ,
3682+ stdout : `{"continue": true, "decision": "block"}` ,
3683+ exitCode : 0 ,
3684+ wantContinue : true ,
3685+ wantErr : false ,
3686+ },
3687+ }
3688+
3689+ for _ , tt := range tests {
3690+ t .Run (tt .name , func (t * testing.T ) {
3691+ runner := & stubRunnerWithOutput {
3692+ stdout : tt .stdout ,
3693+ stderr : tt .stderr ,
3694+ exitCode : tt .exitCode ,
3695+ err : tt .cmdErr ,
3696+ }
3697+ executor := & ActionExecutor {runner : runner }
3698+ input := & SessionEndInput {}
3699+ action := Action {
3700+ Type : "command" ,
3701+ Command : "test-command" ,
3702+ }
3703+
3704+ // Capture stderr to check for warnings
3705+ oldStderr := os .Stderr
3706+ r , w , _ := os .Pipe ()
3707+ os .Stderr = w
3708+
3709+ result , err := executor .ExecuteSessionEndAction (action , input , map [string ]interface {}{})
3710+
3711+ _ = w .Close ()
3712+ os .Stderr = oldStderr
3713+ var buf bytes.Buffer
3714+ _ , _ = io .Copy (& buf , r )
3715+ stderrOutput := buf .String ()
3716+
3717+ if (err != nil ) != tt .wantErr {
3718+ t .Errorf ("ExecuteSessionEndAction() error = %v, wantErr %v" , err , tt .wantErr )
3719+ return
3720+ }
3721+
3722+ if result == nil {
3723+ t .Fatal ("Expected non-nil ActionOutput, got nil" )
3724+ }
3725+
3726+ if result .Continue != tt .wantContinue {
3727+ t .Errorf ("Continue = %v, want %v" , result .Continue , tt .wantContinue )
3728+ }
3729+
3730+ if tt .wantSystemMessage != "" && result .SystemMessage != tt .wantSystemMessage {
3731+ t .Errorf ("SystemMessage = %q, want %q" , result .SystemMessage , tt .wantSystemMessage )
3732+ }
3733+
3734+ // Check for unsupported field warnings
3735+ if tt .name == "Unsupported field in JSON -> warning to stderr, continue processing" {
3736+ if ! strings .Contains (stderrOutput , "Warning" ) || ! strings .Contains (stderrOutput , "decision" ) {
3737+ t .Errorf ("Expected warning about unsupported field 'decision' in stderr, got: %s" , stderrOutput )
3738+ }
3739+ }
3740+ })
3741+ }
3742+ }
0 commit comments