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) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ export default function LogTab({ agentId, refreshTrigger }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
getAgentLogs(agentId, 50).then(data => {
|
getAgentLogs(agentId, 50).then(data => {
|
||||||
if (!cancelled) setLogs(data || []);
|
if (!cancelled) setLogs(Array.isArray(data) ? data : (data?.logs || []));
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [agentId, refreshTrigger]);
|
}, [agentId, refreshTrigger]);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function TaskTab({ agentId, refreshTrigger }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
getAgentTasks(agentId, 20).then(data => {
|
getAgentTasks(agentId, 20).then(data => {
|
||||||
if (!cancelled) setTasks(data || []);
|
if (!cancelled) setTasks(Array.isArray(data) ? data : (data?.tasks || []));
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [agentId, refreshTrigger]);
|
}, [agentId, refreshTrigger]);
|
||||||
|
|||||||
36
src/pages/agent-office/components/TaskTab.test.jsx
Normal file
36
src/pages/agent-office/components/TaskTab.test.jsx
Normal file
@@ -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(<TaskTab agentId="music" refreshTrigger={0} />);
|
||||||
|
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(<TaskTab agentId="music" refreshTrigger={0} />);
|
||||||
|
await waitFor(() => expect(screen.getByText('compose')).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('응답이 falsy/empty이면 No tasks yet 표시', async () => {
|
||||||
|
mockGetAgentTasks.mockResolvedValueOnce({ tasks: [] });
|
||||||
|
render(<TaskTab agentId="music" refreshTrigger={0} />);
|
||||||
|
await waitFor(() => expect(screen.getByText('No tasks yet')).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user