feat: T1016 dns_config probe + server-side drift detection + Crucible DNS DRIFT badge

This commit is contained in:
AetherForge
2026-05-30 23:26:50 -07:00
parent 6704933568
commit d005d5d07c
48 changed files with 4621 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
/**
* @vitest-environment happy-dom
*/
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { authHeaders, clearStoredAuth, getStoredAuth, setStoredAuth } from '../api/auth';
describe('auth session helpers', () => {
@@ -23,4 +23,11 @@ describe('auth session helpers', () => {
clearStoredAuth();
expect(authHeaders()).toEqual({});
});
it('getStoredAuth returns null when sessionStorage throws', () => {
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new Error('blocked');
});
expect(getStoredAuth()).toBeNull();
});
});

View File

@@ -0,0 +1,294 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { api } from './client';
import { clearStoredAuth, setStoredAuth } from './auth';
import { mockAgent, mockServerConfig, mockServerInfo } from '../test/fixtures';
function jsonResponse(data: unknown, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
function textResponse(body: string, status: number) {
return new Response(body, { status });
}
describe('api client', () => {
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
sessionStorage.clear();
fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
function lastFetch(): { url: string; init: RequestInit } {
const [url, init] = fetchMock.mock.calls.at(-1)!;
return { url: url as string, init: init as RequestInit };
}
function expectAuthHeaders(init: RequestInit) {
const headers = init.headers as Record<string, string>;
expect(headers.Authorization).toBe(`Basic ${btoa('user:pass')}`);
}
it('sends JSON Content-Type and auth on listAgents', async () => {
setStoredAuth('user', 'pass');
fetchMock.mockResolvedValueOnce(jsonResponse([mockAgent()]));
const agents = await api.listAgents();
expect(agents).toHaveLength(1);
const { url, init } = lastFetch();
expect(url).toBe('/api/v1/agents');
expect(init.method).toBeUndefined();
expect((init.headers as Record<string, string>)['Content-Type']).toBe('application/json');
expectAuthHeaders(init);
});
it('throws with status and body on API errors', async () => {
fetchMock.mockResolvedValueOnce(textResponse('not found', 404));
await expect(api.getAgent('missing')).rejects.toThrow('API error 404: not found');
expect(lastFetch().url).toBe('/api/v1/agents/missing');
});
it('omits Authorization when logged out', async () => {
clearStoredAuth();
fetchMock.mockResolvedValueOnce(jsonResponse({ status: 'ok' }));
await api.healthCheck();
const headers = lastFetch().init.headers as Record<string, string>;
expect(headers.Authorization).toBeUndefined();
});
it('getAgentStats appends limit query param', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse([]));
await api.getAgentStats('agent-1', 25);
expect(lastFetch().url).toBe('/api/v1/agents/agent-1/stats?limit=25');
});
it('getAgentStats omits limit when undefined', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse([]));
await api.getAgentStats('agent-1');
expect(lastFetch().url).toBe('/api/v1/agents/agent-1/stats');
});
it('getRecentShares appends limit query param', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse([]));
await api.getRecentShares(10);
expect(lastFetch().url).toBe('/api/v1/shares?limit=10');
});
it('updateConfig PUTs JSON body', async () => {
const partial = { server: { dashboard_subtitle: 'test' } };
fetchMock.mockResolvedValueOnce(jsonResponse(mockServerConfig(partial)));
await api.updateConfig(partial);
const { url, init } = lastFetch();
expect(url).toBe('/api/v1/config');
expect(init.method).toBe('PUT');
expect(init.body).toBe(JSON.stringify(partial));
});
it('buildAgent POSTs JSON when fusion disabled', async () => {
const req = { fusion_enabled: false, wallet: '4' + 'A'.repeat(94) } as Parameters<typeof api.buildAgent>[0];
fetchMock.mockResolvedValueOnce(jsonResponse({ build_id: 'b1', success: true }));
await api.buildAgent(req);
const { url, init } = lastFetch();
expect(url).toBe('/api/v1/builder/build');
expect(init.method).toBe('POST');
expect(init.body).toBe(JSON.stringify(req));
});
it('buildAgent rejects fusion without prep file', async () => {
const req = { fusion_enabled: true } as Parameters<typeof api.buildAgent>[0];
await expect(api.buildAgent(req)).rejects.toThrow('Fusion requires prep.exe upload');
expect(fetchMock).not.toHaveBeenCalled();
});
it('buildAgent POSTs multipart when fusion enabled', async () => {
setStoredAuth('user', 'pass');
const req = { fusion_enabled: true, wallet: '4' + 'A'.repeat(94) } as Parameters<typeof api.buildAgent>[0];
const prep = new File(['prep'], 'custom.exe', { type: 'application/octet-stream' });
fetchMock.mockResolvedValueOnce(jsonResponse({ build_id: 'b2', success: true }));
await api.buildAgent(req, prep);
const { url, init } = lastFetch();
expect(url).toBe('/api/v1/builder/build');
expect(init.method).toBe('POST');
expect(init.body).toBeInstanceOf(FormData);
const headers = init.headers as Record<string, string>;
expect(headers.Authorization).toBe(`Basic ${btoa('user:pass')}`);
expect(headers['Content-Type']).toBeUndefined();
const form = init.body as FormData;
expect(form.get('config')).toBe(JSON.stringify(req));
expect(form.get('prep_exe')).toBeInstanceOf(File);
});
it('estimateFusion POSTs multipart with auth', async () => {
setStoredAuth('user', 'pass');
const req = { fusion_enabled: true } as Parameters<typeof api.estimateFusion>[0];
const prep = new File(['prep'], 'prep.exe');
fetchMock.mockResolvedValueOnce(jsonResponse({ estimated_size_mb: 12 }));
await api.estimateFusion(req, prep);
const { url, init } = lastFetch();
expect(url).toBe('/api/v1/builder/estimate');
expect(init.method).toBe('POST');
expectAuthHeaders(init);
});
it('pinBuild, unpinAll, deleteBuild use correct methods and paths', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ ok: true, pinned_id: 'b1' }))
.mockResolvedValueOnce(jsonResponse({ ok: true }))
.mockResolvedValueOnce(jsonResponse({ ok: true, deleted_id: 'b1' }));
await api.pinBuild('b1');
expect(lastFetch().url).toBe('/api/v1/builds/b1/pin');
expect(lastFetch().init.method).toBe('PUT');
await api.unpinAll();
expect(lastFetch().url).toBe('/api/v1/builds/pin');
expect(lastFetch().init.method).toBe('DELETE');
await api.deleteBuild('b1');
expect(lastFetch().url).toBe('/api/v1/builds/b1');
expect(lastFetch().init.method).toBe('DELETE');
});
it('build URL helpers encode paths', () => {
expect(api.buildDownloadUrl('id-1')).toBe('/api/v1/builds/id-1/download');
expect(api.buildArtifactUrl('id-1', 'file with spaces.exe')).toBe(
'/api/v1/builds/id-1/artifact/file%20with%20spaces.exe'
);
expect(api.buildUninstallUrl('id-1')).toBe('/api/v1/builds/id-1/uninstall');
});
it('blueprint CRUD uses encoded names', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse([]))
.mockResolvedValueOnce(jsonResponse({ name: 'preset' }))
.mockResolvedValueOnce(jsonResponse({ success: true, name: 'preset', file_path: '/x', created_at: '' }))
.mockResolvedValueOnce(jsonResponse({ success: true, name: 'preset' }));
await api.listBlueprints();
expect(lastFetch().url).toBe('/api/v1/blueprints');
await api.getBlueprint('my preset');
expect(lastFetch().url).toBe('/api/v1/blueprints/my%20preset');
await api.saveBlueprint('preset', { foo: 1 });
expect(lastFetch().url).toBe('/api/v1/blueprints');
expect(JSON.parse(lastFetch().init.body as string)).toEqual({ name: 'preset', data: { foo: 1 } });
await api.deleteBlueprint('my preset');
expect(lastFetch().url).toBe('/api/v1/blueprints?name=my%20preset');
expect(lastFetch().init.method).toBe('DELETE');
});
it('fleet ops endpoints hit expected paths', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse([]))
.mockResolvedValueOnce(jsonResponse([]))
.mockResolvedValueOnce(jsonResponse([]))
.mockResolvedValueOnce(jsonResponse({ xmr_per_day: 0.01, usd_per_day: 1, network_hashrate: 1 }))
.mockResolvedValueOnce(jsonResponse({ success: true }))
.mockResolvedValueOnce(jsonResponse({ agent_id: 'a1', content: 'log' }))
.mockResolvedValueOnce(jsonResponse({ agent_id: 'a1', content: 'fresh' }))
.mockResolvedValueOnce(jsonResponse({ success: true, agent: mockAgent() }))
.mockResolvedValueOnce(jsonResponse({ success: true, sent: 2, failed: 0, action: 'pause' }));
await api.getAlerts();
expect(lastFetch().url).toBe('/api/v1/alerts');
await api.getPoolStatus();
expect(lastFetch().url).toBe('/api/v1/pools/status');
await api.getAIActivity();
expect(lastFetch().url).toBe('/api/v1/ai/activity');
await api.getEarningsEstimate(1234.5);
expect(lastFetch().url).toBe('/api/v1/earnings/estimate?hashrate=1234.5');
await api.sendAgentCommand('a1', 'pause', { reason: 'test' });
expect(lastFetch().url).toBe('/api/v1/agents/a1/command');
expect(JSON.parse(lastFetch().init.body as string)).toEqual({ action: 'pause', reason: 'test' });
await api.getAgentLog('a1');
expect(lastFetch().url).toBe('/api/v1/agents/a1/log');
await api.getAgentLog('a1', true);
expect(lastFetch().url).toBe('/api/v1/agents/a1/log?refresh=1');
await api.updateAgentMeta('a1', 'notes', ['tag1']);
expect(lastFetch().url).toBe('/api/v1/agents/a1/meta');
expect(JSON.parse(lastFetch().init.body as string)).toEqual({ notes: 'notes', tags: ['tag1'] });
await api.sendBulkCommand(['a1', 'a2'], 'resume');
expect(lastFetch().url).toBe('/api/v1/agents/bulk-command');
expect(JSON.parse(lastFetch().init.body as string)).toEqual({
agent_ids: ['a1', 'a2'],
action: 'resume',
});
});
it('createUser POSTs credentials', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse({ success: true }));
await api.createUser('alice', 'secret');
expect(lastFetch().url).toBe('/api/v1/users');
expect(JSON.parse(lastFetch().init.body as string)).toEqual({ username: 'alice', password: 'secret' });
});
it('getXmrPrice and getServerInfo', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ usd: 200, updated_at: 'now' }))
.mockResolvedValueOnce(jsonResponse(mockServerInfo));
await api.getXmrPrice();
expect(lastFetch().url).toBe('/api/v1/market/xmr');
await api.getServerInfo();
expect(lastFetch().url).toBe('/api/v1/server/info');
});
it('cancelBuild DELETEs encoded token', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse({ cancelled: true }));
await api.cancelBuild('token/with/slash');
expect(lastFetch().url).toBe('/api/v1/builder/cancel/token%2Fwith%2Fslash');
expect(lastFetch().init.method).toBe('DELETE');
});
it('getDashboardStats and listBuilds', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({ total_agents: 1, online_agents: 1, total_hashrate: 100, total_shares: 5 })
)
.mockResolvedValueOnce(jsonResponse([]));
await api.getDashboardStats();
expect(lastFetch().url).toBe('/api/v1/dashboard/stats');
await api.listBuilds();
expect(lastFetch().url).toBe('/api/v1/builds');
});
});

View File

@@ -4,9 +4,14 @@ import { authHeaders } from './auth';
const API_BASE = '/api/v1';
async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
const { headers: extraHeaders, ...rest } = options ?? {};
const res = await fetch(`${API_BASE}${url}`, {
headers: { 'Content-Type': 'application/json', ...authHeaders(), ...(options?.headers as Record<string, string>) },
...options,
...rest,
headers: {
'Content-Type': 'application/json',
...authHeaders(),
...(extraHeaders as Record<string, string> | undefined),
},
});
if (!res.ok) {
const err = await res.text();

View File

@@ -0,0 +1,68 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { downloadAuthedFile, downloadApiFile } from './download';
import { setStoredAuth } from './auth';
describe('downloadAuthedFile', () => {
let fetchMock: ReturnType<typeof vi.fn>;
let clickMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
sessionStorage.clear();
fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
clickMock = vi.fn();
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(clickMock);
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it('normalizes relative paths under /api/v1', async () => {
setStoredAuth('user', 'pass');
fetchMock.mockResolvedValueOnce(new Response(new Blob(['data']), { status: 200 }));
await downloadAuthedFile('/builds/b1/download', 'agent.exe');
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe('/api/v1/builds/b1/download');
expect((init.headers as Record<string, string>).Authorization).toBe(`Basic ${btoa('user:pass')}`);
expect(clickMock).toHaveBeenCalled();
});
it('leaves full /api/v1 paths unchanged', async () => {
fetchMock.mockResolvedValueOnce(new Response(new Blob(['x']), { status: 200 }));
await downloadAuthedFile('/api/v1/builds/b2/uninstall', 'uninstall.bat');
expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/builds/b2/uninstall');
});
it('prefixes bare paths without leading slash', async () => {
fetchMock.mockResolvedValueOnce(new Response(new Blob(['x']), { status: 200 }));
await downloadAuthedFile('builds/b3/artifact/file.exe', 'file.exe');
expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/builds/b3/artifact/file.exe');
});
it('throws server error body on failure', async () => {
fetchMock.mockResolvedValueOnce(new Response('forbidden', { status: 403 }));
await expect(downloadAuthedFile('/builds/x/download', 'x.exe')).rejects.toThrow('forbidden');
});
it('throws status fallback when error body empty', async () => {
fetchMock.mockResolvedValueOnce(new Response('', { status: 500 }));
await expect(downloadAuthedFile('/builds/x/download', 'x.exe')).rejects.toThrow('Download failed (500)');
});
it('downloadApiFile is an alias', () => {
expect(downloadApiFile).toBe(downloadAuthedFile);
});
});

View File

@@ -0,0 +1,795 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { type ReactNode } from 'react';
import { mockAgent, mockServerInfo } from '../test/fixtures';
import { api } from '../api/client';
import { downloadApiFile, downloadAuthedFile } from '../api/download';
import { getStoredAuth } from '../api/auth';
import { useWebSocket } from '../hooks/useWebSocket';
import { DEFAULT_FLEET_FILTERS } from '../help/fleetFilters';
import NeonCard from './NeonCard/NeonCard';
import { HelpTip, FieldHint } from './HelpTip';
import DownloadButton from './DownloadButton';
import AuthDownloadButton from './AuthDownloadButton';
import ErrorBoundary from './ErrorBoundary';
import SessionGate from './SessionGate';
import GaugeRing from './Charts/GaugeRing';
import HashrateChart from './Charts/HashrateChart';
import AgentRemoteActions from './Fleet/AgentRemoteActions';
import AgentListItem from './Fleet/AgentListItem';
import FleetToolbar from './Fleet/FleetToolbar';
import { LanDownloadQR } from './Fleet/LanDownloadQR';
import {
AlertBanner,
PoolStatusPanel,
AIActivityPanel,
EarningsEstimator,
FleetHealthCard,
ContributionBars,
UnderperformerList,
OSArchBreakdown,
LANGroupView,
} from './Fleet/FleetPanels';
import { ForgeLockedHint, ForgeFieldBadge, ForgeSectionHeader } from './Forge/ForgeFieldHints';
import {
PipelineFlow,
FleetPipelineStatus,
ActivityPulse,
ForgeCalibrateCompare,
RoadmapGrid,
} from './Visual/VisualComponents';
import MatrixStreamOverlay from './Visual/MatrixStreamOverlay';
import SystemStatusBar from './Visual/SystemStatusBar';
import FleetTopologyMap from './Visual/3D/FleetTopologyMap';
import Layout from './Layout/Layout';
import AmbientBackground from './Ambient/AmbientBackground';
import CursorFire from './Visual/CursorFire';
import MatrixRain from './Layout/MatrixRain';
vi.mock('../hooks/useWebSocket', () => ({
useWebSocket: vi.fn(),
}));
vi.mock('../context/ForgeContext', () => ({
useForge: vi.fn(() => ({ forging: false, stage: '' })),
}));
vi.mock('../api/download', () => ({
downloadApiFile: vi.fn(),
downloadAuthedFile: vi.fn(),
}));
vi.mock('../api/auth', () => ({
getStoredAuth: vi.fn(),
setStoredAuth: vi.fn(),
}));
vi.mock('qrcode', () => ({
default: {
toCanvas: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock('recharts', async () => {
const actual = await vi.importActual<typeof import('recharts')>('recharts');
return {
...actual,
ResponsiveContainer: ({ children }: { children: ReactNode }) => (
<div data-testid="recharts-responsive">{children}</div>
),
};
});
vi.mock('@react-three/fiber', () => ({
Canvas: ({ children }: { children: ReactNode }) => <div data-testid="three-canvas">{children}</div>,
useFrame: () => {},
}));
vi.mock('@react-three/drei', () => ({
OrbitControls: () => null,
Stars: () => null,
Line: () => null,
Sphere: () => null,
}));
const useWebSocketMock = vi.mocked(useWebSocket);
const downloadApiFileMock = vi.mocked(downloadApiFile);
const downloadAuthedFileMock = vi.mocked(downloadAuthedFile);
const getStoredAuthMock = vi.mocked(getStoredAuth);
function ThrowOnce({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) throw new Error('render boom');
return <span>child ok</span>;
}
describe('NeonCard', () => {
afterEach(() => cleanup());
it('renders children with default brass accent and 3d class', () => {
render(<NeonCard>Inner</NeonCard>);
const card = screen.getByText('Inner').closest('.neon-card');
expect(card).toHaveClass('neon-card-brass', 'neon-card-3d');
expect(card?.querySelector('.neon-card-rim')).toBeTruthy();
});
it('applies accent, hud, tilt3d off, and custom className', () => {
render(
<NeonCard accent="cyan" hud tilt3d={false} className="extra">
X
</NeonCard>
);
const card = screen.getByText('X').closest('.neon-card');
expect(card).toHaveClass('neon-card-cyan', 'hud-corners', 'extra');
expect(card).not.toHaveClass('neon-card-3d');
});
});
describe('HelpTip', () => {
afterEach(() => cleanup());
it('returns null for unknown field', () => {
const { container } = render(<HelpTip field="nonexistent_field_xyz" />);
expect(container.firstChild).toBeNull();
});
it('shows help popup on hover for known field', async () => {
render(<HelpTip field="calibrate_wallet" label="Wallet" />);
expect(screen.getByRole('button', { name: /Help: calibrate_wallet/ })).toBeInTheDocument();
await userEvent.setup().hover(screen.getByRole('button'));
await waitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent(/Monero payout address/i);
});
});
it('FieldHint export is deprecated no-op', () => {
const { container } = render(<FieldHint field="calibrate_wallet" />);
expect(container.firstChild).toBeNull();
});
});
describe('DownloadButton', () => {
afterEach(() => cleanup());
beforeEach(() => {
downloadApiFileMock.mockReset();
});
it('downloads on click and shows busy label', async () => {
downloadApiFileMock.mockImplementation(() => new Promise((r) => setTimeout(r, 50)));
render(
<DownloadButton apiPath="/api/x" filename="a.bin">
Save
</DownloadButton>
);
const btn = screen.getByRole('button', { name: 'Save' });
await userEvent.setup().click(btn);
expect(screen.getByRole('button', { name: 'Downloading…' })).toBeDisabled();
await waitFor(() => expect(downloadApiFileMock).toHaveBeenCalledWith('/api/x', 'a.bin'));
});
it('alerts on download failure', async () => {
downloadApiFileMock.mockRejectedValue(new Error('network down'));
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
render(
<DownloadButton apiPath="/api/x" filename="a.bin">
Save
</DownloadButton>
);
await userEvent.setup().click(screen.getByRole('button'));
await waitFor(() => expect(alertSpy).toHaveBeenCalledWith('network down'));
alertSpy.mockRestore();
});
});
describe('AuthDownloadButton', () => {
afterEach(() => cleanup());
beforeEach(() => {
downloadAuthedFileMock.mockReset();
});
it('calls downloadAuthedFile and shows ellipsis while busy', async () => {
downloadAuthedFileMock.mockImplementation(() => new Promise((r) => setTimeout(r, 30)));
render(
<AuthDownloadButton apiPath="/api/build/1" filename="b.exe">
DL
</AuthDownloadButton>
);
await userEvent.setup().click(screen.getByRole('button', { name: 'DL' }));
expect(screen.getByRole('button', { name: '…' })).toBeDisabled();
await waitFor(() =>
expect(downloadAuthedFileMock).toHaveBeenCalledWith('/api/build/1', 'b.exe')
);
});
});
describe('ErrorBoundary', () => {
afterEach(() => cleanup());
it('renders children when no error', () => {
render(
<ErrorBoundary>
<ThrowOnce shouldThrow={false} />
</ErrorBoundary>
);
expect(screen.getByText('child ok')).toBeInTheDocument();
});
it('shows fallback UI and clears error on retry', async () => {
let throwNow = true;
function MaybeThrow() {
if (throwNow) throw new Error('render boom');
return <span>child ok</span>;
}
render(
<ErrorBoundary>
<MaybeThrow />
</ErrorBoundary>
);
expect(screen.getByRole('heading', { name: 'Something failed to render' })).toBeInTheDocument();
throwNow = false;
await userEvent.setup().click(screen.getByRole('button', { name: 'Retry' }));
expect(screen.getByText('child ok')).toBeInTheDocument();
});
it('uses custom fallback when provided', () => {
render(
<ErrorBoundary fallback={<p>Custom fail</p>}>
<ThrowOnce shouldThrow />
</ErrorBoundary>
);
expect(screen.getByText('Custom fail')).toBeInTheDocument();
});
});
describe('SessionGate', () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
beforeEach(() => {
getStoredAuthMock.mockReturnValue(null);
});
it('shows login form when unauthenticated', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({ ok: false })
);
render(
<SessionGate>
<div>protected</div>
</SessionGate>
);
await waitFor(() => {
expect(screen.getByRole('heading', { name: 'AetherForge' })).toBeInTheDocument();
});
expect(screen.queryByText('protected')).not.toBeInTheDocument();
});
it('renders children when stored auth validates', async () => {
getStoredAuthMock.mockReturnValue('dGVzdA==');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
render(
<SessionGate>
<div>protected</div>
</SessionGate>
);
await waitFor(() => expect(screen.getByText('protected')).toBeInTheDocument());
});
});
describe('GaugeRing', () => {
afterEach(() => cleanup());
it('renders label and percentage for percent-scale max', () => {
render(<GaugeRing value={42} max={100} label="CPU" sublabel="load" />);
expect(screen.getByText('42%')).toBeInTheDocument();
expect(screen.getByText('CPU')).toBeInTheDocument();
expect(screen.getByText('load')).toBeInTheDocument();
});
it('shows raw value in center while ring arc clamps to 0100%', () => {
const { rerender, container } = render(<GaugeRing value={-10} max={100} label="X" />);
expect(screen.getByText('-10%')).toBeInTheDocument();
const fill = container.querySelector('.gauge-ring-fill') as SVGCircleElement;
expect(fill.getAttribute('stroke-dashoffset')).toBe(String(2 * Math.PI * 42));
rerender(<GaugeRing value={200} max={100} label="X" />);
expect(screen.getByText('200%')).toBeInTheDocument();
expect((container.querySelector('.gauge-ring-fill') as SVGCircleElement).getAttribute('stroke-dashoffset')).toBe('0');
});
});
describe('HashrateChart', () => {
afterEach(() => cleanup());
it('shows empty state when data is empty', () => {
render(<HashrateChart data={[]} title="Fleet Hash" />);
expect(screen.getByText('Fleet Hash')).toBeInTheDocument();
expect(screen.getByText(/Awaiting signal from fleet/i)).toBeInTheDocument();
});
it('renders chart with data points', () => {
render(
<HashrateChart
data={[
{ time: '12:00', value: 1500 },
{ time: '12:01', value: 2000 },
]}
title="Live"
/>
);
expect(screen.getByText('Live')).toBeInTheDocument();
expect(screen.getByText('● LIVE')).toBeInTheDocument();
expect(screen.getByTestId('recharts-responsive')).toBeInTheDocument();
});
});
describe('AgentRemoteActions', () => {
afterEach(() => cleanup());
beforeEach(() => {
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
vi.spyOn(api, 'sendAgentCommand').mockResolvedValue({ success: true });
});
it('compact mode disables actions when offline', () => {
render(
<AgentRemoteActions agent={mockAgent({ status: 'offline' })} compact online={false} />
);
expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled();
});
it('compact dispatches pause when online', async () => {
const cmd = vi.spyOn(api, 'sendAgentCommand').mockResolvedValue({ success: true });
render(
<AgentRemoteActions
agent={mockAgent({ id: 'a1', status: 'online' })}
compact
online
/>
);
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
await waitFor(() => expect(cmd).toHaveBeenCalledWith('a1', 'pause', {}));
});
it('full panel shows Target heading and recon section', async () => {
render(<AgentRemoteActions agent={mockAgent({ name: 'Node A' })} online />);
await waitFor(() => {
expect(screen.getByRole('heading', { name: 'Target: Node A' })).toBeInTheDocument();
});
expect(screen.getByRole('heading', { name: 'Recon & Intel' })).toBeInTheDocument();
expect(screen.getByText(/Awaiting telemetry/i)).toBeInTheDocument();
});
it('disables recon buttons when agent offline', async () => {
render(<AgentRemoteActions agent={mockAgent({ status: 'offline' })} online={false} />);
await waitFor(() => {
expect(screen.getByRole('heading', { name: /Target:/i })).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: 'Screenshot' })).toBeDisabled();
});
});
describe('AgentListItem', () => {
afterEach(() => cleanup());
const baseProps = {
agent: mockAgent({ name: 'List Node', tags: ['rack'], notes: 'a'.repeat(90) }),
selected: false,
expanded: false,
onToggleExpand: vi.fn(),
onSelect: vi.fn(),
};
it('renders name, status, and click hint when collapsed', () => {
render(<AgentListItem {...baseProps} />);
expect(screen.getByText('List Node')).toBeInTheDocument();
expect(screen.getByText('online')).toBeInTheDocument();
expect(screen.getByText('click for details')).toBeInTheDocument();
expect(screen.getByText('rack')).toBeInTheDocument();
});
it('truncates long notes preview when collapsed', () => {
render(<AgentListItem {...baseProps} />);
const preview = screen.getByText(/…$/);
expect(preview.textContent!.length).toBeLessThanOrEqual(81);
});
it('expands meta and remote actions', async () => {
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
render(<AgentListItem {...baseProps} expanded selectable checked onCheck={vi.fn()} />);
expect(screen.getByText(/Shares:/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument();
});
it('checkbox stops row navigation', async () => {
const onCheck = vi.fn();
render(<AgentListItem {...baseProps} selectable onCheck={onCheck} />);
await userEvent.setup().click(screen.getByRole('checkbox'));
expect(onCheck).toHaveBeenCalledWith(true);
expect(baseProps.onSelect).not.toHaveBeenCalled();
});
});
describe('FleetToolbar', () => {
afterEach(() => cleanup());
const filters = { ...DEFAULT_FLEET_FILTERS };
const agents = [
mockAgent({ tags: ['prod'], ip: '10.0.0.5' }),
mockAgent({ id: 'a2', name: 'B', tags: ['dev'], ip: '10.0.1.2' }),
];
it('renders search and filter controls', () => {
render(
<FleetToolbar
agents={agents}
filters={filters}
onChange={vi.fn()}
selectedCount={0}
onBulkAction={vi.fn()}
bulkBusy={false}
/>
);
expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument();
expect(screen.getByText('All tags')).toBeInTheDocument();
});
it('shows bulk bar when agents selected', async () => {
const onBulk = vi.fn();
render(
<FleetToolbar
agents={agents}
filters={filters}
onChange={vi.fn()}
selectedCount={2}
onBulkAction={onBulk}
bulkBusy={false}
/>
);
expect(screen.getByText('2 selected')).toBeInTheDocument();
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
expect(onBulk).toHaveBeenCalledWith('pause');
});
it('shows select-all filtered when props provided', () => {
const onSelectAll = vi.fn();
render(
<FleetToolbar
agents={agents}
filters={filters}
onChange={vi.fn()}
selectedCount={0}
onBulkAction={vi.fn()}
bulkBusy={false}
filteredCount={3}
onSelectAllFiltered={onSelectAll}
/>
);
fireEvent.click(screen.getByRole('button', { name: 'Select all filtered (3)' }));
expect(onSelectAll).toHaveBeenCalled();
});
});
describe('LanDownloadQR', () => {
afterEach(() => cleanup());
it('returns null without url', () => {
const { container } = render(<LanDownloadQR url="" />);
expect(container.firstChild).toBeNull();
});
it('renders canvas when url provided', () => {
const { container } = render(<LanDownloadQR url="http://192.168.1.5:8080/get" />);
expect(container.querySelector('canvas')).toBeTruthy();
});
});
describe('FleetPanels', () => {
afterEach(() => cleanup());
it('AlertBanner returns null when empty', () => {
const { container } = render(<AlertBanner alerts={[]} />);
expect(container.firstChild).toBeNull();
});
it('AlertBanner shows up to five alerts', () => {
const alerts = [
{ id: '1', level: 'warning', type: 'offline', message: 'Node down', timestamp: '2026-05-30T12:00:00Z' },
];
render(<AlertBanner alerts={alerts} />);
expect(screen.getByText('OFFLINE')).toBeInTheDocument();
expect(screen.getByText('Node down')).toBeInTheDocument();
});
it('PoolStatusPanel empty hint', () => {
render(<PoolStatusPanel pools={[]} />);
expect(screen.getByRole('heading', { name: /Pool Stratum Status/i })).toBeInTheDocument();
expect(screen.getByText(/No forged pool connections yet/i)).toBeInTheDocument();
});
it('AIActivityPanel maps agent names', () => {
render(
<AIActivityPanel
entries={[
{
agent_id: 'agent-001-uuid',
last_tool: 'pause',
last_success: true,
},
]}
agentNames={{ 'agent-001-uuid': 'Friendly' }}
/>
);
expect(screen.getByText('Friendly')).toBeInTheDocument();
});
it('FleetHealthCard shows score and label', () => {
render(
<FleetHealthCard
health={{
score: 88,
color: 'green',
label: 'NOMINAL',
sentence: 'All good',
issues: [],
}}
/>
);
expect(screen.getByText('NOMINAL')).toBeInTheDocument();
expect(screen.getByText('88')).toBeInTheDocument();
expect(screen.getByText('All good')).toBeInTheDocument();
});
it('ContributionBars returns null when empty', () => {
const { container } = render(<ContributionBars bars={[]} />);
expect(container.firstChild).toBeNull();
});
it('ContributionBars renders rows', () => {
render(
<ContributionBars
bars={[{ id: '1', name: 'A', hashrate: 1000, pct: 50 }]}
xmrPerDay={0.01}
xmrPrice={100}
/>
);
expect(screen.getByRole('heading', { name: /Contribution Map/i })).toBeInTheDocument();
expect(screen.getByText('A')).toBeInTheDocument();
});
it('OSArchBreakdown returns null when empty', () => {
const { container } = render(<OSArchBreakdown platforms={[]} />);
expect(container.firstChild).toBeNull();
});
it('LANGroupView needs at least two groups', () => {
const { container } = render(
<LANGroupView groups={[{ subnet: '10.0.0.0/24', agents: [mockAgent()] }]} />
);
expect(container.firstChild).toBeNull();
});
it('EarningsEstimator returns null when hashrate zero', () => {
const { container } = render(<EarningsEstimator hashrate={0} xmrPrice={100} />);
expect(container.firstChild).toBeNull();
});
it('UnderperformerList restart calls bulk API', async () => {
const agents = [mockAgent({ id: 'u1', name: 'Slow' })];
const bulk = vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({ success: true, sent: 1, failed: 0, action: 'restart' });
render(<UnderperformerList underperformers={agents} medianHashrate={10000} />);
await userEvent.setup().click(screen.getByRole('button', { name: /Restart All/i }));
await waitFor(() => expect(bulk).toHaveBeenCalledWith(['u1'], 'restart'));
expect(await screen.findByText(/Restart sent to 1/i)).toBeInTheDocument();
});
});
describe('ForgeFieldHints', () => {
afterEach(() => cleanup());
it('ForgeLockedHint null without meta', () => {
const { container } = render(<ForgeLockedHint />);
expect(container.firstChild).toBeNull();
});
it('ForgeFieldBadge shows baked label', () => {
render(<ForgeFieldBadge meta={{ badge: 'baked' }} />);
expect(screen.getByText('⛏ baked')).toBeInTheDocument();
});
it('ForgeSectionHeader renders title and badge', () => {
render(
<ForgeSectionHeader title="Pool" description="Pool settings" badge="server-only" />
);
expect(screen.getByRole('heading', { name: 'Pool' })).toBeInTheDocument();
expect(screen.getByText(/server/i)).toBeInTheDocument();
});
});
describe('VisualComponents', () => {
afterEach(() => cleanup());
it('PipelineFlow highlights active step', () => {
render(<PipelineFlow activeStep="forge" />);
expect(document.querySelector('.pipeline-active')).toBeTruthy();
});
it('FleetPipelineStatus shows step labels', () => {
render(
<FleetPipelineStatus
hasBuilds
agentCount={2}
onlineCount={1}
hasHashrate
hasShares={false}
/>
);
expect(screen.getByText('Forged')).toBeInTheDocument();
expect(screen.getByText('Shares')).toBeInTheDocument();
});
it('ActivityPulse empty message', () => {
render(<ActivityPulse items={[]} />);
expect(screen.getByText(/Awaiting fleet activity/i)).toBeInTheDocument();
});
it('ForgeCalibrateCompare links to routes', () => {
render(
<MemoryRouter>
<ForgeCalibrateCompare />
</MemoryRouter>
);
expect(screen.getByText(/Forge — baked into each binary/i)).toBeInTheDocument();
});
it('RoadmapGrid lists features', () => {
render(<RoadmapGrid />);
expect(document.querySelectorAll('.roadmap-card').length).toBeGreaterThan(0);
});
});
describe('MatrixStreamOverlay', () => {
afterEach(() => cleanup());
beforeEach(() => {
useWebSocketMock.mockReturnValue({
isConnected: true,
agents: [],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
});
});
it('returns null when inactive', () => {
const { container } = render(<MatrixStreamOverlay active={false} onClose={vi.fn()} />);
expect(container.firstChild).toBeNull();
});
it('shows overlay heading when active', () => {
render(<MatrixStreamOverlay active onClose={vi.fn()} />);
expect(screen.getByText('RAW_SOCKET_STREAM [ACTIVE]')).toBeInTheDocument();
});
});
describe('SystemStatusBar', () => {
afterEach(() => cleanup());
beforeEach(() => {
vi.spyOn(api, 'healthCheck').mockResolvedValue(undefined);
vi.spyOn(api, 'listAgents').mockResolvedValue([mockAgent(), mockAgent({ id: 'a2', status: 'offline' })]);
vi.spyOn(api, 'listBuilds').mockResolvedValue([{ id: 'b1' } as never]);
});
it('shows server and fleet pills after poll', async () => {
render(
<MemoryRouter>
<SystemStatusBar />
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/SERVER UP/i)).toBeInTheDocument();
});
expect(screen.getByText(/FLEET 1\/2 ONLINE/i)).toBeInTheDocument();
expect(screen.getByText(/1 BUILD/i)).toBeInTheDocument();
});
});
describe('FleetTopologyMap', () => {
afterEach(() => cleanup());
it('renders canvas wrapper for agents', () => {
render(<FleetTopologyMap agents={[mockAgent()]} />);
expect(screen.getByTestId('three-canvas')).toBeInTheDocument();
});
});
describe('AmbientBackground', () => {
afterEach(() => cleanup());
it('renders ambient layers and sacred geometry svg', () => {
const { container } = render(<AmbientBackground />);
expect(container.querySelector('.ambient-bg')).toBeTruthy();
expect(container.querySelector('.ambient-sacred-geo')).toBeTruthy();
});
});
describe('CursorFire', () => {
afterEach(() => cleanup());
it('mounts fullscreen canvas', () => {
const { container } = render(<CursorFire />);
expect(container.querySelector('canvas')).toBeTruthy();
});
});
describe('MatrixRain', () => {
afterEach(() => cleanup());
beforeEach(() => {
useWebSocketMock.mockReturnValue({
isConnected: true,
agents: [],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
});
});
it('renders matrix rain canvas wrapper', () => {
const { container } = render(<MatrixRain />);
expect(container.querySelector('canvas')).toBeTruthy();
});
});
describe('Layout', () => {
afterEach(() => cleanup());
beforeEach(() => {
useWebSocketMock.mockReturnValue({
isConnected: true,
agents: [mockAgent()],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
});
vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo);
});
it('renders nav links and children', async () => {
render(
<MemoryRouter initialEntries={['/dashboard']}>
<Layout>
<div>page body</div>
</Layout>
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText('page body')).toBeInTheDocument();
});
expect(screen.getByRole('link', { name: /Command Deck/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /Fleet Roster/i })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,55 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { act, renderHook } from '@testing-library/react';
import { ForgeProvider, useForge } from './ForgeContext';
describe('ForgeContext', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ForgeProvider>{children}</ForgeProvider>
);
it('starts with idle forge state', () => {
const { result } = renderHook(() => useForge(), { wrapper });
expect(result.current.forging).toBe(false);
expect(result.current.stage).toBe('');
expect(result.current.progress).toBe(0);
});
it('startForge sets initializing state', () => {
const { result } = renderHook(() => useForge(), { wrapper });
act(() => result.current.startForge());
expect(result.current.forging).toBe(true);
expect(result.current.stage).toBe('Initializing forge...');
expect(result.current.progress).toBe(0);
});
it('setStage updates stage and progress while forging', () => {
const { result } = renderHook(() => useForge(), { wrapper });
act(() => {
result.current.startForge();
result.current.setStage('Compiling', 42);
});
expect(result.current.stage).toBe('Compiling');
expect(result.current.progress).toBe(42);
});
it('endForge resets state', () => {
const { result } = renderHook(() => useForge(), { wrapper });
act(() => {
result.current.startForge();
result.current.setStage('Done', 100);
result.current.endForge();
});
expect(result.current.forging).toBe(false);
expect(result.current.stage).toBe('');
expect(result.current.progress).toBe(0);
});
});

View File

@@ -0,0 +1,43 @@
/**
* @vitest-environment happy-dom
*/
import { describe, expect, it } from 'vitest';
import { renderHook } from '@testing-library/react';
import { WebSocketContext, useWebSocketContext } from './WebSocketContext';
import { mockAgent } from '../test/fixtures';
describe('WebSocketContext', () => {
it('useWebSocketContext returns default value outside provider', () => {
const { result } = renderHook(() => useWebSocketContext());
expect(result.current.isConnected).toBe(false);
expect(result.current.agents).toEqual([]);
expect(result.current.commandResults).toEqual([]);
expect(result.current.latestMessage).toBeNull();
});
it('useWebSocketContext reads provider value', () => {
const value = {
isConnected: true,
agents: [mockAgent()],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: { 'agent-001-uuid': 'log line' },
commandResults: [{ agent_id: 'a1', action: 'pause', success: true, _seq: 1 }],
latestMessage: null,
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<WebSocketContext.Provider value={value}>{children}</WebSocketContext.Provider>
);
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
expect(result.current.isConnected).toBe(true);
expect(result.current.agents).toHaveLength(1);
expect(result.current.agentLogs['agent-001-uuid']).toBe('log line');
expect(result.current.commandResults[0]._seq).toBe(1);
});
});

View File

@@ -0,0 +1,180 @@
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, render, renderHook } from '@testing-library/react';
import { WebSocketProvider } from './WebSocketProvider';
import { useWebSocketContext } from './WebSocketContext';
import { useWebSocket } from '../hooks/useWebSocket';
import { setStoredAuth, clearStoredAuth } from '../api/auth';
import { mockAgent, mockShare } from '../test/fixtures';
type WSListener = ((event: { data: string }) => void) | null;
class MockWebSocket {
static instances: MockWebSocket[] = [];
static OPEN = 1;
static CONNECTING = 0;
static CLOSED = 3;
url: string;
readyState = MockWebSocket.CONNECTING;
onopen: (() => void) | null = null;
onclose: (() => void) | null = null;
onerror: (() => void) | null = null;
onmessage: WSListener = null;
constructor(url: string) {
this.url = url;
MockWebSocket.instances.push(this);
}
close() {
this.readyState = MockWebSocket.CLOSED;
this.onclose?.();
}
emitMessage(data: unknown) {
this.onmessage?.({ data: JSON.stringify(data) });
}
emitOpen() {
this.readyState = MockWebSocket.OPEN;
this.onopen?.();
}
}
describe('WebSocketProvider', () => {
beforeEach(() => {
sessionStorage.clear();
MockWebSocket.instances = [];
vi.stubGlobal('WebSocket', MockWebSocket as unknown as typeof WebSocket);
Object.defineProperty(window, 'location', {
value: { protocol: 'http:', host: 'localhost:8080' },
configurable: true,
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
function latestSocket() {
return MockWebSocket.instances.at(-1)!;
}
function wrapper({ children }: { children: React.ReactNode }) {
return <WebSocketProvider>{children}</WebSocketProvider>;
}
it('connects to ws dashboard with auth token query param', () => {
setStoredAuth('drjones', 'secret');
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
const ws = latestSocket();
const token = btoa('drjones:secret');
expect(ws.url).toBe(`ws://localhost:8080/ws/dashboard?token=${encodeURIComponent(token)}`);
act(() => ws.emitOpen());
expect(result.current.isConnected).toBe(true);
});
it('connects without token when logged out', () => {
clearStoredAuth();
renderHook(() => useWebSocketContext(), { wrapper });
expect(latestSocket().url).toBe('ws://localhost:8080/ws/dashboard');
});
it('useWebSocket re-exports context hook', () => {
expect(useWebSocket).toBe(useWebSocketContext);
});
it('handles init and agent_online messages', () => {
const agent = mockAgent({ id: 'live-1' });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({
type: 'init',
payload: { agents: [agent] },
});
});
expect(result.current.agents).toEqual([agent]);
const updated = { ...agent, hashrate_15s: 999 };
act(() =>
latestSocket().emitMessage({
type: 'agent_online',
payload: updated,
})
);
expect(result.current.agents[0].hashrate_15s).toBe(999);
});
it('marks agent offline and caps recent shares', () => {
const agent = mockAgent({ id: 'a-offline' });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({ type: 'init', payload: { agents: [agent] } });
latestSocket().emitMessage({
type: 'agent_offline',
payload: { agent_id: 'a-offline' },
});
});
expect(result.current.agents[0].status).toBe('offline');
act(() => {
for (let i = 0; i < 55; i++) {
latestSocket().emitMessage({
type: 'new_share',
payload: mockShare({ id: i }),
});
}
});
expect(result.current.recentShares.length).toBeLessThanOrEqual(50);
});
it('assigns monotonic _seq on command_result', () => {
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({
type: 'command_result',
payload: { agent_id: 'a1', action: 'pause', success: true },
});
latestSocket().emitMessage({
type: 'command_result',
payload: { agent_id: 'a1', action: 'get_log', success: true, message: 'log data' },
});
});
expect(result.current.commandResults).toHaveLength(2);
expect(result.current.commandResults[0]._seq).toBe(1);
expect(result.current.commandResults[1]._seq).toBe(2);
expect(result.current.agentLogs.a1).toBe('log data');
});
it('schedules reconnect after close', () => {
vi.useFakeTimers();
renderHook(() => useWebSocketContext(), { wrapper });
const first = latestSocket();
act(() => first.close());
expect(MockWebSocket.instances).toHaveLength(1);
act(() => vi.advanceTimersByTime(3000));
expect(MockWebSocket.instances).toHaveLength(2);
vi.useRealTimers();
});
it('closes socket on unmount', () => {
const closeSpy = vi.spyOn(MockWebSocket.prototype, 'close');
const { unmount } = render(<WebSocketProvider><span /></WebSocketProvider>);
unmount();
expect(closeSpy).toHaveBeenCalled();
});
});

View File

@@ -114,6 +114,9 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
(update.shares_accepted ?? a.shares_good)
),
status: 'online' as const,
...(update.dns_servers !== undefined ? { dns_servers: update.dns_servers } : {}),
...(update.dns_search_domains !== undefined ? { dns_search_domains: update.dns_search_domains } : {}),
...(update.dns_drifted !== undefined ? { dns_drifted: update.dns_drifted } : {}),
...(update.cpu_freq_mhz !== undefined ? { cpu_freq_mhz: update.cpu_freq_mhz } : {}),
...(update.cpu_max_mhz !== undefined ? { cpu_max_mhz: update.cpu_max_mhz } : {}),
...(update.cpu_throttle !== undefined ? { cpu_throttle: update.cpu_throttle } : {}),

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest';
import { blueprintDiff, buildRequestFromRecord } from './buildManager';
describe('blueprintDiff', () => {
it('returns empty diff for identical objects', () => {
const base = { a: 1, b: 'two', c: true };
expect(blueprintDiff(base, { ...base })).toEqual([]);
});
it('detects added, removed, and changed keys', () => {
const base = { keep: 1, gone: 'old', tweak: 'a' };
const current = { keep: 1, newKey: true, tweak: 'b' };
const diff = blueprintDiff(base, current);
expect(diff).toEqual([
{ key: 'gone', kind: 'removed', from: 'old' },
{ key: 'newKey', kind: 'added', to: true },
{ key: 'tweak', kind: 'changed', from: 'a', to: 'b' },
]);
});
it('sorts keys alphabetically', () => {
const diff = blueprintDiff({ z: 1 }, { a: 2, m: 3, z: 1 });
expect(diff.map((d) => d.key)).toEqual(['a', 'm']);
});
it('compares nested values via JSON serialization', () => {
const diff = blueprintDiff(
{ nested: { x: 1, y: 2 } },
{ nested: { x: 1, y: 3 } }
);
expect(diff).toEqual([
{ key: 'nested', kind: 'changed', from: { x: 1, y: 2 }, to: { x: 1, y: 3 } },
]);
});
it('treats array order as significant', () => {
const diff = blueprintDiff({ tags: ['a', 'b'] }, { tags: ['b', 'a'] });
expect(diff).toHaveLength(1);
expect(diff[0].kind).toBe('changed');
});
it('handles empty objects', () => {
expect(blueprintDiff({}, {})).toEqual([]);
expect(blueprintDiff({}, { only: 1 })).toEqual([{ key: 'only', kind: 'added', to: 1 }]);
expect(blueprintDiff({ only: 1 }, {})).toEqual([{ key: 'only', kind: 'removed', from: 1 }]);
});
});
describe('buildRequestFromRecord', () => {
const record = {
worker_name: 'worker-a',
server_url: 'https://c2.example.com',
wallet: '4' + 'B'.repeat(94),
threads: 8,
pool_host: 'pool.example.com',
pool_port: 443,
pool_tls: true,
pool_pass: 'secret',
};
it('spreads defaults then overrides with record fields', () => {
const defaults = { threads: 4, stealth_mode: true, extra_flag: false };
const req = buildRequestFromRecord(record, defaults);
expect(req).toMatchObject({
...defaults,
worker_name: record.worker_name,
server_url: record.server_url,
wallet: record.wallet,
threads: record.threads,
pool_host: record.pool_host,
pool_port: record.pool_port,
pool_tls: record.pool_tls,
pool_pass: record.pool_pass,
});
expect(req.threads).toBe(8);
expect(req.stealth_mode).toBe(true);
});
it('record fields win over colliding default keys', () => {
const req = buildRequestFromRecord(record, { worker_name: 'ignored', threads: 1 });
expect(req.worker_name).toBe('worker-a');
expect(req.threads).toBe(8);
});
it('preserves extra default keys not present on record', () => {
const req = buildRequestFromRecord(record, { fusion_enabled: true, ai_model: 'llama3.2' });
expect(req.fusion_enabled).toBe(true);
expect(req.ai_model).toBe('llama3.2');
});
});

View File

@@ -0,0 +1,200 @@
import { describe, expect, it } from 'vitest';
import {
AI_GUIDE,
CHEAT_SECTIONS,
FORGE_VS_CALIBRATE,
FUSION_GUIDE,
NETWORK_GUIDE,
PIPELINE_STEPS,
ROADMAP_FEATURES,
TROUBLESHOOTING,
} from './cheatSheetContent';
function assertSteps(steps: { id: string; title: string; subtitle: string; icon: string; body: string }[]) {
const ids = steps.map((s) => s.id);
expect(new Set(ids).size).toBe(ids.length);
for (const step of steps) {
expect(step.title.trim().length).toBeGreaterThan(0);
expect(step.subtitle.trim().length).toBeGreaterThan(0);
expect(step.icon.trim().length).toBeGreaterThan(0);
expect(step.body.trim().length).toBeGreaterThan(20);
}
}
describe('PIPELINE_STEPS', () => {
it('defines six pipeline stages in workflow order', () => {
expect(PIPELINE_STEPS).toHaveLength(6);
expect(PIPELINE_STEPS.map((s) => s.id)).toEqual([
'calibrate',
'forge',
'buildmgr',
'drop',
'connect',
'mine',
]);
expect(PIPELINE_STEPS.map((s) => s.title)).toEqual([
'Calibrate',
'Forge',
'Build Manager',
'Drop',
'Connect',
'Mine',
]);
});
it('each step has required content fields', () => {
assertSteps(PIPELINE_STEPS);
});
it('routed steps link to primary app pages', () => {
const routed = PIPELINE_STEPS.filter((s) => s.route);
expect(routed.map((s) => s.route)).toEqual([
'/settings',
'/forge',
'/builds',
'/agents',
'/dashboard',
]);
for (const step of routed) {
expect(step.routeLabel?.trim().length).toBeGreaterThan(0);
}
});
it('drop step includes install one-liner example', () => {
const drop = PIPELINE_STEPS.find((s) => s.id === 'drop')!;
expect(drop.code).toContain('install.ps1');
expect(drop.tips?.some((t) => t.includes('install.sh'))).toBe(true);
});
});
describe('FORGE_VS_CALIBRATE', () => {
it('has forge and calibrate sections with titles and item lists', () => {
expect(FORGE_VS_CALIBRATE.forge.title).toMatch(/Forge/i);
expect(FORGE_VS_CALIBRATE.calibrate.title).toMatch(/Calibrate/i);
expect(FORGE_VS_CALIBRATE.forge.items.length).toBeGreaterThan(10);
expect(FORGE_VS_CALIBRATE.calibrate.items.length).toBeGreaterThan(5);
});
it('forge items cover baked-in agent settings', () => {
const joined = FORGE_VS_CALIBRATE.forge.items.join(' ');
expect(joined).toMatch(/C2|server URL/i);
expect(joined).toMatch(/wallet/i);
expect(joined).toMatch(/Fusion/i);
});
it('calibrate items cover server-only settings', () => {
const joined = FORGE_VS_CALIBRATE.calibrate.items.join(' ');
expect(joined).toMatch(/Listen port/i);
expect(joined).toMatch(/retention/i);
});
});
describe('NETWORK_GUIDE', () => {
it('has five network topology steps with unique ids', () => {
expect(NETWORK_GUIDE).toHaveLength(5);
expect(NETWORK_GUIDE.map((s) => s.id)).toEqual(['n1', 'n2', 'n3', 'n4', 'n5']);
assertSteps(NETWORK_GUIDE);
});
});
describe('FUSION_GUIDE', () => {
it('has four fusion workflow steps', () => {
expect(FUSION_GUIDE).toHaveLength(4);
expect(FUSION_GUIDE.map((s) => s.id)).toEqual(['f1', 'f2', 'f3', 'f4']);
assertSteps(FUSION_GUIDE);
});
});
describe('AI_GUIDE', () => {
it('has three AI autonomy steps', () => {
expect(AI_GUIDE).toHaveLength(3);
expect(AI_GUIDE.map((s) => s.id)).toEqual(['a1', 'a2', 'a3']);
assertSteps(AI_GUIDE);
});
it('mentions Ollama and decide loop', () => {
const bodies = AI_GUIDE.map((s) => s.body).join(' ');
expect(bodies).toMatch(/Ollama/i);
expect(bodies).toMatch(/decide/i);
});
});
describe('TROUBLESHOOTING', () => {
it('lists common problems with non-empty fixes', () => {
expect(TROUBLESHOOTING.length).toBeGreaterThanOrEqual(10);
for (const entry of TROUBLESHOOTING) {
expect(entry.problem.trim().length).toBeGreaterThan(10);
expect(entry.fix.trim().length).toBeGreaterThan(20);
}
});
it('covers forge, pool, and dropper failure modes', () => {
const problems = TROUBLESHOOTING.map((t) => t.problem).join(' ');
expect(problems).toMatch(/Forge/i);
expect(problems).toMatch(/hashrate|shares/i);
expect(problems).toMatch(/dropper|PS1/i);
});
it('shares-rejected fix matches wallet validator range', () => {
const entry = TROUBLESHOOTING.find((t) => t.problem.includes('Shares all rejected'))!;
expect(entry.fix).toMatch(/90.*106/);
expect(entry.fix).toMatch(/4 or 8/);
});
});
describe('ROADMAP_FEATURES', () => {
it('entries have priority, title, and description', () => {
expect(ROADMAP_FEATURES.length).toBeGreaterThan(10);
for (const feat of ROADMAP_FEATURES) {
expect(['high', 'medium', 'low']).toContain(feat.priority);
expect(feat.title.trim().length).toBeGreaterThan(3);
expect(feat.desc.trim().length).toBeGreaterThan(10);
}
});
it('includes shipped core features', () => {
const titles = ROADMAP_FEATURES.map((f) => f.title);
expect(titles).toContain('Build Manager full page');
expect(titles).toContain('AI Autonomy (Ollama)');
expect(titles).toContain('Dropper endpoints');
});
});
describe('CHEAT_SECTIONS', () => {
const expectedSections = [
{ id: 'pipeline', title: 'End-to-end pipeline' },
{ id: 'network', title: 'Network topology — Cloudflare tunnel setup' },
{ id: 'fusion', title: 'Fusion workflow' },
{ id: 'ai', title: 'AI Autonomy workflow' },
{ id: 'troubleshoot', title: 'Troubleshooting' },
];
it('registers five guide sections with stable ids and titles', () => {
expect(CHEAT_SECTIONS).toHaveLength(5);
expect(CHEAT_SECTIONS.map((s) => ({ id: s.id, title: s.title }))).toEqual(expectedSections);
});
it('each section has a non-empty description', () => {
for (const section of CHEAT_SECTIONS) {
expect(section.description.trim().length).toBeGreaterThan(20);
}
});
it('step sections reference the exported step arrays', () => {
const byId = Object.fromEntries(CHEAT_SECTIONS.map((s) => [s.id, s]));
expect(byId.pipeline.steps).toBe(PIPELINE_STEPS);
expect(byId.network.steps).toBe(NETWORK_GUIDE);
expect(byId.fusion.steps).toBe(FUSION_GUIDE);
expect(byId.ai.steps).toBe(AI_GUIDE);
});
it('troubleshoot section maps cards from TROUBLESHOOTING', () => {
const troubleshoot = CHEAT_SECTIONS.find((s) => s.id === 'troubleshoot')!;
expect(troubleshoot.cards).toHaveLength(TROUBLESHOOTING.length);
expect(troubleshoot.cards![0]).toEqual({
title: TROUBLESHOOTING[0].problem,
body: TROUBLESHOOTING[0].fix,
accent: 'amber',
});
});
});

View File

@@ -361,7 +361,7 @@ export const TROUBLESHOOTING = [
},
{
problem: 'Shares all rejected',
fix: 'Wallet address is invalid or wrong for the pool. Monero wallet addresses are 95 chars starting with 4. Some pools require exact format — check your pool dashboard. Accept rate updates live once real shares come in.',
fix: 'Wallet address is invalid or wrong for the pool. Monero wallet addresses are 90106 chars starting with 4 or 8. Some pools require exact format — check your pool dashboard. Accept rate updates live once real shares come in.',
},
{
problem: 'Preflight ✕ blocking forge',

View File

@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { FORGE_BUILD_DEFAULTS, forgeDefaultsFromServer } from './forgeDefaults';
import { mockServerConfig, mockServerInfo } from '../test/fixtures';
describe('FORGE_BUILD_DEFAULTS', () => {
it('sets safe production defaults for a new forge form', () => {
expect(FORGE_BUILD_DEFAULTS.threads).toBe(4);
expect(FORGE_BUILD_DEFAULTS.thread_mode).toBe('percent');
expect(FORGE_BUILD_DEFAULTS.stealth_mode).toBe(true);
expect(FORGE_BUILD_DEFAULTS.persistence).toBe(true);
expect(FORGE_BUILD_DEFAULTS.fusion_enabled).toBe(false);
expect(FORGE_BUILD_DEFAULTS.ai_enabled).toBe(false);
expect(FORGE_BUILD_DEFAULTS.auto_spread).toBe(false);
expect(FORGE_BUILD_DEFAULTS.remote_aggressive).toBe(false);
});
it('omits per-build identity fields (filled by server merge)', () => {
const keys = Object.keys(FORGE_BUILD_DEFAULTS);
expect(keys).not.toContain('worker_name');
expect(keys).not.toContain('server_url');
expect(keys).not.toContain('wallet');
expect(keys).not.toContain('pool_host');
expect(keys).not.toContain('pool_port');
expect(keys).not.toContain('pool_tls');
expect(keys).not.toContain('pool_pass');
});
});
describe('forgeDefaultsFromServer', () => {
it('merges server config pool/wallet with build defaults', () => {
const config = mockServerConfig();
const result = forgeDefaultsFromServer(config, mockServerInfo);
expect(result.wallet).toBe(config.wallet.address);
expect(result.pool_host).toBe(config.pool.host);
expect(result.pool_port).toBe(config.pool.port);
expect(result.pool_tls).toBe(config.pool.use_tls);
expect(result.pool_pass).toBe('x');
expect(result.worker_name).toBe('');
expect(result.stealth_mode).toBe(FORGE_BUILD_DEFAULTS.stealth_mode);
});
it('prefers trimmed public_url over suggested_url', () => {
const config = mockServerConfig({
server: { public_url: ' https://tunnel.example.com ' },
});
const result = forgeDefaultsFromServer(config, mockServerInfo);
expect(result.server_url).toBe('https://tunnel.example.com');
});
it('falls back to suggested_url when public_url is blank', () => {
const config = mockServerConfig({
server: { public_url: ' ' },
});
const result = forgeDefaultsFromServer(config, mockServerInfo);
expect(result.server_url).toBe(mockServerInfo.suggested_url);
});
it('reflects obfuscate and sign defaults from server config', () => {
const config = mockServerConfig({
server: { obfuscate_default: true, sign_enabled: true },
});
const result = forgeDefaultsFromServer(config, mockServerInfo);
expect(result.obfuscate).toBe(true);
expect(result.sign_build).toBe(true);
});
it('uses custom pool password when configured', () => {
const config = mockServerConfig({
pool: { password: 'worker-pass' },
});
const result = forgeDefaultsFromServer(config, mockServerInfo);
expect(result.pool_pass).toBe('worker-pass');
});
});

View File

@@ -1,5 +1,9 @@
import { describe, it, expect } from 'vitest';
import { AGGRESSIVE_REMOTE_ACTIONS, canRunAggressiveAction } from './aggressiveActions';
import {
AGGRESSIVE_REMOTE_ACTIONS,
aggressiveActionHint,
canRunAggressiveAction,
} from './aggressiveActions';
/** Buttons in AgentRemoteActions (full + compact) — must match agent/client handleCommand. */
const UI_REMOTE_ACTIONS = [
@@ -71,3 +75,69 @@ describe('remote action wiring', () => {
expect(canRunAggressiveAction('defender_off', caps, 'windows')).toBe(true);
});
});
describe('AGGRESSIVE_REMOTE_ACTIONS', () => {
it('lists every wired aggressive command once', () => {
expect(AGGRESSIVE_REMOTE_ACTIONS).toHaveLength(9);
expect(new Set(AGGRESSIVE_REMOTE_ACTIONS).size).toBe(9);
});
});
const fullCaps = {
hole_punch: true,
remote_aggressive: true,
mesh_p2p: true,
auto_spread: true,
process_hollowing: false,
ai_enabled: false,
};
describe('canRunAggressiveAction edge cases', () => {
it('allows all actions when caps are undefined (legacy agents)', () => {
for (const action of AGGRESSIVE_REMOTE_ACTIONS) {
if (action === 'defender_off') continue;
expect(canRunAggressiveAction(action, undefined, 'windows')).toBe(true);
}
});
it('spread_now requires auto_spread or remote_aggressive', () => {
const base = { ...fullCaps, auto_spread: false, remote_aggressive: false };
expect(canRunAggressiveAction('spread_now', base)).toBe(false);
expect(canRunAggressiveAction('spread_now', { ...base, auto_spread: true })).toBe(true);
expect(canRunAggressiveAction('spread_now', { ...base, remote_aggressive: true })).toBe(true);
});
it('mesh_status requires mesh_p2p capability', () => {
expect(canRunAggressiveAction('mesh_status', { ...fullCaps, mesh_p2p: false })).toBe(false);
expect(canRunAggressiveAction('mesh_status', fullCaps)).toBe(true);
});
it('remote aggressive ops gate tunnel, scan, defender, firewall', () => {
const noAgg = { ...fullCaps, remote_aggressive: false };
for (const action of ['start_tunnel', 'subnet_scan', 'defender_off', 'firewall_punch'] as const) {
expect(canRunAggressiveAction(action, noAgg, 'windows')).toBe(false);
expect(canRunAggressiveAction(action, fullCaps, 'windows')).toBe(true);
}
});
});
describe('aggressiveActionHint', () => {
it('returns undefined when action is allowed', () => {
expect(aggressiveActionHint('hole_punch', fullCaps)).toBeUndefined();
expect(aggressiveActionHint('spread_now', fullCaps)).toBeUndefined();
});
it('returns macOS-specific hint for defender_off', () => {
expect(aggressiveActionHint('defender_off', fullCaps, 'darwin')).toBe(
'Defender disable not supported on macOS'
);
});
it('suggests re-forge hints when capability missing', () => {
const noCaps = { ...fullCaps, hole_punch: false, auto_spread: false, remote_aggressive: false, mesh_p2p: false };
expect(aggressiveActionHint('hole_punch', noCaps)).toContain('NAT Hole Punch');
expect(aggressiveActionHint('spread_now', noCaps)).toContain('Auto-Spread');
expect(aggressiveActionHint('mesh_status', noCaps)).toContain('Mesh P2P');
expect(aggressiveActionHint('start_tunnel', noCaps)).toContain('Remote Aggressive Ops');
});
});

View File

@@ -13,6 +13,10 @@ vi.mock('../hooks/useWebSocket', () => ({
useWebSocket: vi.fn(),
}));
vi.mock('../components/Fleet/AgentRemoteActions', () => ({
default: () => <div data-testid="agent-remote-actions-mock" />,
}));
const useWebSocketMock = vi.mocked(useWebSocket);
function wsValue(overrides: Partial<ReturnType<typeof useWebSocket>> = {}) {

View File

@@ -226,6 +226,38 @@
50% { opacity: 0.45; }
}
/* ── DNS row (T1016) ──────────────────────────────────────────────────────── */
.cn-dns-row {
font-size: 0.6rem;
font-family: var(--font-tech);
color: #666;
margin-top: 4px;
padding: 2px 0;
letter-spacing: 0.04em;
display: flex;
align-items: center;
gap: 4px;
}
.cn-dns-row.dns-drifted { color: var(--neon-amber); }
.cn-dns-icon { opacity: 0.4; font-size: 0.55rem; }
.dns-drift-flag { color: var(--neon-amber); font-weight: 700; margin-left: 4px; }
/* ── DNS DRIFT badge ──────────────────────────────────────────────────────── */
.cn-dns {
font-size: 0.62rem;
font-family: var(--font-tech);
padding: 1px 4px;
border-radius: 3px;
letter-spacing: 0.04em;
cursor: default;
}
.cn-dns.dns-drift {
color: var(--neon-amber);
background: rgba(255,176,32,0.14);
font-weight: 700;
animation: rb-blink 1.2s step-end infinite;
}
/* ── Resource pressure badges ─────────────────────────────────────────────── */
.cn-thermal, .cn-disk, .cn-throttle {
font-size: 0.62rem;

View File

@@ -78,6 +78,14 @@ function postureTooltip(agent: Agent): string {
if (agent.reboot_pending !== undefined) {
lines.push(`Reboot required: ${agent.reboot_pending ? 'YES ⚠' : 'no ✓'}`);
}
// DNS config
if (agent.dns_servers?.length) {
lines.push('──────────────────────');
lines.push(`DNS (T1016): ${agent.dns_servers.join(', ')}`);
if (agent.dns_search_domains?.length) lines.push(`Search: ${agent.dns_search_domains.join(', ')}`);
if (agent.dns_drifted) lines.push('⚠ DNS changed since last heartbeat!');
}
// Resource pressure
const tempLabel = agent.gpu_temp_c !== undefined ? `GPU ${agent.gpu_temp_c}°C` : agent.cpu_temp_c !== undefined ? `CPU ${agent.cpu_temp_c}°C` : null;
if (tempLabel || agent.disk_free_pct !== undefined || agent.cpu_throttle !== undefined) {
@@ -146,6 +154,13 @@ function throttleBadge(agent: Agent): { label: string; cls: string } | null {
return { label, cls: 'therm-warm' };
}
// ── DNS helpers (T1016) ────────────────────────────────────────────────────
function dnsBadge(agent: Agent): { label: string; cls: string } | null {
if (agent.dns_drifted) return { label: 'DNS DRIFT', cls: 'dns-drift' };
return null;
}
// ── Service helpers (T1007) ────────────────────────────────────────────────
// Human-readable label for well-known service names
@@ -575,7 +590,21 @@ export default function CruciblePage() {
title={`CPU running at ${a.cpu_freq_mhz ?? '?'} MHz (max ${a.cpu_max_mhz ?? '?'} MHz)`}
>{trb.label}</div>
); })()}
{(() => { const db2 = dnsBadge(a); return db2 && (
<div
className={`cn-dns ${db2.cls}`}
title={`DNS changed since last heartbeat!\nCurrent: ${a.dns_servers?.join(', ') ?? '?'}`}
>{db2.label}</div>
); })()}
</div>
{a.dns_servers && a.dns_servers.length > 0 && (
<div className={`cn-dns-row${a.dns_drifted ? ' dns-drifted' : ''}`}
title={`DNS: ${a.dns_servers.join(', ')}${a.dns_search_domains?.length ? ' Search: ' + a.dns_search_domains.join(', ') : ''}`}>
<span className="cn-dns-icon"></span>
{a.dns_servers.slice(0, 2).join(' · ')}
{a.dns_drifted && <span className="dns-drift-flag"> DRIFT</span>}
</div>
)}
{a.services && a.services.length > 0 && (
<div className="cn-services">
{importantServices(a.services).map(svc => (

View File

@@ -24,6 +24,11 @@ export interface Agent {
arch?: string;
os_version?: string;
capabilities?: AgentCapabilities;
// DNS config — T1016
dns_servers?: string[];
dns_search_domains?: string[];
dns_drifted?: boolean;
// Resource pressure
cpu_freq_mhz?: number;
cpu_max_mhz?: number;

View File

@@ -19,6 +19,11 @@ export interface WSStatsUpdate {
uptime_seconds?: number;
shares_submitted?: number;
shares_accepted?: number;
// DNS config — T1016
dns_servers?: string[];
dns_search_domains?: string[];
dns_drifted?: boolean;
// Resource pressure
cpu_freq_mhz?: number;
cpu_max_mhz?: number;

View File

@@ -7,7 +7,10 @@ export default defineConfig({
setupFiles: ['src/test/setup.ts'],
environmentMatchGlobs: [
['src/api/**', 'happy-dom'],
['src/context/**', 'happy-dom'],
['src/hooks/**', 'happy-dom'],
['src/pages/**', 'happy-dom'],
['src/components/**', 'happy-dom'],
],
},
});