From e6742e06bab33fd1f6f4f0ab9e852f5dc106e5dd Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 18 May 2026 08:34:08 +0900 Subject: [PATCH] fix(agent-office): unwrap {tasks}/{logs} response objects before .map Backend returns {"tasks": [...]} and {"logs": [...]} but TaskTab and LogTab stored the raw object and called .map on it, throwing 'l.map is not a function' the moment a user opened the Tasks or Logs tab. Unwrap via Array.isArray check (also covers theoretical bare-array responses). Regression test for TaskTab covers all three response shapes: wrapped object, bare array, and empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/agent-office/components/LogTab.jsx | 2 +- src/pages/agent-office/components/TaskTab.jsx | 2 +- .../agent-office/components/TaskTab.test.jsx | 36 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/pages/agent-office/components/TaskTab.test.jsx diff --git a/src/pages/agent-office/components/LogTab.jsx b/src/pages/agent-office/components/LogTab.jsx index 447c180..91c8b4e 100644 --- a/src/pages/agent-office/components/LogTab.jsx +++ b/src/pages/agent-office/components/LogTab.jsx @@ -15,7 +15,7 @@ export default function LogTab({ agentId, refreshTrigger }) { useEffect(() => { let cancelled = false; getAgentLogs(agentId, 50).then(data => { - if (!cancelled) setLogs(data || []); + if (!cancelled) setLogs(Array.isArray(data) ? data : (data?.logs || [])); }); return () => { cancelled = true; }; }, [agentId, refreshTrigger]); diff --git a/src/pages/agent-office/components/TaskTab.jsx b/src/pages/agent-office/components/TaskTab.jsx index 3c9412f..967920f 100644 --- a/src/pages/agent-office/components/TaskTab.jsx +++ b/src/pages/agent-office/components/TaskTab.jsx @@ -27,7 +27,7 @@ export default function TaskTab({ agentId, refreshTrigger }) { useEffect(() => { let cancelled = false; getAgentTasks(agentId, 20).then(data => { - if (!cancelled) setTasks(data || []); + if (!cancelled) setTasks(Array.isArray(data) ? data : (data?.tasks || [])); }); return () => { cancelled = true; }; }, [agentId, refreshTrigger]); diff --git a/src/pages/agent-office/components/TaskTab.test.jsx b/src/pages/agent-office/components/TaskTab.test.jsx new file mode 100644 index 0000000..7d93092 --- /dev/null +++ b/src/pages/agent-office/components/TaskTab.test.jsx @@ -0,0 +1,36 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import TaskTab from './TaskTab.jsx'; + +const mockGetAgentTasks = vi.fn(); +vi.mock('../../../api', () => ({ + getAgentTasks: (...args) => mockGetAgentTasks(...args), +})); + +describe('TaskTab response shape handling', () => { + it('백엔드가 {tasks: [...]} 객체로 wrapping해서 응답해도 .map 깨지지 않음', async () => { + mockGetAgentTasks.mockResolvedValueOnce({ + tasks: [ + { id: 't1', task_type: 'compose', status: 'succeeded', created_at: '2026-05-18T08:00:00Z' }, + { id: 't2', task_type: 'fetch_news', status: 'failed', created_at: '2026-05-18T08:05:00Z' }, + ], + }); + render(); + await waitFor(() => expect(screen.getByText('compose')).toBeInTheDocument()); + expect(screen.getByText('fetch_news')).toBeInTheDocument(); + }); + + it('백엔드가 bare array를 반환해도 동작 (backward compat)', async () => { + mockGetAgentTasks.mockResolvedValueOnce([ + { id: 't9', task_type: 'compose', status: 'succeeded', created_at: '2026-05-18T08:00:00Z' }, + ]); + render(); + await waitFor(() => expect(screen.getByText('compose')).toBeInTheDocument()); + }); + + it('응답이 falsy/empty이면 No tasks yet 표시', async () => { + mockGetAgentTasks.mockResolvedValueOnce({ tasks: [] }); + render(); + await waitFor(() => expect(screen.getByText('No tasks yet')).toBeInTheDocument()); + }); +});