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
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
42
server/web/e2e/crucible-lotl.spec.ts
Normal file
42
server/web/e2e/crucible-lotl.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
5
server/web/e2e/global-teardown.ts
Normal file
5
server/web/e2e/global-teardown.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { teardownLiveStubAgent } from './live-stub';
|
||||
|
||||
export default function globalTeardown(): void {
|
||||
teardownLiveStubAgent();
|
||||
}
|
||||
38
server/web/e2e/live-stub.ts
Normal file
38
server/web/e2e/live-stub.ts
Normal 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;
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user