Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
52
web/src/test/mockEventSource.ts
Normal file
52
web/src/test/mockEventSource.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export class MockEventSource {
|
||||
static instances: MockEventSource[] = [];
|
||||
url: string;
|
||||
onopen: (() => void) | null = null;
|
||||
onmessage: ((event: { data: string }) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
closed = false;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
MockEventSource.instances.push(this);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
/** Test helper: simulate successful connection */
|
||||
simulateOpen() {
|
||||
this.onopen?.();
|
||||
}
|
||||
|
||||
/** Test helper: push SSE data */
|
||||
simulateMessage(data: string) {
|
||||
this.onmessage?.({ data });
|
||||
}
|
||||
|
||||
/** Test helper: simulate connection error */
|
||||
simulateError() {
|
||||
this.onerror?.();
|
||||
}
|
||||
|
||||
static reset() {
|
||||
MockEventSource.instances = [];
|
||||
}
|
||||
|
||||
static latest(): MockEventSource | undefined {
|
||||
return MockEventSource.instances.at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
export function installMockEventSource() {
|
||||
MockEventSource.reset();
|
||||
vi.stubGlobal("EventSource", MockEventSource);
|
||||
}
|
||||
|
||||
export function uninstallMockEventSource() {
|
||||
vi.unstubAllGlobals();
|
||||
MockEventSource.reset();
|
||||
}
|
||||
57
web/src/test/mockWebSocket.ts
Normal file
57
web/src/test/mockWebSocket.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
type MessageHandler = (event: { data: string }) => void;
|
||||
|
||||
export class MockWebSocket {
|
||||
static instances: MockWebSocket[] = [];
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
|
||||
url: string;
|
||||
readyState = MockWebSocket.CONNECTING;
|
||||
onopen: (() => void) | null = null;
|
||||
onmessage: MessageHandler | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
onclose: (() => void) | null = null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
MockWebSocket.instances.push(this);
|
||||
queueMicrotask(() => {
|
||||
this.readyState = MockWebSocket.OPEN;
|
||||
this.onopen?.();
|
||||
});
|
||||
}
|
||||
|
||||
send = vi.fn();
|
||||
|
||||
close() {
|
||||
this.readyState = MockWebSocket.CLOSED;
|
||||
this.onclose?.();
|
||||
}
|
||||
|
||||
/** Test helper: push a JSON message to listeners */
|
||||
simulateMessage(data: unknown) {
|
||||
this.onmessage?.({ data: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
static reset() {
|
||||
MockWebSocket.instances = [];
|
||||
}
|
||||
|
||||
static latest(): MockWebSocket | undefined {
|
||||
return MockWebSocket.instances.at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
export function installMockWebSocket() {
|
||||
MockWebSocket.reset();
|
||||
vi.stubGlobal("WebSocket", MockWebSocket);
|
||||
}
|
||||
|
||||
export function uninstallMockWebSocket() {
|
||||
vi.unstubAllGlobals();
|
||||
MockWebSocket.reset();
|
||||
}
|
||||
38
web/src/test/mocks/fleet.ts
Normal file
38
web/src/test/mocks/fleet.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { FleetResponse } from "@/types/fleet";
|
||||
|
||||
export const MOCK_API_FLEET: FleetResponse = {
|
||||
totalHashrate: 55_000_000,
|
||||
onlineCount: 2,
|
||||
hosts: [
|
||||
{
|
||||
id: "host-001",
|
||||
hostname: "forge-node-alpha",
|
||||
ip: "10.0.1.12",
|
||||
arch: "linux/amd64",
|
||||
hashrate: 30_000_000,
|
||||
tier: 2,
|
||||
tierName: "Bundled xmrig",
|
||||
tierState: "active",
|
||||
algo: "rx/0",
|
||||
uptimeSec: 7200,
|
||||
lastSeen: new Date().toISOString(),
|
||||
clearance: 2,
|
||||
online: true,
|
||||
},
|
||||
{
|
||||
id: "host-002",
|
||||
hostname: "mesh-worker-beta",
|
||||
ip: "10.0.1.47",
|
||||
arch: "linux/amd64",
|
||||
hashrate: 25_000_000,
|
||||
tier: 3,
|
||||
tierName: "GPU lolMiner",
|
||||
tierState: "probing",
|
||||
algo: "kawpow",
|
||||
uptimeSec: 1800,
|
||||
lastSeen: new Date().toISOString(),
|
||||
clearance: 1,
|
||||
online: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
89
web/src/test/mocks/handlers.ts
Normal file
89
web/src/test/mocks/handlers.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { MOCK_API_FLEET } from "./fleet";
|
||||
|
||||
const MOCK_MINING_PROFILE = {
|
||||
wallet_address: "48edfHu7V9Z3N9z6M7x8K2pL4qR5sT6uV7wX8yZ9aB0cD1eF2gH3jK4mN5oP6qR",
|
||||
tiers: [
|
||||
{ tier: 1, name: "OCI podman", enabled: true },
|
||||
{ tier: 2, name: "Bundled xmrig", enabled: true },
|
||||
{ tier: 3, name: "GPU lolMiner", enabled: true },
|
||||
{ tier: 4, name: "Stratum-direct fallback", enabled: false },
|
||||
],
|
||||
};
|
||||
|
||||
const MOCK_BUILDS = {
|
||||
builds: [
|
||||
{
|
||||
id: "build-001",
|
||||
os: "linux",
|
||||
arch: "amd64",
|
||||
version: "0.1.0-dev",
|
||||
checksum: "abc123def4567890abcdef1234567890",
|
||||
signature: "sig-ed25519",
|
||||
public: true,
|
||||
created_at: "2026-06-07T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "build-002",
|
||||
os: "linux",
|
||||
arch: "arm64",
|
||||
version: "0.1.0-dev",
|
||||
checksum: "fedcba0987654321fedcba0987654321",
|
||||
signature: "sig-ed25519",
|
||||
public: false,
|
||||
created_at: "2026-06-06T18:30:00Z",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const MOCK_DROPPER = {
|
||||
dropper_url: "http://localhost:8989/get",
|
||||
install_url: "http://localhost:8989/install.sh",
|
||||
one_liner:
|
||||
"FORGE_MESH_FLEET_SECRET=change-me-fleet-secret bash <(curl -fsSL http://localhost:8989/install.sh)",
|
||||
fleet_secret: "change-me-fleet-secret",
|
||||
public_key: "abcdef1234567890abcdef1234567890",
|
||||
has_build: true,
|
||||
version: "0.1.0-dev",
|
||||
};
|
||||
|
||||
export const handlers = [
|
||||
http.get("/api/v1/fleet", () => HttpResponse.json(MOCK_API_FLEET)),
|
||||
http.post("/api/v1/ws/ticket", () => HttpResponse.json({ ticket: "test-ws-ticket" })),
|
||||
http.get("/api/v1/dropper", () => HttpResponse.json(MOCK_DROPPER)),
|
||||
http.get("/api/v1/forge/builds", () => HttpResponse.json(MOCK_BUILDS)),
|
||||
http.post("/api/v1/forge/builds/trigger", () => HttpResponse.json({ ok: true, message: "Build queued" })),
|
||||
http.get("/api/v1/policy/mining-profile", () => HttpResponse.json(MOCK_MINING_PROFILE)),
|
||||
http.put("/api/v1/policy/mining-profile", () => HttpResponse.json({ ok: true })),
|
||||
http.put("/api/v1/policy/wallet", () => HttpResponse.json({ ok: true })),
|
||||
http.post("/api/v1/fleet/:hostId/mining-profile", async ({ request, params }) => {
|
||||
const body = (await request.json()) as { wallet_address?: string };
|
||||
return HttpResponse.json({
|
||||
id: `profile-${params.hostId}`,
|
||||
wallet_address: body.wallet_address ?? MOCK_MINING_PROFILE.wallet_address,
|
||||
});
|
||||
}),
|
||||
http.get("/api/v1/crucible/history", () => HttpResponse.json({ jobs: [] })),
|
||||
http.get("/api/v1/operator/me", () => HttpResponse.json({ clearance_level: 4 })),
|
||||
http.post("/api/v1/crucible/batch", async ({ request }) => {
|
||||
const body = (await request.json()) as { command?: string; all?: boolean };
|
||||
if (!body.command) {
|
||||
return HttpResponse.json({ error: "command required" }, { status: 400 });
|
||||
}
|
||||
return HttpResponse.json(
|
||||
{
|
||||
id: "batch-test-001",
|
||||
command: body.command,
|
||||
status: "completed",
|
||||
results: [
|
||||
{
|
||||
host_id: "host-001",
|
||||
hostname: "forge-node-alpha",
|
||||
status: "dispatched",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 202 },
|
||||
);
|
||||
}),
|
||||
];
|
||||
4
web/src/test/mocks/server.ts
Normal file
4
web/src/test/mocks/server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from "msw/node";
|
||||
import { handlers } from "./handlers";
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
20
web/src/test/setupTests.ts
Normal file
20
web/src/test/setupTests.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterAll, afterEach, beforeAll } from "vitest";
|
||||
import { server } from "./mocks/server";
|
||||
|
||||
if (!HTMLElement.prototype.scrollTo) {
|
||||
HTMLElement.prototype.scrollTo = function scrollTo() {
|
||||
/* jsdom polyfill */
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
server.resetHandlers();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
afterAll(() => server.close());
|
||||
47
web/src/test/test-utils.tsx
Normal file
47
web/src/test/test-utils.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { render, type RenderOptions } from "@testing-library/react";
|
||||
import { MemoryRouter, type MemoryRouterProps } from "react-router-dom";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import { setStoredAuth } from "@/lib/auth";
|
||||
|
||||
interface TestProvidersProps {
|
||||
children: ReactNode;
|
||||
routerProps?: MemoryRouterProps;
|
||||
authenticated?: boolean;
|
||||
}
|
||||
|
||||
export function TestProviders({
|
||||
children,
|
||||
routerProps = { initialEntries: ["/dashboard"] },
|
||||
authenticated = true,
|
||||
}: TestProvidersProps) {
|
||||
if (authenticated) {
|
||||
setStoredAuth({ username: "operator", password: "test-pass" });
|
||||
}
|
||||
|
||||
return <MemoryRouter {...routerProps}>{children}</MemoryRouter>;
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: ReactElement,
|
||||
options: RenderOptions & { routerProps?: MemoryRouterProps; authenticated?: boolean } = {},
|
||||
) {
|
||||
const { routerProps, authenticated, ...renderOptions } = options;
|
||||
return render(ui, {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders routerProps={routerProps} authenticated={authenticated}>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
...renderOptions,
|
||||
});
|
||||
}
|
||||
|
||||
export function renderAppRoutes(initialPath: string, authenticated = true) {
|
||||
// Lazy import to avoid circular deps in some test setups
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const App = require("@/App").default as () => ReactElement;
|
||||
return renderWithProviders(<App />, {
|
||||
routerProps: { initialEntries: [initialPath] },
|
||||
authenticated,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user