From 429bc8c7cbaa8d9ef88a820b5ecefbc73da0e0d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 22:30:39 +0900 Subject: [PATCH 1/3] fix(tasks): preserve first task on duplicate ids --- frontend/src/components/TasksLayout.test.tsx | 47 ++++++++++++++++++++ frontend/src/components/TasksLayout.tsx | 6 ++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TasksLayout.test.tsx b/frontend/src/components/TasksLayout.test.tsx index a0cac6748..a8401c256 100644 --- a/frontend/src/components/TasksLayout.test.tsx +++ b/frontend/src/components/TasksLayout.test.tsx @@ -136,4 +136,51 @@ describe("TasksLayout", () => { expect(detailTab?.getAttribute("aria-selected")).toBe("true"); expect(detailTab?.tabIndex).toBe(0); }); + + it("keeps the first task when reply SLA results repeat an existing id", async () => { + const existingTask = { + id: "task-duplicate-1", + title: "기존 작업 제목", + status: "open", + priority: "normal", + source_type: "email", + source_email_id: "mail-1", + related_thread_id: "thread-1", + updated_at: "2026-06-18T05:00:00Z", + }; + const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith("/api/tasks")) return Promise.resolve(jsonResponse([existingTask])); + if (url.endsWith("/api/tasks/reply-sla-escalations")) { + expect(init?.method).toBe("POST"); + return Promise.resolve(jsonResponse({ + evaluated: 1, + created: 1, + policy: { overdue_hours: 48 }, + tasks: [{ ...existingTask, title: "새로 반환된 중복 제목", status: "in_progress" }], + })); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + await act(async () => { + root?.render(); + }); + await flushAsyncWork(); + + await act(async () => { + container?.querySelector('button[aria-label="보낸 메일 미답변 팔로업 작업 생성"]')?.click(); + }); + await flushAsyncWork(); + + const ticketList = container.querySelector('[aria-label="원본 연결 티켓 목록"]'); + expect(ticketList?.textContent).toContain("기존 작업 제목"); + expect(ticketList?.textContent).not.toContain("새로 반환된 중복 제목"); + expect(ticketList?.querySelectorAll("article")).toHaveLength(1); + }); }); diff --git a/frontend/src/components/TasksLayout.tsx b/frontend/src/components/TasksLayout.tsx index 98df4788a..dc5191b57 100644 --- a/frontend/src/components/TasksLayout.tsx +++ b/frontend/src/components/TasksLayout.tsx @@ -240,8 +240,10 @@ export function TasksLayout() { { overdue_hours: 48 }, ); setTicketTasks((currentTasks) => { - const mergedTasks = new Map(currentTasks.map((task) => [task.id, task])); - result.tasks.forEach((task) => mergedTasks.set(task.id, task)); + const mergedTasks = new Map(); + [...currentTasks, ...result.tasks].forEach((task) => { + if (!mergedTasks.has(task.id)) mergedTasks.set(task.id, task); + }); return Array.from(mergedTasks.values()); }); if (result.tasks.length > 0) { From aa0aa733cc591e5b46376bbb6293d16efebe5530 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 22:40:58 +0900 Subject: [PATCH 2/3] test(tasks): preserve authoritative reply SLA mutation response --- frontend/src/components/TasksLayout.test.tsx | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/src/components/TasksLayout.test.tsx b/frontend/src/components/TasksLayout.test.tsx index a0cac6748..fdb53ce03 100644 --- a/frontend/src/components/TasksLayout.test.tsx +++ b/frontend/src/components/TasksLayout.test.tsx @@ -136,4 +136,60 @@ describe("TasksLayout", () => { expect(detailTab?.getAttribute("aria-selected")).toBe("true"); expect(detailTab?.tabIndex).toBe(0); }); + + it("uses the reply SLA mutation response for an existing task id", async () => { + const existingTask = { + id: "task-duplicate-1", + title: "기존 미답변 작업", + status: "open", + priority: "normal", + source_type: "reply_sla", + source_email_id: "mail-1", + related_thread_id: "thread-1", + updated_at: "2026-06-18T05:00:00Z", + }; + const refreshedTask = { + ...existingTask, + title: "미답변 팔로업: 최신 메일 제목", + status: "blocked", + priority: "urgent", + updated_at: "2026-06-18T06:00:00Z", + }; + const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith("/api/tasks")) return Promise.resolve(jsonResponse([existingTask])); + if (url.endsWith("/api/tasks/reply-sla-escalations")) { + expect(init?.method).toBe("POST"); + return Promise.resolve(jsonResponse({ + evaluated: 1, + created: 0, + policy: { overdue_hours: 48 }, + tasks: [refreshedTask], + })); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + await act(async () => { + root?.render(); + }); + await flushAsyncWork(); + + expect(container.textContent).toContain("기존 미답변 작업"); + + await act(async () => { + container?.querySelector('button[aria-label="보낸 메일 미답변 팔로업 작업 생성"]')?.click(); + }); + await flushAsyncWork(); + + expect(container.textContent).toContain("미답변 팔로업: 최신 메일 제목"); + expect(container.textContent).not.toContain("기존 미답변 작업"); + expect(container.textContent).toContain("긴급"); + expect(container.textContent).toContain("검토 필요"); + }); }); From 8ad819786c6c2a99e0a158d2192054eb8189b2f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 22:51:21 +0900 Subject: [PATCH 3/3] test(tasks): restore authoritative reply SLA response regression --- frontend/src/components/TasksLayout.test.tsx | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/src/components/TasksLayout.test.tsx b/frontend/src/components/TasksLayout.test.tsx index a0cac6748..fdb53ce03 100644 --- a/frontend/src/components/TasksLayout.test.tsx +++ b/frontend/src/components/TasksLayout.test.tsx @@ -136,4 +136,60 @@ describe("TasksLayout", () => { expect(detailTab?.getAttribute("aria-selected")).toBe("true"); expect(detailTab?.tabIndex).toBe(0); }); + + it("uses the reply SLA mutation response for an existing task id", async () => { + const existingTask = { + id: "task-duplicate-1", + title: "기존 미답변 작업", + status: "open", + priority: "normal", + source_type: "reply_sla", + source_email_id: "mail-1", + related_thread_id: "thread-1", + updated_at: "2026-06-18T05:00:00Z", + }; + const refreshedTask = { + ...existingTask, + title: "미답변 팔로업: 최신 메일 제목", + status: "blocked", + priority: "urgent", + updated_at: "2026-06-18T06:00:00Z", + }; + const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith("/api/tasks")) return Promise.resolve(jsonResponse([existingTask])); + if (url.endsWith("/api/tasks/reply-sla-escalations")) { + expect(init?.method).toBe("POST"); + return Promise.resolve(jsonResponse({ + evaluated: 1, + created: 0, + policy: { overdue_hours: 48 }, + tasks: [refreshedTask], + })); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal("fetch", fetchMock); + + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + await act(async () => { + root?.render(); + }); + await flushAsyncWork(); + + expect(container.textContent).toContain("기존 미답변 작업"); + + await act(async () => { + container?.querySelector('button[aria-label="보낸 메일 미답변 팔로업 작업 생성"]')?.click(); + }); + await flushAsyncWork(); + + expect(container.textContent).toContain("미답변 팔로업: 최신 메일 제목"); + expect(container.textContent).not.toContain("기존 미답변 작업"); + expect(container.textContent).toContain("긴급"); + expect(container.textContent).toContain("검토 필요"); + }); });