import { act, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MemoryRouter, Outlet } from 'react-router-dom'; import App from '@/App'; import { useAuthStore } from '@/stores/auth'; import { useProviderStore } from '@/stores/providers'; import { useSettingsStore } from '@/stores/settings'; import { useUserSyncStore } from '@/stores/user-sync'; import { codingWorkspaceStore } from '@/stores/coding-workspace'; import { useProjectConfigStore } from '@/stores/project-config'; import { createCodingProjectConfigV2 } from '@electron/coding-projects/project-config'; vi.mock('@/components/layout/MainLayout', () => ({ MainLayout: () => (
), })); vi.mock('@/pages/AiHardware', () => ({ AiHardware: () =>
Robot workspace
, })); vi.mock('@/pages/ImageCanvas', () => ({ ImageCanvas: () =>
Canvas workspace
, })); vi.mock('@/pages/Chat', () => ({ Chat: () =>
Programming workspace
, })); vi.mock('@/pages/ModuleSelection', () => ({ ModuleSelection: () =>
Module chooser
, })); vi.mock('@/pages/Login', () => ({ Login: () =>
Login surface
, })); vi.mock('@/pages/Settings', () => ({ Settings: () =>
Global settings
, })); describe('App programming provider initialization gate', () => { const initProviders = vi.fn(); const project = { id: 'local-project', name: 'Local project', createdAt: '2026-09-06T00:00:00.000Z', updatedAt: '2026-09-06T00:00:00.000Z', lastOpenedAt: '2026-09-06T00:00:00.000Z', }; beforeEach(() => { vi.clearAllMocks(); useSettingsStore.setState({ setupComplete: true, init: vi.fn(), }); useProviderStore.setState({ init: initProviders }); codingWorkspaceStore.setState({ activeProjectId: project.id, activeProject: project, load: vi.fn().mockResolvedValue(undefined), }); useProjectConfigStore.setState({ load: vi.fn().mockResolvedValue({ status: 'valid', config: { ...createCodingProjectConfigV2(), initialized: false }, knowledgeFiles: [], }), }); useAuthStore.setState({ initialized: true, accessToken: 'access-token', expiresAt: Date.now() + 60_000, user: { username: 'robot-tester', userId: '1', tenantId: null, deptId: null, authorities: [], }, moduleAccess: { programming: true, design: true, robot: true, }, init: vi.fn(), }); useUserSyncStore.setState({ bootstrap: vi.fn().mockResolvedValue(undefined) }); }); async function renderAt(pathname: string) { render( , ); await waitFor(() => expect(useSettingsStore.getState().init).toHaveBeenCalled()); } it.each(['/ai-hardware', '/image-canvas', '/module-select'])( 'does not initialize programming providers at %s', async (pathname) => { await renderAt(pathname); expect(initProviders).not.toHaveBeenCalled(); }, ); it('initializes programming providers at the explicit programming route', async () => { await renderAt('/chat'); await waitFor(() => expect(initProviders).toHaveBeenCalledTimes(1)); }); it('opens a valid project without requiring the legacy initialized flag', async () => { await renderAt('/chat'); expect(await screen.findByText('Programming workspace')).toBeInTheDocument(); expect(screen.queryByText('项目尚未初始化')).not.toBeInTheDocument(); }); it('redirects the retired inspiration route to the Canvas workspace', async () => { await renderAt('/image-prompts/example'); expect(await screen.findByText('Canvas workspace')).toBeInTheDocument(); expect(screen.queryByText('Module chooser')).not.toBeInTheDocument(); expect(initProviders).not.toHaveBeenCalled(); }); it('redirects an orphaned token without a user identity away from a direct workspace route', async () => { useAuthStore.setState({ initialized: true, accessToken: 'orphaned-access-token', expiresAt: Date.now() + 60_000, user: null, }); await renderAt('/chat'); expect(await screen.findByTestId('login-surface')).toBeInTheDocument(); expect(screen.queryByTestId('main-layout')).not.toBeInTheDocument(); expect(initProviders).not.toHaveBeenCalled(); }); it('waits for the startup module policy before initializing programming providers', async () => { useAuthStore.setState({ initialized: false, moduleAccess: { programming: true, design: true, robot: true, }, }); await renderAt('/chat'); expect(initProviders).not.toHaveBeenCalled(); act(() => { useAuthStore.setState({ initialized: true, moduleAccess: { programming: false, design: true, robot: true, }, }); }); expect(await screen.findByText('Module chooser')).toBeInTheDocument(); expect(initProviders).not.toHaveBeenCalled(); }); it.each([ ['/chat', 'programming'], ['/workbench/project-1', 'programming'], ['/chat', 'programming'], ['/image-canvas', 'design'], ['/image-prompts/example', 'design'], ['/ai-hardware', 'robot'], ['/ai-hardware/device-1', 'robot'], ] as const)('redirects disabled %s routes before mounting their module', async (pathname, accessKey) => { useAuthStore.setState({ moduleAccess: { programming: true, design: true, robot: true, [accessKey]: false, }, }); await renderAt(pathname); expect(await screen.findByText('Module chooser')).toBeInTheDocument(); expect(screen.queryByTestId('main-layout')).not.toBeInTheDocument(); if (accessKey === 'programming') { expect(initProviders).not.toHaveBeenCalled(); } }); it.each(['/learning', '/learning/project/project-1'])( 'redirects retired Learning route %s to the module chooser', async (pathname) => { await renderAt(pathname); expect(await screen.findByText('Module chooser')).toBeInTheDocument(); expect(screen.queryByTestId('main-layout')).not.toBeInTheDocument(); expect(initProviders).not.toHaveBeenCalled(); }, ); it('keeps global settings available when Programming is disabled', async () => { useAuthStore.setState({ moduleAccess: { programming: false, design: true, robot: true, }, }); await renderAt('/settings'); expect(await screen.findByText('Global settings')).toBeInTheDocument(); expect(screen.queryByText('Module chooser')).not.toBeInTheDocument(); expect(screen.getByTestId('main-layout')).toBeInTheDocument(); await waitFor(() => expect(initProviders).toHaveBeenCalledTimes(1)); }); });