diff --git a/ui/app/(prowler)/lighthouse/page.test.ts b/ui/app/(prowler)/lighthouse/page.test.ts
new file mode 100644
index 0000000000..e7260e2506
--- /dev/null
+++ b/ui/app/(prowler)/lighthouse/page.test.ts
@@ -0,0 +1,19 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { describe, expect, it } from "vitest";
+
+describe("Lighthouse page", () => {
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
+ const pagePath = path.join(currentDir, "page.tsx");
+ const source = readFileSync(pagePath, "utf8");
+
+ it("keys the Cloud chat by the active route conversation", () => {
+ // Given / When / Then
+ expect(source).toContain(
+ 'const chatRouteKey = activeSessionId ?? initialPrompt ?? "new";',
+ );
+ expect(source).toContain("key={chatRouteKey}");
+ });
+});
diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx
index 2860776ca1..396b9c243f 100644
--- a/ui/app/(prowler)/lighthouse/page.tsx
+++ b/ui/app/(prowler)/lighthouse/page.tsx
@@ -66,11 +66,13 @@ export default async function AIChatbot({
const initialMessages = activeSessionId
? await getLighthouseV2Messages(activeSessionId)
: { data: [] };
+ const chatRouteKey = activeSessionId ?? initialPrompt ?? "new";
return (
{
});
});
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
it("uses the neutral page background instead of the global app background token", () => {
// Given / When
const { container } = renderPage();
@@ -148,6 +152,43 @@ describe("LighthouseV2ChatPage", () => {
expect(createSessionMock).toHaveBeenCalledWith("Summarize findings");
expect(pushMock).toHaveBeenCalledWith("/lighthouse?session=session-1");
});
+
+ it("closes an active stream when the chat unmounts", async () => {
+ // Given
+ const user = userEvent.setup();
+ const closeMock = vi.fn();
+ const eventSourceMock = vi.fn(function MockEventSource(
+ this: Record,
+ ) {
+ this.addEventListener = vi.fn();
+ this.close = closeMock;
+ });
+ vi.stubGlobal("EventSource", eventSourceMock);
+ sendMessageMock.mockResolvedValue({
+ data: {
+ task: {
+ id: "task-1",
+ name: "lighthouse-run",
+ state: "executing",
+ },
+ streamUrl: "/api/stream",
+ },
+ });
+ const { unmount } = renderPage({ initialSessionId: "session-1" });
+
+ // When
+ await user.type(
+ screen.getByRole("textbox", { name: "Message" }),
+ ["Summarize findings", "{Enter}"].join(""),
+ );
+ await waitFor(() =>
+ expect(eventSourceMock).toHaveBeenCalledWith("/api/stream"),
+ );
+ unmount();
+
+ // Then
+ expect(closeMock).toHaveBeenCalledTimes(1);
+ });
});
function renderPage(
diff --git a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx
index dbb2b211b7..dca6f11f3b 100644
--- a/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx
+++ b/ui/components/lighthouse-v2/chat/lighthouse-v2-chat-page.tsx
@@ -280,6 +280,10 @@ export function LighthouseV2ChatPage({
}
};
+ useMountEffect(() => {
+ return () => closeStream();
+ });
+
useMountEffect(() => {
if (initialPrompt && !initialPromptSentRef.current) {
initialPromptSentRef.current = true;