import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { LocalTrainingPanel } from './LocalTrainingPanel'; beforeEach(() => { localStorage.clear(); sessionStorage.clear(); vi.unstubAllGlobals(); }); describe('LocalTrainingPanel', () => { it('连接本地服务并从图形界面发起训练请求', async () => { const health = { version: '0.1.0', ready: true, trainerRoot: '/opt/unitree_rl_mjlab', python: '/env/bin/python', tasks: ['Unitree-Go2-Flat'], }; const job = { id: 'a'.repeat(32), state: 'queued', taskId: 'Unitree-Go2-Flat', createdAt: '2025-01-01T00:00:00Z', iteration: 0, maxIterations: 2000, progress: 0, message: '等待启动', logs: [], artifactReady: false, }; const fetchMock = vi .fn() .mockResolvedValueOnce( new Response(JSON.stringify(health), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ presets: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify(job), { status: 202, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify(health), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ presets: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) .mockResolvedValueOnce( new Response(JSON.stringify({ error: '训练任务不存在或服务已重启' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); render(); fireEvent.change(screen.getByLabelText('训练服务访问令牌'), { target: { value: 'secret-token' }, }); fireEvent.click(screen.getByRole('button', { name: '连接' })); expect(await screen.findByText('/opt/unitree_rl_mjlab')).toBeInTheDocument(); const open = vi.spyOn(window, 'open').mockImplementation(() => null); fireEvent.click(screen.getByRole('button', { name: '打开自调参 Agent 工作台' })); expect(open).toHaveBeenCalledWith(expect.any(URL), '_blank'); expect(String(open.mock.calls[0][0])).toContain('tuning.html'); expect(String(open.mock.calls[0][0])).not.toContain('secret-token'); fireEvent.change(screen.getByLabelText('并行环境'), { target: { value: '32' } }); fireEvent.click(screen.getByRole('button', { name: '发起本地训练' })); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(3)); const request = fetchMock.mock.calls[2][1] as RequestInit; expect(JSON.parse(String(request.body))).toMatchObject({ taskId: 'Unitree-Go2-Flat', numEnvs: 32, device: 'gpu', gpuIds: [0], wandbMode: 'offline', }); expect(new Headers(request.headers).get('Authorization')).toBe('Bearer secret-token'); expect(await screen.findByText('排队中')).toBeInTheDocument(); const tokenInput = screen.getByLabelText('训练服务访问令牌'); expect(tokenInput).toBeEnabled(); fireEvent.change(tokenInput, { target: { value: 'new-secret-token' } }); fireEvent.click(screen.getByRole('button', { name: '连接' })); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(6)); expect(await screen.findByRole('button', { name: '发起本地训练' })).toBeInTheDocument(); expect(sessionStorage.getItem('mujoco-local-training-token')).toBe('new-secret-token'); }); });