// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackruntime import ( "testing" "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" ) // TestActionInvocationHooksValidation validates that action invocation status // hooks work correctly, including enum values, hook data structure, and lifecycle ordering. func TestActionInvocationHooksValidation(t *testing.T) { t.Run("hook_capture_mechanism", func(t *testing.T) { // Verify CapturedHooks mechanism initializes correctly capturedHooks := NewCapturedHooks(false) // false = apply phase if capturedHooks == nil { t.Fatal("CapturedHooks should not be nil") } // Verify the hooks slice starts empty (nil or zero length) if len(capturedHooks.ReportActionInvocationStatus) != 0 { t.Errorf("expected empty initial hook list, got %d", len(capturedHooks.ReportActionInvocationStatus)) } // Verify we can append to it capturedHooks.ReportActionInvocationStatus = append( capturedHooks.ReportActionInvocationStatus, &hooks.ActionInvocationStatusHookData{ Addr: mustAbsActionInvocationInstance("component.test.action.example.run"), ProviderAddr: mustDefaultRootProvider("testing").Provider, Status: hooks.ActionInvocationRunning, }, ) if len(capturedHooks.ReportActionInvocationStatus) != 1 { t.Errorf("after append, expected 1 hook, got %d", len(capturedHooks.ReportActionInvocationStatus)) } }) t.Run("action_invocation_status_enum", func(t *testing.T) { // Test that all enum constants are defined and have valid string representations statuses := []hooks.ActionInvocationStatus{ hooks.ActionInvocationStatusInvalid, hooks.ActionInvocationPending, hooks.ActionInvocationRunning, hooks.ActionInvocationCompleted, hooks.ActionInvocationErrored, } expectedStrings := map[hooks.ActionInvocationStatus]string{ hooks.ActionInvocationStatusInvalid: "ActionInvocationStatusInvalid", hooks.ActionInvocationPending: "ActionInvocationPending", hooks.ActionInvocationRunning: "ActionInvocationRunning", hooks.ActionInvocationCompleted: "ActionInvocationCompleted", hooks.ActionInvocationErrored: "ActionInvocationErrored", } // Verify String() returns expected values for _, status := range statuses { str := status.String() expected, ok := expectedStrings[status] if !ok { t.Errorf("unexpected status constant: %v", status) continue } if str != expected { t.Errorf("status %v: expected String() = %q, got %q", status, expected, str) } } // Verify ForProtobuf() returns valid values (non-negative) for _, status := range statuses { proto := status.ForProtobuf() if proto < 0 { t.Errorf("status %v has invalid protobuf value: %v", status, proto) } } // Verify we have exactly 5 status values if len(statuses) != 5 { t.Errorf("expected 5 status constants, got %d", len(statuses)) } }) t.Run("hook_data_structure", func(t *testing.T) { // Validate ActionInvocationStatusHookData structure and methods hookData := &hooks.ActionInvocationStatusHookData{ Addr: mustAbsActionInvocationInstance("component.test.action.example.run"), ProviderAddr: mustDefaultRootProvider("testing").Provider, Status: hooks.ActionInvocationRunning, } // Verify fields are set if hookData.Addr.String() == "" { t.Error("Addr should not be empty") } if hookData.ProviderAddr.String() == "" { t.Error("ProviderAddr should not be empty") } if hookData.Status == hooks.ActionInvocationStatusInvalid { t.Error("Status should not be Invalid when explicitly set to Running") } // Verify String() method str := hookData.String() if str == "" || str == "" { t.Errorf("String() should return valid representation, got: %q", str) } // Verify String() contains address if !contains(str, "component.test") { t.Errorf("String() should contain address, got: %q", str) } // Verify nil handling var nilHook *hooks.ActionInvocationStatusHookData if nilHook.String() != "" { t.Errorf("nil hook String() should return , got: %q", nilHook.String()) } }) t.Run("hook_status_lifecycle_ordering", func(t *testing.T) { // Test expected hook status sequences for different scenarios testCases := []struct { name string capturedStatuses []hooks.ActionInvocationStatus wantValid bool description string }{ { name: "successful_action", capturedStatuses: []hooks.ActionInvocationStatus{ hooks.ActionInvocationRunning, hooks.ActionInvocationCompleted, }, wantValid: true, description: "Action starts running and completes successfully", }, { name: "failed_action", capturedStatuses: []hooks.ActionInvocationStatus{ hooks.ActionInvocationRunning, hooks.ActionInvocationErrored, }, wantValid: true, description: "Action starts running but encounters an error", }, { name: "pending_then_running_then_completed", capturedStatuses: []hooks.ActionInvocationStatus{ hooks.ActionInvocationPending, hooks.ActionInvocationRunning, hooks.ActionInvocationCompleted, }, wantValid: true, description: "Action goes through all states including pending", }, { name: "invalid_only_completed", capturedStatuses: []hooks.ActionInvocationStatus{ hooks.ActionInvocationCompleted, }, wantValid: false, description: "Invalid: completed without running", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Verify we captured the expected number of statuses if len(tc.capturedStatuses) == 0 { t.Error("test case should have at least one status") return } // For valid sequences, verify terminal state is at the end if tc.wantValid && len(tc.capturedStatuses) > 0 { lastStatus := tc.capturedStatuses[len(tc.capturedStatuses)-1] isTerminal := lastStatus == hooks.ActionInvocationCompleted || lastStatus == hooks.ActionInvocationErrored if !isTerminal { t.Errorf("valid sequence should end in terminal state (Completed/Errored), got %v", lastStatus) } } // For invalid sequences starting with Completed, verify it's actually invalid if !tc.wantValid && len(tc.capturedStatuses) > 0 { firstStatus := tc.capturedStatuses[0] if firstStatus == hooks.ActionInvocationCompleted && len(tc.capturedStatuses) == 1 { // This is indeed invalid - can't complete without running t.Logf("correctly identified invalid sequence: %v", tc.capturedStatuses) } } }) } }) } // contains checks if a string contains a substring func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr)) } func containsHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }