Add phenotype cloning, failure atlas, AI court session, and clearance L0-L4
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 02:41:54 -07:00
parent 4b94776432
commit d9f36f182c
47 changed files with 4859 additions and 94 deletions

View File

@@ -1,40 +1,16 @@
import { expect, test } from '@playwright/test';
import { fetchFleetSecret, loginToDashboard } from './fixtures';
import {
connectStubAgent,
E2E_STUB_AGENT_HOSTNAME,
E2E_STUB_AGENT_ID,
} from './stub-agent';
const baseURL = process.env.AETHERFORGE_URL || 'http://127.0.0.1:8989';
let serverReady = false;
let disconnectStub: (() => void) | null = null;
import { loginToDashboard } from './fixtures';
import { ensureLiveStubAgent, isLiveStubReady } from './live-stub';
import { E2E_STUB_AGENT_HOSTNAME, E2E_STUB_AGENT_ID } from './stub-agent';
test.describe('Crucible bulk command', () => {
test.beforeAll(async ({ request }) => {
try {
const res = await request.get('/api/v1/health', { timeout: 5_000 });
serverReady = res.ok();
} catch {
serverReady = false;
}
if (!serverReady) return;
const fleetSecret = await fetchFleetSecret(request);
disconnectStub = await connectStubAgent(baseURL, fleetSecret);
// Allow agent_online + DB upsert to settle before UI tests.
await new Promise((r) => setTimeout(r, 500));
});
test.afterAll(() => {
disconnectStub?.();
disconnectStub = null;
await ensureLiveStubAgent(request);
});
test.beforeEach(async ({ page }) => {
test.skip(
!serverReady,
!isLiveStubReady(),
'Requires live E2E server (test-suite phase 8 on :18989 or AETHERFORGE_URL)',
);
await loginToDashboard(page);
@@ -49,8 +25,12 @@ test.describe('Crucible bulk command', () => {
const card = page.locator('.crucible-node-card').filter({ hasText: E2E_STUB_AGENT_HOSTNAME });
await expect(card.locator('.cn-status-dot.on')).toBeVisible({ timeout: 15_000 });
await card.click();
await expect(page.getByText(/1 selected/)).toBeVisible();
await expect(page.getByText(new RegExp(`→ 1 node.*${E2E_STUB_AGENT_HOSTNAME}`))).toBeVisible();
await expect(page.locator('.fleet-bulk-bar').getByText(/1 selected/)).toBeVisible({
timeout: 10_000,
});
await expect(
page.locator('.crucible-actions-card').getByText(new RegExp(`${E2E_STUB_AGENT_HOSTNAME}`)),
).toBeVisible();
const bulkRequest = page.waitForRequest(
(req) =>

View File

@@ -1,41 +1,16 @@
import { expect, test } from '@playwright/test';
import { fetchFleetSecret, loginToDashboard } from './fixtures';
import {
connectStubAgent,
E2E_STUB_AGENT_HOSTNAME,
E2E_STUB_LOTL_BADGE,
E2E_WHOAMI_RESPONSE,
} from './stub-agent';
const baseURL = process.env.AETHERFORGE_URL || 'http://127.0.0.1:8989';
let serverReady = false;
let disconnectStub: (() => void) | null = null;
import { loginToDashboard } from './fixtures';
import { ensureLiveStubAgent, isLiveStubReady } from './live-stub';
import { E2E_STUB_AGENT_HOSTNAME, E2E_WHOAMI_RESPONSE } from './stub-agent';
test.describe('Crucible remote command', () => {
test.beforeAll(async ({ request }) => {
try {
const res = await request.get('/api/v1/health');
serverReady = res.ok();
} catch {
serverReady = false;
}
if (!serverReady) return;
const fleetSecret = await fetchFleetSecret(request);
disconnectStub = await connectStubAgent(baseURL, fleetSecret);
// Allow agent_online, stats_batch (250ms coalesce), and DB upsert to settle.
await new Promise((r) => setTimeout(r, 1_500));
});
test.afterAll(() => {
disconnectStub?.();
disconnectStub = null;
await ensureLiveStubAgent(request);
});
test.beforeEach(async ({ page }) => {
test.skip(
!serverReady,
!isLiveStubReady(),
'Requires live E2E server (test-suite phase 8 on :18989 or AETHERFORGE_URL)',
);
await loginToDashboard(page);
@@ -46,20 +21,20 @@ test.describe('Crucible remote command', () => {
test('whoami on selected online node shows terminal output', async ({ page }) => {
await page.getByText(E2E_STUB_AGENT_HOSTNAME).click();
await expect(page.getByText(new RegExp(`→ 1 node.*${E2E_STUB_AGENT_HOSTNAME}`))).toBeVisible();
await expect(
page.locator('.crucible-actions-card').getByText(new RegExp(`${E2E_STUB_AGENT_HOSTNAME}`)),
).toBeVisible();
await page.getByRole('button', { name: 'CMD', exact: true }).click();
await page.getByRole('button', { name: 'whoami' }).click();
const input = page.locator('.crucible-term-input');
await input.fill('whoami');
await page.getByRole('button', { name: 'SEND' }).click();
const terminal = page.locator('.crucible-terminal');
await expect(terminal.getByText('whoami', { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(terminal.getByText(E2E_WHOAMI_RESPONSE)).toBeVisible({ timeout: 15_000 });
});
test('shows LOTL tier badge when stub sends lotl_tier', async ({ page }) => {
const card = page.locator('.crucible-node-card').filter({ hasText: E2E_STUB_AGENT_HOSTNAME });
await expect(card.getByText(E2E_STUB_LOTL_BADGE)).toBeVisible({ timeout: 15_000 });
});
test('exec echo via master terminal shows output', async ({ page }) => {
await page.getByText(E2E_STUB_AGENT_HOSTNAME).click();
await page.getByRole('button', { name: 'CMD', exact: true }).click();

View File

@@ -0,0 +1,42 @@
import { expect, test } from '@playwright/test';
import { loginToDashboard } from './fixtures';
import { ensureLiveStubAgent, isLiveStubReady } from './live-stub';
import { E2E_STUB_AGENT_HOSTNAME, E2E_STUB_LOTL_BADGE } from './stub-agent';
test.describe('Crucible LOTL', () => {
test.beforeAll(async ({ request }) => {
await ensureLiveStubAgent(request);
});
test.beforeEach(async ({ page }) => {
test.skip(
!isLiveStubReady(),
'Requires live E2E server (test-suite phase 8 on :18989 or AETHERFORGE_URL)',
);
await loginToDashboard(page);
});
test('Crucible node card shows LOTL tier badge from stub stats', async ({ page }) => {
await page.getByRole('link', { name: /Crucible/i }).click();
await expect(page.getByRole('heading', { name: 'Crucible' })).toBeVisible({ timeout: 10_000 });
const card = page.locator('.crucible-node-card').filter({ hasText: E2E_STUB_AGENT_HOSTNAME });
await expect(card).toBeVisible({ timeout: 15_000 });
await expect(card.getByText(E2E_STUB_LOTL_BADGE)).toBeVisible({ timeout: 15_000 });
});
test('Onion timeline shows stub agent tier progression', async ({ page }) => {
await page.getByRole('navigation').getByRole('link', { name: 'Onion', exact: true }).click();
await expect(page.getByRole('heading', { name: 'LOTL Timeline' })).toBeVisible({
timeout: 10_000,
});
await expect(
page.locator('.lotl-fleet-chip').filter({ hasText: E2E_STUB_AGENT_HOSTNAME }),
).toBeVisible({ timeout: 15_000 });
await expect(page.getByText('ONION TIER CHAIN')).toBeVisible();
await expect(page.locator('.lotl-tier-timeline')).toBeVisible();
// Stub lotl_attempts: container (aliases to docker) failed — spread onion timeline.
await expect(page.locator('.lotl-tier-step--failed', { hasText: /Docker/i })).toBeVisible({
timeout: 15_000,
});
});
});

View File

@@ -15,14 +15,52 @@ export function e2eAuthHeaders(): Record<string, string> {
};
}
/** Reads fleet_secret from live server config (generated on first server start). */
/**
* Reads fleet_secret for stub agent auth.
* Prefer AETHERFORGE_FLEET_SECRET (test-suite.ps1 seeds from data/config.json).
*/
export async function fetchFleetSecret(request: APIRequestContext): Promise<string> {
const res = await request.get('/api/v1/config', { headers: e2eAuthHeaders() });
const fromEnv = process.env.AETHERFORGE_FLEET_SECRET?.trim();
if (fromEnv) return fromEnv;
const res = await request.get('/api/v1/config', {
headers: e2eAuthHeaders(),
timeout: 10_000,
});
if (!res.ok()) {
throw new Error(`config fetch failed: ${res.status()}`);
}
const body = (await res.json()) as { server?: { fleet_secret?: string } };
return body.server?.fleet_secret ?? '';
const secret = body.server?.fleet_secret?.trim() ?? '';
if (!secret) {
throw new Error(
'fleet_secret missing — set AETHERFORGE_FLEET_SECRET from data/config.json in the E2E runner',
);
}
return secret;
}
/** Poll /api/v1/health until status ok or timeout (mirrors test-suite.ps1 phase 8). */
export async function waitForServerHealth(
request: APIRequestContext,
opts?: { timeoutMs?: number; intervalMs?: number },
): Promise<boolean> {
const timeoutMs = opts?.timeoutMs ?? 30_000;
const intervalMs = opts?.intervalMs ?? 1_000;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await request.get('/api/v1/health', { timeout: 2_000 });
if (res.ok()) {
const body = (await res.json()) as { status?: string };
if (body.status === 'ok') return true;
}
} catch {
/* retry */
}
await new Promise((r) => setTimeout(r, intervalMs));
}
return false;
}
export async function loginToDashboard(page: Page): Promise<void> {
@@ -31,5 +69,5 @@ export async function loginToDashboard(page: Page): Promise<void> {
await page.getByLabel('Username').fill(E2E_USER);
await page.getByLabel('Password').fill(E2E_PASS);
await page.getByRole('button', { name: /enter command deck/i }).click();
await expect(page.getByRole('heading', { name: 'Command Deck' })).toBeVisible({ timeout: 15_000 });
await expect(page.getByRole('heading', { name: 'Command Deck' })).toBeVisible({ timeout: 20_000 });
}

View File

@@ -0,0 +1,5 @@
import { teardownLiveStubAgent } from './live-stub';
export default function globalTeardown(): void {
teardownLiveStubAgent();
}

View File

@@ -0,0 +1,38 @@
import type { APIRequestContext } from '@playwright/test';
import { fetchFleetSecret, waitForServerHealth } from './fixtures';
import { connectStubAgent } from './stub-agent';
const baseURL = process.env.AETHERFORGE_URL || 'http://127.0.0.1:8989';
let serverReady = false;
let disconnectStub: (() => void) | null = null;
let connectPromise: Promise<boolean> | null = null;
/** One stub agent per Playwright worker (avoids parallel auth races on the same agent_id). */
export async function ensureLiveStubAgent(request: APIRequestContext): Promise<boolean> {
if (disconnectStub) return serverReady;
if (!connectPromise) {
connectPromise = (async () => {
serverReady = await waitForServerHealth(request);
if (!serverReady) return false;
const fleetSecret = await fetchFleetSecret(request);
disconnectStub = await connectStubAgent(baseURL, fleetSecret);
// Allow agent_online, stats_batch (250ms coalesce), and DB upsert to settle.
await new Promise((r) => setTimeout(r, 2_500));
return true;
})();
}
return connectPromise;
}
export function isLiveStubReady(): boolean {
return serverReady;
}
export function teardownLiveStubAgent(): void {
disconnectStub?.();
disconnectStub = null;
connectPromise = null;
serverReady = false;
}

View File

@@ -29,6 +29,19 @@ test.describe('Page smoke', () => {
await expect(page.getByRole('button', { name: 'Save Calibration' })).toBeVisible();
});
test('Settings shows Calibration Control mode toggle', async ({ page }) => {
await page.getByRole('navigation').getByRole('link', { name: 'Calibrate', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Calibrate' })).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('group', { name: 'Calibration control mode' })).toBeVisible({
timeout: 10_000,
});
await expect(page.getByRole('button', { name: /Logic gates/i })).toBeVisible();
await expect(page.getByRole('button', { name: /AI Control/i })).toBeVisible();
await page.getByRole('button', { name: /AI Control/i }).click();
await expect(page.getByPlaceholderText('http://127.0.0.1:11434/v1')).toBeVisible();
await expect(page.getByRole('button', { name: 'Refresh models' })).toBeVisible();
});
test('Builder renders The Forge', async ({ page }) => {
await page.getByRole('link', { name: /Forge/i }).click();
await expect(page.getByRole('heading', { name: 'The Forge' })).toBeVisible({ timeout: 10_000 });

View File

@@ -99,18 +99,25 @@ export async function connectStubAgent(
});
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('stub agent auth timeout')), 10_000);
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(String(ev.data)) as HubMessage;
const timer = setTimeout(() => reject(new Error('stub agent auth timeout')), 30_000);
const onMessage = (ev: MessageEvent) => {
let msg: HubMessage;
try {
msg = JSON.parse(String(ev.data)) as HubMessage;
} catch {
return;
}
if (msg.type !== 'auth_response') return;
clearTimeout(timer);
ws.removeEventListener('message', onMessage);
const body = parsePayload(msg.payload);
if (body.success !== true) {
reject(new Error(`stub agent auth rejected: ${JSON.stringify(body)}`));
return;
}
resolve();
}, { once: true });
};
ws.addEventListener('message', onMessage);
});
sendStubStats(ws);