diff --git a/src/Frontend/src/components/failedmessages/MessageGroupList.vue b/src/Frontend/src/components/failedmessages/MessageGroupList.vue index 977bbaf75..c6072f07c 100644 --- a/src/Frontend/src/components/failedmessages/MessageGroupList.vue +++ b/src/Frontend/src/components/failedmessages/MessageGroupList.vue @@ -565,7 +565,7 @@ defineExpose({ Messages left to delete: - {{ group.operation_remaining_count || 0 }} + {{ Math.max(group.operation_remaining_count || 0, 0) }} diff --git a/src/Frontend/src/components/failedmessages/messageGroupDeleteCounters.spec.ts b/src/Frontend/src/components/failedmessages/messageGroupDeleteCounters.spec.ts new file mode 100644 index 000000000..6a6519ac0 --- /dev/null +++ b/src/Frontend/src/components/failedmessages/messageGroupDeleteCounters.spec.ts @@ -0,0 +1,72 @@ +import { describe, test, expect, beforeEach, vi } from "vitest"; +import { render } from "@testing-library/vue"; +import { createTestingPinia } from "@pinia/testing"; +import { defineComponent, h, nextTick, ref } from "vue"; +import { createRouter, createMemoryHistory } from "vue-router"; +import type GroupOperation from "@/resources/GroupOperation"; +import MessageGroupList, { type IMessageGroupList } from "@/components/failedmessages/MessageGroupList.vue"; + +// Regression test for https://github.com/Particular/ServicePulse/issues/3126 +// While a group delete is in progress, ServiceControl's starting count and the +// running deleted count come from separate queries. If more failures keep +// arriving in the group, the deleted count can overtake the starting count, +// which drives "Messages left to delete" (starting count - deleted count) negative. +const groupBeingDeleted: GroupOperation = { + id: "group-1", + title: "SomeException", + type: "exception-type", + count: 666677, + operation_messages_completed_count: 670000, + comment: "", + operation_status: "ArchiveProgressing", + operation_failed: false, + operation_progress: 0.5, + operation_remaining_count: -3323, + need_user_acknowledgement: false, +}; + +vi.mock("@/components/failedmessages/messageGroupClient", () => ({ + default: () => ({ + getExceptionGroups: vi.fn().mockResolvedValue([groupBeingDeleted]), + isError: () => false, + }), +})); + +async function renderGroupList() { + const listRef = ref(); + + const Harness = defineComponent({ + setup() { + return () => h(MessageGroupList, { ref: listRef, sortFunction: () => 0 }); + }, + }); + + const router = createRouter({ history: createMemoryHistory(), routes: [{ path: "/:catchAll(.*)", component: { template: "
" } }] }); + + const result = render(Harness, { + global: { + plugins: [router, createTestingPinia({ stubActions: true })], + }, + }); + + await nextTick(); + await listRef.value?.loadFailedMessageGroups(); + await nextTick(); + + return result; +} + +describe("failed-message group delete counters", () => { + beforeEach(() => { + document.body.innerHTML = '
'; + }); + + test("Messages left to delete does not go negative when more failures arrive during a delete", async () => { + const { container } = await renderGroupList(); + + const text = container.textContent?.replace(/\s+/g, " ") ?? ""; + const match = text.match(/Messages left to delete:\s*(-?\d+)/); + + expect(match?.[1]).toBe("0"); + }); +});