From 56b21cb62816e605e8757a870d133443b8e82059 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Sat, 20 Jun 2026 17:53:17 +0530 Subject: [PATCH] enrich webhook payload and add notification on status update - expand WebhookPayload to capture workflow_id, version, and error details - add WebhookError struct for structured error info from stakwork - send websocket notification when webhook status is received - add tests for HandleChatWebhook covering success and error cases Signed-off-by: Gautam Kumar --- handlers/chat.go | 35 ++++++++- handlers/chat_test.go | 171 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 4 deletions(-) diff --git a/handlers/chat.go b/handlers/chat.go index efb67dadc..94adbf035 100644 --- a/handlers/chat.go +++ b/handlers/chat.go @@ -191,10 +191,20 @@ type SSEMaintenanceResponse struct { } type WebhookPayload struct { - ProjectStatus string `json:"project_status"` - Error *struct { - Message string `json:"message"` - } `json:"error,omitempty"` + ProjectStatus string `json:"project_status"` + ProjectOutput map[string]interface{} `json:"project_output,omitempty"` + WorkflowID int `json:"workflow_id,omitempty"` + WorkflowVersionID int `json:"workflow_version_id,omitempty"` + WorkflowVersion int `json:"workflow_version,omitempty"` + Error *WebhookError `json:"error,omitempty"` +} + +type WebhookError struct { + Message string `json:"message"` + WorkflowID int `json:"workflow_id,omitempty"` + WorkflowVersionID int `json:"workflow_version_id,omitempty"` + StepName string `json:"step_name,omitempty"` + SkillName string `json:"skill_name,omitempty"` } type ChatStatusWebhookResponse struct { @@ -2480,6 +2490,7 @@ func (ch *ChatHandler) HandleChatWebhook(w http.ResponseWriter, r *http.Request) if payload.ProjectStatus == "completed" { status = "success" + message = "Workflow completed successfully" } else if payload.ProjectStatus == "error" { status = "error" if payload.Error != nil { @@ -2511,6 +2522,22 @@ func (ch *ChatHandler) HandleChatWebhook(w http.ResponseWriter, r *http.Request) logger.Log.Info("Created chat status for chat %s: %s - %s", chatID, createdStatus.Status, createdStatus.Message) + wsMessage := websocket.TicketMessage{ + BroadcastType: "chat", + Message: fmt.Sprintf("Workflow %s for chat %s", status, chatID), + Action: "webhook_status", + ChatMessage: db.ChatMessage{ + ID: chatID, + ChatID: chatID, + Message: fmt.Sprintf("Project status: %s", message), + Status: status, + }, + } + + if err := websocket.WebsocketPool.SendTicketMessage(wsMessage); err != nil { + logger.Log.Error("Failed to send websocket message for webhook: %v", err) + } + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ChatStatusWebhookResponse{ Status: "success", diff --git a/handlers/chat_test.go b/handlers/chat_test.go index a98a70788..367d221f6 100644 --- a/handlers/chat_test.go +++ b/handlers/chat_test.go @@ -3387,6 +3387,177 @@ func TestSSEMaintenance(t *testing.T) { }) } +func TestHandleChatWebhook(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + chatHandler := NewChatHandler(&http.Client{}, db.TestDB) + + t.Run("should return bad request when chat_id is missing", func(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat//update", nil) + + rctx := chi.NewRouteContext() + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "error", response.Status) + assert.Equal(t, "Chat ID is required", response.Message) + }) + + t.Run("should return not found when chat does not exist", func(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat/nonexistent/update", nil) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("chat_id", "nonexistent") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "error", response.Status) + }) + + t.Run("should return bad request for invalid payload", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: uuid.New().String(), + Title: "Test Chat", + } + db.TestDB.AddChat(chat) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat/"+chat.ID+"/update", strings.NewReader("invalid json")) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("chat_id", chat.ID) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "error", response.Status) + }) + + t.Run("should process completed webhook successfully", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: uuid.New().String(), + Title: "Test Chat Completed", + } + db.TestDB.AddChat(chat) + + payload := WebhookPayload{ + ProjectStatus: "completed", + WorkflowID: 1358, + WorkflowVersionID: 6155, + WorkflowVersion: 6155, + } + bodyBytes, _ := json.Marshal(payload) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat/"+chat.ID+"/update", bytes.NewReader(bodyBytes)) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("chat_id", chat.ID) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "success", response.Status) + }) + + t.Run("should process error webhook successfully", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: uuid.New().String(), + Title: "Test Chat Error", + } + db.TestDB.AddChat(chat) + + payload := WebhookPayload{ + ProjectStatus: "error", + WorkflowID: 1358, + WorkflowVersionID: 6161, + WorkflowVersion: 6161, + Error: &WebhookError{ + Message: "Error: couldn't connect to the website", + WorkflowID: 1357, + WorkflowVersionID: 1357, + StepName: "req", + SkillName: "request", + }, + } + bodyBytes, _ := json.Marshal(payload) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat/"+chat.ID+"/update", bytes.NewReader(bodyBytes)) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("chat_id", chat.ID) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "success", response.Status) + }) + + t.Run("should process error webhook without error details", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: uuid.New().String(), + Title: "Test Chat Error No Details", + } + db.TestDB.AddChat(chat) + + payload := WebhookPayload{ + ProjectStatus: "error", + } + bodyBytes, _ := json.Marshal(payload) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/hivechat/"+chat.ID+"/update", bytes.NewReader(bodyBytes)) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("chat_id", chat.ID) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.HandleChatWebhook(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response ChatStatusWebhookResponse + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "success", response.Status) + }) +} + type RoundTripFunc func(req *http.Request) (*http.Response, error) func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {