/**
* @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();
});
});