fix(dashboard): close SSE connections on outbound backpressure
The global SSE broadcast called res.write() without checking the return value, so a paused or backgrounded client would silently accumulate every store event for every entity (tasks, missions, plugins, agents, chat, ...) into res.outputData until the dashboard process OOMed. Add a 4 MB writableLength threshold; when exceeded, tear down the connection so the OS releases the buffer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
interface BackendConnectionErrorPageProps {
|
||||
errorMessage: string;
|
||||
isRetrying: boolean;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export function BackendConnectionErrorPage({
|
||||
errorMessage,
|
||||
isRetrying,
|
||||
onRetry,
|
||||
}: BackendConnectionErrorPageProps) {
|
||||
return (
|
||||
<div className="project-overview-empty" role="alert" aria-live="polite">
|
||||
<h2>Can't reach the Fusion backend</h2>
|
||||
<p className="settings-muted">
|
||||
Fusion couldn't load your projects right now. Please make sure the backend is running and try again.
|
||||
</p>
|
||||
<p className="settings-muted">Error: {errorMessage}</p>
|
||||
<button type="button" className="btn btn-primary" onClick={onRetry} disabled={isRetrying}>
|
||||
{isRetrying ? "Retrying…" : "Retry Connection"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -165,9 +165,12 @@ vi.mock("../../components/CustomModelDropdown", () => ({
|
||||
}));
|
||||
|
||||
// Mock state holders for dynamic mocking
|
||||
const mockRefreshProjects = vi.fn(async () => {});
|
||||
|
||||
const mockProjectsState = {
|
||||
projects: [] as any[],
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
};
|
||||
|
||||
const DEFAULT_PROJECT_ID = "proj_123";
|
||||
@@ -185,8 +188,8 @@ vi.mock("../../hooks/useProjects", () => ({
|
||||
useProjects: () => ({
|
||||
projects: mockProjectsState.projects,
|
||||
loading: mockProjectsState.loading,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
error: mockProjectsState.error,
|
||||
refresh: mockRefreshProjects,
|
||||
register: vi.fn(),
|
||||
update: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
@@ -258,6 +261,9 @@ beforeEach(() => {
|
||||
// Reset mock states
|
||||
mockProjectsState.projects = [];
|
||||
mockProjectsState.loading = false;
|
||||
mockProjectsState.error = null;
|
||||
mockRefreshProjects.mockReset();
|
||||
mockRefreshProjects.mockImplementation(async () => {});
|
||||
mockCurrentProjectState.currentProject = { id: DEFAULT_PROJECT_ID, name: "Test Project", path: "/test", status: "active", isolationMode: "in-process", createdAt: "", updatedAt: "" };
|
||||
mockCurrentProjectState.setCurrentProject.mockClear();
|
||||
mockCurrentProjectState.clearCurrentProject.mockClear();
|
||||
@@ -292,6 +298,51 @@ beforeEach(() => {
|
||||
mockGetStepData.mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe("App backend-unreachable first-run flow", () => {
|
||||
it("renders backend connection error page instead of setup wizard when projects fetch fails during first-run", async () => {
|
||||
mockProjectsState.projects = [];
|
||||
mockProjectsState.error = "Backend unavailable";
|
||||
mockCurrentProjectState.currentProject = null;
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Can't reach the Fusion backend")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByRole("button", { name: "Retry Connection" })).toBeTruthy();
|
||||
expect(screen.queryByText("Welcome to Fusion")).toBeNull();
|
||||
});
|
||||
|
||||
it("retries project loading and resumes setup wizard flow after connectivity recovers", async () => {
|
||||
mockProjectsState.projects = [];
|
||||
mockProjectsState.error = "Backend unavailable";
|
||||
mockCurrentProjectState.currentProject = null;
|
||||
|
||||
mockRefreshProjects.mockImplementation(async () => {
|
||||
mockProjectsState.error = null;
|
||||
});
|
||||
|
||||
const { rerender } = render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Retry Connection" })).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Retry Connection" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshProjects).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
rerender(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Welcome to Fusion")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("App mailbox unread count", () => {
|
||||
it("logs a warning when unread count fetch fails and keeps the zero-count fallback", async () => {
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
@@ -2267,9 +2267,11 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||
});
|
||||
|
||||
// The model dropdown should be pre-populated with the saved default
|
||||
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
|
||||
// Wait for async model/settings hydration before asserting selected value
|
||||
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
await waitFor(() => {
|
||||
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves selectedModel empty when no default is configured in global settings", async () => {
|
||||
@@ -2285,7 +2287,7 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
|
||||
// The model dropdown should be empty
|
||||
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
expect(dropdown.value).toBe("");
|
||||
});
|
||||
|
||||
@@ -2300,7 +2302,7 @@ describe("ModelOnboardingModal", () => {
|
||||
});
|
||||
|
||||
// The modal should still render with empty dropdown
|
||||
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||
expect(dropdown.value).toBe("");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user