From 96cc5e78399b97f6e0e10b4e600fb4f526bb3636 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 18 May 2026 08:38:06 +0900 Subject: [PATCH] fix(agent-office): render TaskTab result_data when it's already an object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old code assumed result_data was a JSON string and ran JSON.parse on it, falling back to returning the value verbatim on parse error. When the backend ships result_data as a dict (e.g. compose tasks return {music_task_id, tracks}), JSON.parse threw, the catch returned the raw object, and React threw error #31 'Objects are not valid as a React child' the moment the user expanded the task row. Extract formatResultData helper: object → JSON.stringify, JSON string → parse then pretty-print, plain string → as-is. Regression tests cover all three input shapes. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/agent-office/components/TaskTab.jsx | 21 ++++++-- .../agent-office/components/TaskTab.test.jsx | 53 ++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/pages/agent-office/components/TaskTab.jsx b/src/pages/agent-office/components/TaskTab.jsx index 967920f..2091e8a 100644 --- a/src/pages/agent-office/components/TaskTab.jsx +++ b/src/pages/agent-office/components/TaskTab.jsx @@ -11,6 +11,22 @@ const STATUS_STYLE = { rejected: { bg: '#7f1d1d', fg: '#fca5a5' } }; +// result_data는 백엔드에서 dict 또는 string 둘 다 올 수 있다. +// React child로 직접 못 그리는 객체는 stringify, string은 parse 시도 후 pretty, +// 둘 다 안 되면 원본 문자열을 그대로 표시. +function formatResultData(rd) { + if (rd == null) return ''; + if (typeof rd === 'object') { + try { return JSON.stringify(rd, null, 2); } + catch { return String(rd); } + } + if (typeof rd === 'string') { + try { return JSON.stringify(JSON.parse(rd), null, 2); } + catch { return rd; } + } + return String(rd); +} + function formatTime(ts) { if (!ts) return ''; const d = new Date(ts); @@ -46,10 +62,7 @@ export default function TaskTab({ agentId, refreshTrigger }) { {expanded === task.id && task.result_data && (
-                {(() => {
-                  try { return JSON.stringify(JSON.parse(task.result_data), null, 2); }
-                  catch { return task.result_data; }
-                })()}
+                {formatResultData(task.result_data)}
               
)} diff --git a/src/pages/agent-office/components/TaskTab.test.jsx b/src/pages/agent-office/components/TaskTab.test.jsx index 7d93092..15592e4 100644 --- a/src/pages/agent-office/components/TaskTab.test.jsx +++ b/src/pages/agent-office/components/TaskTab.test.jsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import TaskTab from './TaskTab.jsx'; const mockGetAgentTasks = vi.fn(); @@ -33,4 +33,55 @@ describe('TaskTab response shape handling', () => { render(); await waitFor(() => expect(screen.getByText('No tasks yet')).toBeInTheDocument()); }); + + it('task 클릭 → result_data가 객체일 때도 stringify되어 안전하게 렌더', async () => { + mockGetAgentTasks.mockResolvedValueOnce({ + tasks: [{ + id: 't_compose', + task_type: 'compose', + status: 'succeeded', + created_at: '2026-05-18T08:00:00Z', + result_data: { music_task_id: 'abc-123', tracks: [] }, + }], + }); + render(); + const row = await screen.findByText('compose'); + fireEvent.click(row.closest('.ao-task-item')); + const pre = await screen.findByText(/music_task_id/); + expect(pre.textContent).toContain('"music_task_id": "abc-123"'); + expect(pre.textContent).toContain('"tracks": []'); + }); + + it('task 클릭 → result_data가 JSON 문자열일 때 parse 후 pretty 렌더', async () => { + mockGetAgentTasks.mockResolvedValueOnce({ + tasks: [{ + id: 't_str', + task_type: 'compose', + status: 'succeeded', + created_at: '2026-05-18T08:00:00Z', + result_data: '{"foo":"bar"}', + }], + }); + render(); + const row = await screen.findByText('compose'); + fireEvent.click(row.closest('.ao-task-item')); + const pre = await screen.findByText(/foo/); + expect(pre.textContent).toContain('"foo": "bar"'); + }); + + it('task 클릭 → result_data가 plain string이면 그대로 표시 (parse 실패 fallback)', async () => { + mockGetAgentTasks.mockResolvedValueOnce({ + tasks: [{ + id: 't_plain', + task_type: 'fetch_news', + status: 'succeeded', + created_at: '2026-05-18T08:00:00Z', + result_data: 'Just a log line', + }], + }); + render(); + const row = await screen.findByText('fetch_news'); + fireEvent.click(row.closest('.ao-task-item')); + expect(await screen.findByText('Just a log line')).toBeInTheDocument(); + }); });