From 750954b6d39358f63c576f03981bb0f2b241d931 Mon Sep 17 00:00:00 2001 From: dwebxr Date: Tue, 22 Sep 2026 06:56:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent):=20Agent=20activity=20=E3=81=AE?= =?UTF-8?q?=E7=A2=BA=E5=AE=9A=E5=BE=8C=E3=81=AE=E5=86=8D=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=82=92=2060=20=E7=A7=92=20=E2=86=92=20120=20=E7=A7=92?= =?UTF-8?q?=E3=81=AB=20(=E6=9C=AC=E7=95=AA=E5=AE=9F=E6=B8=AC=E3=81=A7=2060?= =?UTF-8?q?=20=E7=A7=92=E3=81=A7=E3=81=AF=E6=96=B0=E3=81=97=E3=81=84?= =?UTF-8?q?=E8=A1=8C=E3=81=8C=E5=87=BA=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 入金・送金の確定後、履歴を 20 秒おきに 3 回 (60 秒) 取り直していた。本番の実測 (2026-09-22): 支払い tx は確定の 19 秒後・54 秒後にはまだ API に出ず、89 秒後に出た (索引側の遅れに、 CDN の stale-while-revalidate で 1 回ぶん古い応答が返る分が重なる)。60 秒で打ち切ると、 ページを開き直すまで新しい行が出ない。 20 秒おき 6 回 (120 秒) に延ばす。上流 (Etherscan) の呼び出しは CDN (s-maxage=30) が吸収するので、 API 予算への影響は 1 回の確定あたり最大 +2 回程度。可視文言・money-path の変更なし。 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc --- components/agent/AgentActivity.tsx | 10 ++++++++-- tests/components/agent/AgentActivity.test.tsx | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/components/agent/AgentActivity.tsx b/components/agent/AgentActivity.tsx index 80901699..44bd8ef9 100644 --- a/components/agent/AgentActivity.tsx +++ b/components/agent/AgentActivity.tsx @@ -23,6 +23,12 @@ export function AgentActivity(props: Props) { return ; } +// 確定後の再取得。実測 (2026-09-22・本番): 支払い tx は確定の 54 秒後にはまだ API に出ず、89 秒後に出た +// (索引側の遅れ + CDN の stale-while-revalidate で 1 回ぶん古い応答が返る)。60 秒で打ち切ると、 +// 開き直すまで新しい行が出ない。120 秒まで追う。上流の呼び出しは CDN (s-maxage=30) が吸収する。 +const REFRESH_INTERVAL_MS = 20_000; +const REFRESH_ATTEMPTS = 6; + function ActivityForAddress({ address, locale, c, refreshKey, chainId, tokenAddress }: Props & { chainId: number; tokenAddress: string }) { const [filter, setFilter] = useState<'all' | 'in' | 'out'>('all'); const [visibleCount, setVisibleCount] = useState(10); @@ -55,11 +61,11 @@ function ActivityForAddress({ address, locale, c, refreshKey, chainId, tokenAddr const timer = window.setInterval(() => { void refetch(); attempts += 1; - if (attempts === 3) { + if (attempts === REFRESH_ATTEMPTS) { window.clearInterval(timer); setRefreshing(false); } - }, 20_000); + }, REFRESH_INTERVAL_MS); return () => window.clearInterval(timer); }, [refreshKey, refetch, supported]); diff --git a/tests/components/agent/AgentActivity.test.tsx b/tests/components/agent/AgentActivity.test.tsx index 6ed39df4..44823c28 100644 --- a/tests/components/agent/AgentActivity.test.tsx +++ b/tests/components/agent/AgentActivity.test.tsx @@ -236,7 +236,7 @@ describe('AgentActivity', () => { expect(screen.getByText(c.unsupported, { exact: false })).toBeVisible(); expectExplorer('https://amoy.polygonscan.com'); update({ refreshKey: 1 }); - await act(async () => { await vi.advanceTimersByTimeAsync(80_000); }); + await act(async () => { await vi.advanceTimersByTimeAsync(200_000); }); expect(mockFetch).not.toHaveBeenCalled(); expect(screen.queryByText(c.loading)).toBeNull(); expect(screen.queryByText(c.refreshing)).toBeNull(); @@ -291,7 +291,7 @@ describe('AgentActivity', () => { expect(screen.getAllByText('0 JPYC')).toHaveLength(2); }); - it('refetches at 20-second intervals exactly three times after confirmation', async () => { + it('refetches at 20-second intervals exactly six times (120 s) after confirmation', async () => { const { update } = mount(); await screen.findByRole('table'); vi.useFakeTimers(); @@ -306,9 +306,16 @@ describe('AgentActivity', () => { expect(mockFetch).toHaveBeenCalledTimes(3); await act(async () => { await vi.advanceTimersByTimeAsync(20_000); }); expect(mockFetch).toHaveBeenCalledTimes(4); + // 60 秒では打ち切らない (本番実測: 確定の 54 秒後にまだ出ず 89 秒後に出た)。 + expect(screen.getByText(c.refreshing)).toBeVisible(); + await act(async () => { await vi.advanceTimersByTimeAsync(59_999); }); + expect(mockFetch).toHaveBeenCalledTimes(6); + expect(screen.getByText(c.refreshing)).toBeVisible(); + await act(async () => { await vi.advanceTimersByTimeAsync(1); }); + expect(mockFetch).toHaveBeenCalledTimes(7); expect(screen.queryByText(c.refreshing)).toBeNull(); - await act(async () => { await vi.advanceTimersByTimeAsync(60_000); }); - expect(mockFetch).toHaveBeenCalledTimes(4); + await act(async () => { await vi.advanceTimersByTimeAsync(120_000); }); + expect(mockFetch).toHaveBeenCalledTimes(7); expect(mockFetch.mock.calls.every(([url]) => url === `/api/agent/activity?address=${address.toLowerCase()}`)).toBe(true); }); @@ -323,8 +330,8 @@ describe('AgentActivity', () => { else update({ refreshKey: 2 }); await act(async () => { await vi.advanceTimersByTimeAsync(10_000); }); expect(mockFetch).toHaveBeenCalledTimes(change === 'address' ? 2 : 1); - await act(async () => { await vi.advanceTimersByTimeAsync(100_000); }); - expect(mockFetch).toHaveBeenCalledTimes(change === 'address' ? 2 : change === 'confirmation' ? 4 : 1); + await act(async () => { await vi.advanceTimersByTimeAsync(200_000); }); + expect(mockFetch).toHaveBeenCalledTimes(change === 'address' ? 2 : change === 'confirmation' ? 7 : 1); expect(screen.queryByText(c.refreshing)).toBeNull(); });