/** * @vitest-environment happy-dom */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; import BuilderPage, { formatBytes } from './BuilderPage'; import { ForgeProvider } from '../context/ForgeContext'; import { routerFuture } from '../routerFuture'; import { mockServerConfig, mockServerInfo } from '../test/fixtures'; import { api } from '../api/client'; function renderBuilder(initialEntries = ['/forge']) { return render( ); } function missionWizardScope() { const root = screen.getByText('MISSION RITUAL — 3-STEP WIZARD').closest('.forge-mission-wizard'); if (!root) throw new Error('Mission wizard not found'); return within(root as HTMLElement); } function pathForgeSection() { const root = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section'); if (!root) throw new Error('Path forge section not found'); return within(root as HTMLElement); } function fusionBatchFileInput() { const label = screen.getByText(/Batch fusion — fuse many files at once/i); const formGroup = label.closest('.form-group'); if (!formGroup) throw new Error('Batch fusion form group not found'); const input = formGroup.querySelector('input[type="file"][multiple]') as HTMLInputElement | null; if (!input) throw new Error('Batch fusion file input not found'); return input; } function mockFusionFile(name: string, content = 'payload') { return new File([content], name, { type: 'application/octet-stream' }); } type BuildResult = Awaited>; function successfulBuild(overrides: Partial = {}): BuildResult { return { success: true, file_name: 'worker-1.exe', file_size: 4 * 1024 * 1024, download_url: '/api/v1/builds/test/download', fusion_enabled: false, obfuscated: false, signed: false, ...overrides, }; } describe('formatBytes', () => { it('formats sub-kilobyte values as bytes', () => { expect(formatBytes(512)).toBe('512 B'); }); it('formats kilobytes and megabytes with two decimals', () => { expect(formatBytes(2048)).toBe('2.00 KB'); expect(formatBytes(5 * 1024 * 1024)).toBe('5.00 MB'); }); it('formats gigabytes and terabytes', () => { expect(formatBytes(3 * 1024 * 1024 * 1024)).toBe('3.00 GB'); expect(formatBytes(2 * 1024 * 1024 * 1024 * 1024)).toBe('2.00 TB'); }); }); describe('BuilderPage', () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); vi.spyOn(api, 'getConfig').mockResolvedValue(mockServerConfig()); vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo); vi.spyOn(api, 'listBuilds').mockResolvedValue([]); vi.spyOn(api, 'buildAgent').mockResolvedValue({ success: true, file_name: 'worker-1.exe', file_size: 4 * 1024 * 1024, download_url: '/api/v1/builds/test/download', fusion_enabled: false, obfuscated: false, signed: false, }); vi.spyOn(api, 'listBlueprints').mockResolvedValue([]); vi.spyOn(api, 'saveBlueprint').mockResolvedValue({ name: 'my-blueprint', size: 1024, created_at: '' }); vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true }); }); afterEach(() => { vi.unstubAllGlobals(); cleanup(); }); it('shows loading hero before defaults arrive', () => { renderBuilder(); expect(screen.getByRole('heading', { level: 1, name: 'Forge' })).toBeInTheDocument(); expect(screen.getByText('Loading forge defaults from server...')).toBeInTheDocument(); }); it('renders forge form with identity field labels after load', async () => { renderBuilder(); expect(await screen.findByRole('heading', { level: 2, name: 'Quick Forge' })).toBeInTheDocument(); expect(screen.getByText('INSTALLER FORGE')).toBeInTheDocument(); expect(screen.getByText('Worker Name')).toBeInTheDocument(); expect(screen.getByText('Control Endpoint')).toBeInTheDocument(); expect(screen.getByText('XMR Wallet Address')).toBeInTheDocument(); expect(api.getConfig).toHaveBeenCalled(); expect(api.getServerInfo).toHaveBeenCalled(); expect(api.listBuilds).toHaveBeenCalled(); }); it('surfaces load failure when server info cannot be fetched', async () => { vi.spyOn(api, 'getConfig').mockRejectedValue(new Error('offline')); renderBuilder(); expect( await screen.findByText('Failed to load server config — is the control server running?') ).toBeInTheDocument(); }); it('shows LAN endpoint chips from server info', async () => { renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); expect(screen.getByRole('button', { name: mockServerInfo.suggested_url! })).toBeInTheDocument(); }); it('switches to advanced mode and reveals pool configuration', async () => { renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await userEvent.setup().click(screen.getByRole('button', { name: 'Advanced' })); expect(await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' })).toBeInTheDocument(); expect(screen.getByText('FORGE RULES — READ THIS ONCE')).toBeInTheDocument(); expect(screen.getByText('Mining Pools')).toBeInTheDocument(); }); it('submits forge when preflight passes', async () => { const buildSpy = vi.spyOn(api, 'buildAgent'); renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); await waitFor(() => { expect(buildSpy).toHaveBeenCalled(); }); expect(await screen.findByText('worker-1.exe')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Download/i })).toBeInTheDocument(); }); it('offers forge-next-worker after a successful build', async () => { renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); expect(await screen.findByText('worker-1.exe')).toBeInTheDocument(); await userEvent.setup().click(screen.getByRole('button', { name: 'Forge next worker' })); expect(screen.queryByText('worker-1.exe')).not.toBeInTheDocument(); expect(screen.getByPlaceholderText(/office-pc-1/i)).toHaveValue('worker-2'); }); it('shows fleet blueprint save prompt after first successful forge', async () => { const promptMock = vi.fn().mockReturnValue('fleet-default'); vi.stubGlobal('prompt', promptMock); const saveSpy = vi.spyOn(api, 'saveBlueprint'); renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); expect(await screen.findByText('worker-1.exe')).toBeInTheDocument(); await userEvent.setup().click(screen.getByRole('button', { name: 'Save as fleet blueprint' })); await waitFor(() => { expect(promptMock).toHaveBeenCalledWith('Save this setup as a fleet blueprint:', 'fleet-default'); expect(saveSpy).toHaveBeenCalledWith('fleet-default', expect.any(Object)); }); }); it('blocks forge when preflight has errors (empty wallet)', async () => { renderBuilder(); const wallet = await screen.findByDisplayValue(/^4A+/); fireEvent.change(wallet, { target: { value: '' } }); const forgeBtn = screen.getByRole('button', { name: /FIX .* ERROR/i }); expect(forgeBtn).toBeDisabled(); expect(screen.getByText('PREFLIGHT CROSS-CHECK')).toBeInTheDocument(); }); it('persists simple vs advanced mode in localStorage', async () => { renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await userEvent.setup().click(screen.getByRole('button', { name: 'Advanced' })); expect(localStorage.getItem('aetherforge-forge-mode')).toBe('advanced'); await userEvent.setup().click(screen.getByRole('button', { name: 'Simple' })); expect(localStorage.getItem('aetherforge-forge-mode')).toBe('simple'); }); it('loads blueprint list when Load Blueprint is clicked', async () => { vi.spyOn(api, 'listBlueprints').mockResolvedValue([ { name: 'office-preset', size: 2048, created_at: '2026-05-30T12:00:00Z' }, ]); renderBuilder(); await screen.findByRole('button', { name: /Load Blueprint/i }); await userEvent.setup().click(screen.getByRole('button', { name: /Load Blueprint/i })); expect(await screen.findByText('Saved Blueprints')).toBeInTheDocument(); expect(screen.getByText('office-preset')).toBeInTheDocument(); expect(api.listBlueprints).toHaveBeenCalled(); }); it('shows fusion deliverable hint when Fusion card is selected', async () => { renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await userEvent.setup().click( screen.getByRole('button', { name: /Hide miner in any file/i }) ); expect( screen.getByText(/Drop any file — PDF, video, document/i) ).toBeInTheDocument(); }); it('applies forge skin class from default operation mode', async () => { localStorage.setItem('aetherforge-operation-mode', 'ghost_walk'); localStorage.setItem('aetherforge-forge-theme', 'auto'); const { container } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--ghost'); }); it('honors forge theme override over operation mode', async () => { localStorage.setItem('aetherforge-operation-mode', 'ghost_walk'); localStorage.setItem('aetherforge-forge-theme', 'halloween'); const { container } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--halloween'); expect(container.querySelector('.command-deck')).not.toHaveClass('forge-skin--ghost'); }); it('updates forge skin when operation mode chip is selected', async () => { localStorage.setItem('aetherforge-forge-theme', 'auto'); const { container } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await userEvent.setup().click(screen.getByRole('button', { name: 'Wildfire' })); expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--wildfire'); }); it('applies crucible skin for Crucible Storm mode', async () => { localStorage.setItem('aetherforge-operation-mode', 'crucible_storm'); localStorage.setItem('aetherforge-forge-theme', 'auto'); const { container } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--crucible'); }); it('applies halloween skin for Sigil Mask mode', async () => { localStorage.setItem('aetherforge-operation-mode', 'sigil_mask'); localStorage.setItem('aetherforge-forge-theme', 'auto'); const { container } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--halloween'); }); it('renders mission ritual wizard with three step pills', async () => { renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); const wizard = missionWizardScope(); expect(screen.getByText('MISSION RITUAL — 3-STEP WIZARD')).toBeInTheDocument(); expect(wizard.getByRole('tab', { name: /Mode/i })).toBeInTheDocument(); expect(wizard.getByRole('tab', { name: /Profile/i })).toBeInTheDocument(); expect(wizard.getByRole('tab', { name: /Launch/i })).toBeInTheDocument(); expect(wizard.getByRole('button', { name: 'Ghost' })).toBeInTheDocument(); expect(wizard.getByRole('button', { name: 'Loud' })).toBeInTheDocument(); expect(wizard.getByRole('button', { name: 'Spread' })).toBeInTheDocument(); }); it('advances mission wizard from mode to profile to launch', async () => { const user = userEvent.setup(); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); const wizard = missionWizardScope(); await user.click(wizard.getByRole('button', { name: 'Spread' })); await user.click(wizard.getByRole('button', { name: 'Next →' })); expect(wizard.getByText('Spread profile (optional)')).toBeInTheDocument(); await user.click(wizard.getByRole('button', { name: 'LAN Kindling' })); await user.click(wizard.getByRole('button', { name: 'Next →' })); expect(wizard.getByRole('button', { name: '🚀 Launch Ritual' })).toBeInTheDocument(); expect(wizard.getByLabelText('Campaign slug (?c=)')).toBeInTheDocument(); }); it('runs mission ritual and shows success modal with spread landing link', async () => { vi.spyOn(api, 'buildAgent').mockResolvedValue({ success: true, build_id: 'ritual-build-1', file_name: 'worker-ritual.exe', file_size: 4096, download_url: '/api/v1/builds/ritual-build-1/download', fusion_enabled: false, obfuscated: false, signed: false, }); vi.spyOn(api, 'exportSpreadKit').mockResolvedValue(undefined); const user = userEvent.setup(); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); const wizard = missionWizardScope(); await user.click(wizard.getByRole('button', { name: 'Spread' })); await user.click(wizard.getByRole('button', { name: 'Next →' })); await user.click(wizard.getByRole('button', { name: 'LAN Kindling' })); await user.click(wizard.getByRole('button', { name: 'Next →' })); await user.click(wizard.getByRole('button', { name: '🚀 Launch Ritual' })); expect(await screen.findByText('Mission complete')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /View install commands in Builds/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Open spread landing' })).toHaveAttribute('href', '/spread/'); expect(api.buildAgent).toHaveBeenCalled(); expect(api.exportSpreadKit).toHaveBeenCalled(); }); it('APK mode toggle locks platform to Android arm64 and hides Fusion', async () => { localStorage.setItem('aetherforge-forge-mode', 'advanced'); const user = userEvent.setup(); renderBuilder(); await screen.findByText('Worker Name'); const apkToggle = await screen.findByRole('checkbox', { name: /APK mode/i }); await user.click(apkToggle); expect(screen.getByDisplayValue('Android (arm64)')).toBeInTheDocument(); expect(screen.getByText(/platform=android/i)).toBeInTheDocument(); expect(screen.queryByText('Fusion — Hide miner in any file')).not.toBeInTheDocument(); }); it('polls builder progress endpoint while a forge is running', async () => { type BuildResult = Awaited>; let resolveBuild!: (value: BuildResult) => void; vi.spyOn(api, 'buildAgent').mockReturnValue( new Promise((resolve) => { resolveBuild = resolve; }), ); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Compiling agent', pct: 42 }), }); vi.stubGlobal('fetch', fetchMock); renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); await waitFor( () => { expect(fetchMock).toHaveBeenCalledWith( expect.stringMatching(/\/api\/v1\/builder\/progress\//), expect.objectContaining({ headers: expect.any(Object) }), ); }, { timeout: 3000 }, ); resolveBuild({ success: true, file_name: 'worker-1.exe', file_size: 1024, download_url: '/api/v1/builds/test/download', fusion_enabled: false, obfuscated: false, signed: false, }); await waitFor(() => expect(screen.getByText('worker-1.exe')).toBeInTheDocument()); }); it('calls cancelBuild when Kill Build is clicked during single forge', async () => { type BuildResult = Awaited>; vi.spyOn(api, 'buildAgent').mockReturnValue(new Promise(() => {})); const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true }); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Compiling', pct: 10 }), })); renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); const killBtn = await screen.findByRole('button', { name: /Kill Build/i }); fireEvent.click(killBtn); await waitFor(() => { expect(cancelSpy).toHaveBeenCalled(); }); }); it('submits path forge and shows placement summary', async () => { localStorage.setItem('aetherforge-forge-mode', 'advanced'); const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ success: true, placed: 3, total: 1, skipped: 2, errors: 0, results: [{ source: 'movie.mkv', files: ['movie.bat', 'click_bat_to_unlock_movie'] }], }), }); vi.stubGlobal('fetch', fetchMock); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' }); const pathForgeSection = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section') as HTMLElement; const pathInput = within(pathForgeSection).getByPlaceholderText(/E:\\Movies/i); fireEvent.change(pathInput, { target: { value: 'D:\\Movies' } }); fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })); expect(await screen.findByText(/3 files placed across 1 source files/i)).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith( '/api/v1/builder/path-forge', expect.objectContaining({ method: 'POST' }), ); }); it('shows path forge busy state while seeding', async () => { localStorage.setItem('aetherforge-forge-mode', 'advanced'); let resolveForge!: (value: Response) => void; const fetchMock = vi.fn().mockReturnValue( new Promise((resolve) => { resolveForge = resolve; }), ); vi.stubGlobal('fetch', fetchMock); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' }); const section = pathForgeSection(); fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' }, }); fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })); expect(await screen.findByRole('button', { name: /Seeding/i })).toBeDisabled(); resolveForge({ ok: true, json: async () => ({ success: true, placed: 1, total: 1, errors: 0, results: [] }), } as Response); await waitFor(() => { expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled(); }); }); it('calls cancelBuild on unmount when single forge is in flight', async () => { const capturedTokens: string[] = []; vi.spyOn(api, 'buildAgent').mockImplementation((req) => { capturedTokens.push(req.cancel_token ?? ''); return new Promise(() => {}); }); const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true }); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Compiling', pct: 10 }), })); const { unmount } = renderBuilder(); await screen.findByRole('button', { name: /FORGE INSTALLER/i }); fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); await waitFor(() => expect(api.buildAgent).toHaveBeenCalled()); unmount(); await waitFor(() => { expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]); }); }); it('path forge blocks duplicate launch while seeding (race guard)', async () => { localStorage.setItem('aetherforge-forge-mode', 'advanced'); let resolveForge!: (value: Response) => void; const fetchMock = vi.fn().mockReturnValue( new Promise((resolve) => { resolveForge = resolve; }), ); vi.stubGlobal('fetch', fetchMock); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' }); const section = pathForgeSection(); fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } }); const launchBtn = screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }); fireEvent.click(launchBtn); const seedingBtn = await screen.findByRole('button', { name: /Seeding/i }); expect(seedingBtn).toBeDisabled(); fireEvent.click(seedingBtn); expect(fetchMock).toHaveBeenCalledTimes(1); resolveForge({ ok: true, json: async () => ({ success: true, placed: 1, total: 1, errors: 0, results: [] }), } as Response); await waitFor(() => expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled()); }); it('path forge unmount during seeding does not throw', async () => { localStorage.setItem('aetherforge-forge-mode', 'advanced'); vi.stubGlobal('fetch', vi.fn().mockReturnValue(new Promise(() => {}))); const { unmount } = renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' }); const section = pathForgeSection(); fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } }); fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })); await screen.findByRole('button', { name: /Seeding/i }); expect(() => unmount()).not.toThrow(); }); it('batch forge processes multiple files with per-file preflight and cancel_token', async () => { const user = userEvent.setup(); const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) => successfulBuild({ fusion_enabled: true, bundle_file_name: `${file?.name ?? 'file'}-package.zip`, fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`, }), ); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Packaging', pct: 80 }), })); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: /Hide miner in any file/i })); const alpha = mockFusionFile('alpha.pdf'); const beta = mockFusionFile('beta.mkv'); fireEvent.change(fusionBatchFileInput(), { target: { files: [alpha, beta] } }); expect(await screen.findByText(/2 files queued/i)).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /Forge all 2 files/i })); await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(2)); expect(buildSpy.mock.calls[0][1]?.name).toBe('alpha.pdf'); expect(buildSpy.mock.calls[1][1]?.name).toBe('beta.mkv'); expect(buildSpy.mock.calls[0][0].fusion_enabled).toBe(true); expect(buildSpy.mock.calls[0][0].cancel_token).toBeTruthy(); expect(buildSpy.mock.calls[1][0].cancel_token).toBeTruthy(); expect(await screen.findByText(/Batch forged 2 file/i)).toBeInTheDocument(); expect(screen.getByText(/2\/2 — done/i)).toBeInTheDocument(); }); it('cancels batch forge mid-loop via Cancel Batch and calls cancelBuild', async () => { const user = userEvent.setup(); let resolveFirst!: (value: BuildResult) => void; const capturedTokens: string[] = []; const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation((req) => { capturedTokens.push(req.cancel_token ?? ''); if (capturedTokens.length === 1) { return new Promise((resolve) => { resolveFirst = resolve; }); } return Promise.resolve(successfulBuild({ fusion_enabled: true })); }); const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true }); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Compiling', pct: 35 }), })); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: /Hide miner in any file/i })); fireEvent.change(fusionBatchFileInput(), { target: { files: [mockFusionFile('one.pdf'), mockFusionFile('two.pdf')] }, }); await user.click(screen.getByRole('button', { name: /Forge all 2 files/i })); await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1)); fireEvent.click(await screen.findByRole('button', { name: /Cancel Batch/i })); expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]); resolveFirst(successfulBuild({ fusion_enabled: true, bundle_file_name: 'one-package.zip', fusion_export_dir: 'fusion-deliverables/one.pdf', })); expect(await screen.findByText(/Batch forge cancelled after 1 of 2 file/i)).toBeInTheDocument(); await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1)); expect(screen.getByText(/1\/2 — cancelled/i)).toBeInTheDocument(); }); it('skips per-file build failures and shows skipped counter in batch progress', async () => { const user = userEvent.setup(); const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) => { if (file?.name === 'bad.pdf') { return { success: false, error: 'Payload too large for fusion stub' }; } return successfulBuild({ fusion_enabled: true, bundle_file_name: `${file?.name ?? 'file'}-package.zip`, fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`, }); }); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Packaging', pct: 90 }), })); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: /Hide miner in any file/i })); fireEvent.change(fusionBatchFileInput(), { target: { files: [mockFusionFile('bad.pdf'), mockFusionFile('good.pdf')] }, }); await user.click(screen.getByRole('button', { name: /Forge all 2 files/i })); await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(2)); expect(await screen.findByText(/Batch forged 1 file\(s\) · 1 skipped/i)).toBeInTheDocument(); expect(screen.getByText(/bad\.pdf — Payload too large for fusion stub/i)).toBeInTheDocument(); expect(screen.getByText(/2\/2 — done · 1 skipped/i)).toBeInTheDocument(); expect(screen.getByText(/good\.pdf — Saved → fusion-deliverables\/good\.pdf/i)).toBeInTheDocument(); }); it('skips per-file preflight failures without aborting the batch', async () => { const user = userEvent.setup(); const forgeValidation = await import('../help/forgeValidation'); const { runForgePreflight: realPreflight } = await vi.importActual< typeof import('../help/forgeValidation') >('../help/forgeValidation'); const preflightSpy = vi.spyOn(forgeValidation, 'runForgePreflight').mockImplementation((form, fusion) => { if (form.fusion_media_base_name === 'blocked.pdf') { return [{ id: 'fusion', level: 'error', message: 'Fusion media exceeds size cap' }]; } return realPreflight(form, fusion); }); const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) => successfulBuild({ fusion_enabled: true, bundle_file_name: `${file?.name ?? 'file'}-package.zip`, fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`, }), ); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Packaging', pct: 90 }), })); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: /Hide miner in any file/i })); fireEvent.change(fusionBatchFileInput(), { target: { files: [mockFusionFile('blocked.pdf'), mockFusionFile('allowed.pdf')] }, }); await user.click(screen.getByRole('button', { name: /Forge all 2 files/i })); await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1)); expect(buildSpy.mock.calls[0][1]?.name).toBe('allowed.pdf'); expect(await screen.findByText(/blocked\.pdf — Fusion media exceeds size cap/i)).toBeInTheDocument(); expect(screen.getByText(/2\/2 — done · 1 skipped/i)).toBeInTheDocument(); preflightSpy.mockRestore(); }); it('selects fleet role seeder chip in advanced spread section', async () => { const buildSpy = vi.spyOn(api, 'buildAgent'); const user = userEvent.setup(); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: 'Advanced' })); await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' }); const seederChip = screen.getByRole('button', { name: 'Seeder' }); expect(seederChip).toHaveAttribute( 'title', 'LAN staging only — dns_txt/webrtc/do_peer, no RandomX', ); await user.click(seederChip); expect(seederChip.className).toMatch(/active/); await user.click(screen.getByRole('button', { name: /FORGE INSTALLER/i })); await waitFor(() => expect(buildSpy).toHaveBeenCalled()); expect(buildSpy.mock.calls[0][0].fleet_role).toBe('seeder'); expect(screen.getByText(/Seeder skips mining/i)).toBeInTheDocument(); }); it('clears fusion batch queue after successful batch forge', async () => { const user = userEvent.setup(); vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) => successfulBuild({ fusion_enabled: true, bundle_file_name: `${file?.name ?? 'file'}-package.zip`, fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`, }), ); vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ stage: 'Packaging', pct: 90 }), })); renderBuilder(); await screen.findByRole('heading', { level: 2, name: 'Quick Forge' }); await user.click(screen.getByRole('button', { name: /Hide miner in any file/i })); fireEvent.change(fusionBatchFileInput(), { target: { files: [mockFusionFile('solo.pdf')] }, }); expect(await screen.findByText(/1 file queued/i)).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /Forge all 1 file/i })); await waitFor(() => expect(screen.getByText(/Batch forged 1 file/i)).toBeInTheDocument()); expect(screen.queryByText(/1 file queued/i)).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Cancel Batch/i })).not.toBeInTheDocument(); }); });