fix: close delete resume and SSE replay races

This commit is contained in:
User
2026-07-15 02:08:11 +02:00
parent 4d36f4acf1
commit 0bcec1591b
5 changed files with 208 additions and 5 deletions
@@ -29,6 +29,7 @@ function deferred() {
beforeEach(() => {
FakeEventSource.instances = [];
(globalThis as any).EventSource = FakeEventSource;
(globalThis as any).PointerEvent = MouseEvent;
useSessionStore.getState().resetSession();
server.use(
http.get("http://localhost:8787/sessions", () => HttpResponse.json(LIST)),
@@ -489,6 +490,139 @@ test("starting a new question invalidates a pending Resume intent", async () =>
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
});
test("a successful Delete invalidates an earlier pending Resume for the same target", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteCompleted = deferred();
server.use(
http.post("http://localhost:8787/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("http://localhost:8787/sessions/:id", ({ params }) => {
expect(params.id).toBe("s1");
deleteCompleted.resolve();
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteCompleted.promise;
resumeGate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(FakeEventSource.instances).toHaveLength(0);
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false");
});
test("a successful Delete detaches a Resume that commits while Delete is pending", async () => {
const deleteGate = deferred();
const deleteStarted = deferred();
server.use(
http.delete("http://localhost:8787/sessions/:id", async ({ params }) => {
expect(params.id).toBe("s1");
deleteStarted.resolve();
await deleteGate.promise;
return new HttpResponse(null, { status: 204 });
}),
http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")),
http.get("http://localhost:8787/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteStarted.promise;
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
deleteGate.resolve();
await waitFor(() => {
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false");
});
expect(FakeEventSource.instances[0].closed).toBe(true);
});
test("deleting another session does not invalidate a pending Resume", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteCompleted = deferred();
server.use(
http.post("http://localhost:8787/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("http://localhost:8787/sessions/:id", ({ params }) => {
expect(params.id).toBe("s2");
deleteCompleted.resolve();
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
(await screen.findByRole("checkbox", { name: "Select Archiviata due" })).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteCompleted.promise;
resumeGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events");
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
});
test("a failed Delete does not invalidate a pending Resume for its target", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteFailed = deferred();
server.use(
http.post("http://localhost:8787/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("http://localhost:8787/sessions/:id", ({ params }) => {
expect(params.id).toBe("s1");
deleteFailed.resolve();
return new HttpResponse(null, { status: 500 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteFailed.promise;
resumeGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events");
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
});
test("a failed Resume with no active session preserves the panel and existing activity", async () => {
server.use(http.post("http://localhost:8787/sessions/:id/resume", () => new HttpResponse(null, { status: 409 })));
useSessionStore.setState({