mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
fix: detect team/org-level budget errors in error banner (#13003)
This commit is contained in:
@@ -358,6 +358,30 @@ describe("Conversation WebSocket Handler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show friendly i18n message for budget ConversationErrorEvent", async () => {
|
||||
const mockBudgetConversationError = createMockConversationErrorEvent({
|
||||
detail:
|
||||
"Budget has been exceeded! Current cost: 18.51, Max budget: 18.24",
|
||||
});
|
||||
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
client.send(JSON.stringify(mockBudgetConversationError));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithWebSocketContext(<ErrorMessageStoreComponent />);
|
||||
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("error-message")).toHaveTextContent(
|
||||
"STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set error message store on WebSocket connection errors", async () => {
|
||||
// Simulate a connect-then-fail sequence (the MSW server auto-connects by default).
|
||||
// This should surface an error message because the app has previously connected.
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "vitest";
|
||||
import { screen, waitFor, render, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createMockAgentErrorEvent } from "#/mocks/mock-ws-helpers";
|
||||
import {
|
||||
createMockAgentErrorEvent,
|
||||
createMockConversationErrorEvent,
|
||||
} from "#/mocks/mock-ws-helpers";
|
||||
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
|
||||
import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup";
|
||||
import { ConnectionStatusComponent } from "./helpers/websocket-test-components";
|
||||
@@ -229,5 +232,35 @@ describe("PostHog Analytics Tracking", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should track credit_limit_reached when ConversationErrorEvent contains budget error", async () => {
|
||||
const mockBudgetConversationError = createMockConversationErrorEvent({
|
||||
detail:
|
||||
"Budget has been exceeded! Current cost: 18.51, Max budget: 18.24",
|
||||
});
|
||||
|
||||
mswServer.use(
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
client.send(JSON.stringify(mockBudgetConversationError));
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<ConnectionStatusComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("connection-state")).toHaveTextContent(
|
||||
"OPEN",
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTrackCreditLimitReached).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
conversationId: "test-conversation-123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,7 +358,14 @@ export function ConversationWebSocketProvider({
|
||||
},
|
||||
posthog,
|
||||
});
|
||||
setErrorMessage(event.detail);
|
||||
if (isBudgetOrCreditError(event.detail)) {
|
||||
setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS);
|
||||
trackCreditLimitReached({
|
||||
conversationId: conversationId || "unknown",
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(event.detail);
|
||||
}
|
||||
} else {
|
||||
// Clear error message on any non-ConversationErrorEvent
|
||||
removeErrorMessage();
|
||||
@@ -498,6 +505,31 @@ export function ConversationWebSocketProvider({
|
||||
};
|
||||
addEvent(eventWithPlanningFlag);
|
||||
|
||||
// Handle ConversationErrorEvent specifically - show error banner
|
||||
// AgentErrorEvent errors are displayed inline in the chat, not as banners
|
||||
if (isConversationErrorEvent(event)) {
|
||||
trackError({
|
||||
message: event.detail,
|
||||
source: "planning_conversation",
|
||||
metadata: {
|
||||
eventId: event.id,
|
||||
errorCode: event.code,
|
||||
},
|
||||
posthog,
|
||||
});
|
||||
if (isBudgetOrCreditError(event.detail)) {
|
||||
setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS);
|
||||
trackCreditLimitReached({
|
||||
conversationId: conversationId || "unknown",
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(event.detail);
|
||||
}
|
||||
} else {
|
||||
// Clear error message on any non-ConversationErrorEvent
|
||||
removeErrorMessage();
|
||||
}
|
||||
|
||||
// Handle AgentErrorEvent specifically
|
||||
if (isAgentErrorEvent(event)) {
|
||||
trackError({
|
||||
@@ -513,6 +545,9 @@ export function ConversationWebSocketProvider({
|
||||
// Use friendly i18n message for budget/credit errors instead of raw error
|
||||
if (isBudgetOrCreditError(event.error)) {
|
||||
setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS);
|
||||
trackCreditLimitReached({
|
||||
conversationId: conversationId || "unknown",
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(event.error);
|
||||
}
|
||||
@@ -609,15 +644,18 @@ export function ConversationWebSocketProvider({
|
||||
isLoadingHistoryPlanning,
|
||||
expectedEventCountPlanning,
|
||||
setErrorMessage,
|
||||
removeErrorMessage,
|
||||
removeOptimisticUserMessage,
|
||||
queryClient,
|
||||
subConversations,
|
||||
conversationId,
|
||||
setExecutionStatus,
|
||||
appendInput,
|
||||
appendOutput,
|
||||
readConversationFile,
|
||||
setPlanContent,
|
||||
updateMetricsFromStats,
|
||||
trackCreditLimitReached,
|
||||
posthog,
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user