Add full test suite with unit, integration, and E2E smoke tests.

Introduce test.bat orchestrating Go/Vitest/Playwright phases, expand coverage across server, agent, and dashboard, and document in tests/README.md.
This commit is contained in:
drjones
2026-05-29 10:04:52 -07:00
parent f9e26bb1a6
commit e55f11a657
23 changed files with 1052 additions and 12 deletions

View File

@@ -0,0 +1,29 @@
import { expect, test } from '@playwright/test';
test.describe('AetherForge smoke', () => {
test('health endpoint responds', async ({ request }) => {
const res = await request.get('/api/v1/health');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.status).toBe('ok');
});
test('login gate renders and accepts credentials', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'AetherForge' })).toBeVisible({ timeout: 15_000 });
await page.getByLabel('Username').fill('drjones');
await page.getByLabel('Password').fill('czapiewski');
await page.getByRole('button', { name: /enter command deck/i }).click();
await expect(page.getByRole('heading', { name: 'Command Deck' })).toBeVisible({ timeout: 15_000 });
});
test('forge page loads after login', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Username').fill('drjones');
await page.getByLabel('Password').fill('czapiewski');
await page.getByRole('button', { name: /enter command deck/i }).click();
await expect(page.getByRole('heading', { name: 'Command Deck' })).toBeVisible({ timeout: 15_000 });
await page.getByRole('link', { name: /forge/i }).click();
await expect(page.getByRole('heading', { name: 'The Forge' })).toBeVisible({ timeout: 10_000 });
});
});

View File

@@ -20,9 +20,11 @@
"three": "^0.170.0"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"happy-dom": "^15.11.7",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^2.1.9"
@@ -784,6 +786,22 @@
"three": ">= 0.159.0"
}
},
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-spring/animated": {
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
@@ -2243,6 +2261,19 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -2393,6 +2424,21 @@
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
"license": "MIT"
},
"node_modules/happy-dom": {
"version": "15.11.7",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz",
"integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^4.5.0",
"webidl-conversions": "^7.0.0",
"whatwg-mimetype": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/hls.js": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
@@ -2722,6 +2768,53 @@
"dev": true,
"license": "ISC"
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -3607,6 +3700,26 @@
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -7,7 +7,9 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run"
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@react-three/drei": "^9.114.0",
@@ -22,9 +24,11 @@
"three": "^0.170.0"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react": "^4.2.0",
"happy-dom": "^15.11.7",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vitest": "^2.1.9"

View File

@@ -0,0 +1,12 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 60_000,
retries: 0,
use: {
baseURL: process.env.AETHERFORGE_URL || 'http://127.0.0.1:8989',
trace: 'on-first-retry',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});

View File

@@ -0,0 +1,26 @@
/**
* @vitest-environment happy-dom
*/
import { beforeEach, describe, expect, it } from 'vitest';
import { authHeaders, clearStoredAuth, getStoredAuth, setStoredAuth } from '../api/auth';
describe('auth session helpers', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('stores and retrieves basic token', () => {
setStoredAuth('drjones', 'secret');
expect(getStoredAuth()).toBe(btoa('drjones:secret'));
});
it('builds Authorization header when logged in', () => {
setStoredAuth('user', 'pass');
expect(authHeaders()).toEqual({ Authorization: `Basic ${btoa('user:pass')}` });
});
it('returns empty headers when logged out', () => {
clearStoredAuth();
expect(authHeaders()).toEqual({});
});
});

View File

@@ -57,10 +57,11 @@ export default function SessionGate({ children }: { children: ReactNode }) {
<form className="session-gate-card card" onSubmit={handleLogin}>
<h1 className="font-display">AetherForge</h1>
<p className="form-hint">Sign in to open the command deck.</p>
<label className="label">Username</label>
<input className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
<label className="label">Password</label>
<label className="label" htmlFor="session-user">Username</label>
<input id="session-user" className="input" value={user} onChange={(e) => setUser(e.target.value)} autoComplete="username" />
<label className="label" htmlFor="session-pass">Password</label>
<input
id="session-pass"
className="input"
type="password"
value={pass}

View File

@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { preflightHasErrors, runForgePreflight } from './forgeValidation';
import type { BuildRequest } from '../types';
const baseForm = (): BuildRequest => ({
worker_name: 'pc-lab-1',
server_url: 'http://192.168.1.10:8989',
wallet: '4' + 'A'.repeat(94),
pool_host: 'pool.example.com',
pool_port: 3333,
pool_tls: false,
pool_pass: 'x',
threads: 4,
thread_mode: 'percent',
thread_percent: 75,
cpu_priority: 'normal',
mining_mode: 'always',
display_mode: 'normal',
silent_mode: false,
run_as: 'current',
auto_start: true,
persistence: false,
process_name: 'msedgewebview2',
max_cpu_usage_pct: 80,
max_memory_percent: 80,
min_free_ram_mb: 512,
idle_threshold_pct: 10,
idle_duration_minutes: 5,
schedule_start: '',
schedule_end: '',
install_base: 'appdata',
install_custom_base: '',
install_relative_path: '',
adapt_to_hardware: true,
self_healing: true,
file_logging: false,
stealth_mode: false,
firewall_exclusion: false,
fusion_enabled: false,
fusion_run_order: 'parallel',
fusion_output_name: 'prep.exe',
ai_enabled: false,
ai_ollama_endpoint: '',
ai_model: '',
output_dir: '',
});
describe('runForgePreflight', () => {
it('passes valid LAN forge form', () => {
const checks = runForgePreflight(baseForm(), false);
expect(preflightHasErrors(checks)).toBe(false);
});
it('errors on localhost server URL', () => {
const form = { ...baseForm(), server_url: 'http://localhost:8989' };
const checks = runForgePreflight(form, false);
expect(preflightHasErrors(checks)).toBe(true);
expect(checks.some((c) => c.id === 'server' && c.level === 'error')).toBe(true);
});
it('errors when fusion enabled without prep', () => {
const form = { ...baseForm(), fusion_enabled: true };
const checks = runForgePreflight(form, false);
expect(preflightHasErrors(checks)).toBe(true);
expect(checks.some((c) => c.id === 'fusion')).toBe(true);
});
it('errors on traversal output_dir', () => {
const form = { ...baseForm(), output_dir: '../../etc' };
const checks = runForgePreflight(form, false);
expect(preflightHasErrors(checks)).toBe(true);
});
});

View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import {
defaultEmbeddedName,
defaultRunnerName,
fusionTitleFromFilename,
isFusionVideoFile,
} from './fusionMedia';
describe('fusionMedia', () => {
it('detects video extensions', () => {
expect(isFusionVideoFile({ name: 'Vacation.mkv' } as File)).toBe(true);
expect(isFusionVideoFile({ name: 'prep.exe' } as File)).toBe(false);
expect(isFusionVideoFile(null)).toBe(false);
});
it('derives title from filename', () => {
expect(fusionTitleFromFilename('C:\\movies\\Vacation.mkv')).toBe('Vacation');
expect(fusionTitleFromFilename('clip.MP4')).toBe('clip');
});
it('builds default runner and embedded names', () => {
expect(defaultRunnerName('Vacation.mkv')).toBe('Vacation-runner.exe');
expect(defaultEmbeddedName('Vacation.mkv')).toBe('Vacation.mkv.exe');
});
});

View File

@@ -4,5 +4,8 @@ export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
environmentMatchGlobs: [
['src/api/**', 'happy-dom'],
],
},
});