feat: T1007 System Service Discovery - fixed allowlist probe in posture heartbeat
This commit is contained in:
157
server/web/src/pages/BuilderPage.test.tsx
Normal file
157
server/web/src/pages/BuilderPage.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } 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 { mockServerConfig, mockServerInfo } from '../test/fixtures';
|
||||
import { api } from '../api/client';
|
||||
|
||||
function renderBuilder(initialEntries = ['/forge']) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<ForgeProvider>
|
||||
<BuilderPage />
|
||||
</ForgeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
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(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('shows loading hero before defaults arrive', () => {
|
||||
renderBuilder();
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'The 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 info — 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('Pool Host')).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('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(/Fusion selected — drop your files below and forge/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user