quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
113 lines
11 KiB
TypeScript
113 lines
11 KiB
TypeScript
import { act, Profiler, useState, type ReactNode } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import type { ExplainResponse } from '@evofw/shared'
|
|
import { AgentPortRuleForm, AgentPortImportForm } from '@/components/agents/agent-port-acl-forms'
|
|
import { portAclDraftSchema } from '@/components/agents/port-acl-validation'
|
|
import { AgentExplainSheet } from '@/components/agents/agent-explain-sheet'
|
|
import { AgentOverrideSheet } from '@/components/agents/agent-settings-sheets'
|
|
import { StatsAgentSelect } from '@/components/stats/stats-agent-select'
|
|
import '@evofw/ui/globals.css'
|
|
|
|
if (!import.meta.env.DEV) throw new Error('Development checks only')
|
|
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
const results = document.querySelector<HTMLPreElement>('#results')!
|
|
const metrics = document.querySelector<HTMLPreElement>('#metrics')!
|
|
const host = document.querySelector<HTMLDivElement>('#fixture')!
|
|
const root = createRoot(host)
|
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
|
const originalFetch = window.fetch
|
|
const checks: string[] = []
|
|
const measured = { scope: 'Current fixture harness only; no baseline comparison', commits: 0, actualDurationMs: 0, requestCounts: {} as Record<string, number> }
|
|
function reportMetrics() { metrics.textContent = JSON.stringify(measured, null, 2) }
|
|
function check(condition: unknown, label: string) { if (!condition) throw new Error(label); checks.push(`PASS ${label}`); results.textContent = checks.join('\n') }
|
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 50))
|
|
async function render(node: ReactNode) { await act(async () => { root.render(<Profiler id="f1-fixtures" onRender={(_id, _phase, actualDuration) => { measured.commits++; measured.actualDurationMs += actualDuration; reportMetrics() }}><QueryClientProvider client={qc}>{node}</QueryClientProvider></Profiler>); await flush() }) }
|
|
async function input(id: string, value: string) { await act(async () => { const field = document.getElementById(id) as HTMLInputElement; Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(field, value); field.dispatchEvent(new Event('input', { bubbles: true })); await flush() }) }
|
|
async function submit() { await act(async () => { document.querySelector<HTMLFormElement>('[data-slot="sheet-content"] form')!.requestSubmit(); await flush() }) }
|
|
async function choose(id: string, label: string) { await act(async () => { document.getElementById(id)!.click(); await flush() }); await act(async () => { const option = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find((item) => item.textContent?.trim() === label); if (!option) throw new Error(`Option not found: ${label}`); option.click(); await flush() }) }
|
|
const explain: ExplainResponse = { agent_id: 'fixture', query: { ip: '203.0.113.10', port: 443, protocol: 'tcp', chain: 'input', dnat: false, connection_state: 'new' }, desired: { revision: 8, hash: 'desired', decision: null, stage: 'external', match: null }, applied: { revision: null, hash: null, same_as_desired: null, status: 'unsupported' }, matches: [], matches_total: 0, truncated: false, limits: ['desired_policy_not_kernel_observation', 'external_firewall_may_override_accept'], external_firewall: { observed_at: null, rule_count: 0, complete: false } }
|
|
let explainGets = 0
|
|
let overridePosts: Record<string, unknown>[] = []
|
|
let failOverride = true
|
|
window.fetch = async (request, init) => {
|
|
const url = String(request)
|
|
const path = url.split('?')[0]
|
|
const key = `${init?.method ?? 'GET'} ${path}`
|
|
measured.requestCounts[key] = (measured.requestCounts[key] ?? 0) + 1
|
|
reportMetrics()
|
|
if (path.endsWith('/api/v1/agents')) return Response.json({ items: [{ id: 'fixture-office', name: 'Office network', hostname: 'office.example' }, { id: 'fixture-edge', name: 'Edge gateway', hostname: 'edge.example' }] })
|
|
if (path.endsWith('/explain')) { explainGets++; return Response.json(explain) }
|
|
if (path.endsWith('/overrides')) {
|
|
if (init?.method === 'POST') { overridePosts.push(JSON.parse(String(init.body))); return failOverride ? Response.json({ error: { message: 'Fixture write rejected' } }, { status: 503 }) : Response.json({ ok: true }) }
|
|
return Response.json({ items: [{ id: 'expired', agent_id: 'fixture', cidr: '198.51.100.0/24', action: 'deny', created_at: '2026-10-23T00:00:00Z', created_by_name: 'Fixture author', reason: 'Fixture incident', expires_at: '2026-10-23T01:00:00Z', expired: true, expiration_enforcement: 'next_policy_sync' }] })
|
|
}
|
|
throw new Error(`Unexpected fixture request: ${key}`)
|
|
}
|
|
|
|
try {
|
|
const catalog = { items: [{ value: 'fixture-list', label: 'Office ranges' }], loading: false, ready: true, error: null, retry: () => {} }
|
|
const initial = { action: 'open' as const, protocol: 'tcp' as const, port_start: '', port_end: '', src_kind: 'all' as const, src_cidr: '', list_id: '', enabled: true, comment: '' }
|
|
let saves = 0
|
|
let savedPort: number | null = null
|
|
const rule = (list = catalog) => <AgentPortRuleForm open onOpenChange={() => {}} title="Fixture rule" description="Fixture only" canWrite pending={false} lists={list} initialValues={initial} onSubmit={(values) => { saves++; savedPort = portAclDraftSchema.parse(values).port_start }} />
|
|
await render(rule())
|
|
await input('acl-port_start', '1e3'); await submit()
|
|
check(saves === 0 && document.getElementById('acl-port_start')?.getAttribute('aria-invalid') === 'true', 'RHF Port ACL blocks exponent notation before save')
|
|
await input('acl-port_start', '443'); await input('acl-port_end', '80'); await submit()
|
|
check(saves === 0 && document.getElementById('acl-port_end')?.getAttribute('aria-invalid') === 'true', 'RHF Port ACL rejects reversed range with an inline error')
|
|
await input('acl-port_end', '')
|
|
const portNode = document.getElementById('acl-port_start')
|
|
await render(rule({ ...catalog, items: [...catalog.items] }))
|
|
check(document.getElementById('acl-port_start') === portNode && (portNode as HTMLInputElement).value === '443', 'catalog refresh preserves the mounted dirty Port ACL draft')
|
|
await submit()
|
|
check(saves === 1 && savedPort === 443, 'form submit delivers one validated Port ACL save')
|
|
|
|
let imports = 0
|
|
let imported = ''
|
|
await render(<AgentPortImportForm open onOpenChange={() => {}} canWrite pending={false} lists={catalog} sets={catalog} onSubmit={(values) => { imports++; imported = `${values.listId}:${values.ports}` }} />)
|
|
await input('acl-import-ports', '80-'); await submit()
|
|
check(imports === 0 && document.getElementById('acl-import-ports')?.getAttribute('aria-invalid') === 'true', 'RHF import rejects malformed ports and missing source')
|
|
await choose('acl-import-source', 'Office ranges'); await input('acl-import-ports', '22,8000-8001'); await submit()
|
|
check(imports === 1 && imported === 'fixture-list:22,8000-8001', 'RHF import submits its selected source and valid ranges once')
|
|
|
|
let selection: string[] = []
|
|
function Selection() { const [value, setValue] = useState<string[]>([]); return <StatsAgentSelect value={value} onChange={(next) => { selection = next; setValue(next) }} /> }
|
|
await render(<Selection />)
|
|
await act(async () => { const field = document.getElementById('stats-agents')!; field.focus(); field.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); await flush(); await flush() })
|
|
check(document.getElementById('stats-agents')?.getAttribute('aria-expanded') === 'true', 'agent combobox opens through keyboard interaction')
|
|
await act(async () => { const option = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find((item) => item.textContent?.includes('Office network')); if (!option) throw new Error('Office network option not found'); option.click(); await flush() })
|
|
check(selection[0] === 'fixture-office' && host.textContent?.includes('Office network') && !host.textContent?.includes('fixture-office'), 'agent filtering selects names without exposing raw identifiers')
|
|
|
|
await render(<AgentExplainSheet agentId="fixture" open onOpenChange={() => {}} />)
|
|
await input('explain-ip', '203.0.113.10'); await input('explain-port', '443'); await submit()
|
|
check(explainGets === 0 && document.getElementById('explain-port')?.getAttribute('aria-invalid') === 'true', 'Explain blocks an unpaired transport input without a request')
|
|
await choose('explain-protocol', 'TCP'); await submit(); await act(async () => { await flush() })
|
|
check(explainGets === 1 && document.body.textContent?.includes('Решение не определено') && document.body.textContent?.includes('Совпадение неизвестно'), 'Explain preserves external and unconfirmed decisions after a valid read')
|
|
await input('explain-ip', '203.0.113.11')
|
|
check(explainGets === 1 && document.body.textContent?.includes('Форма изменена — запустите проверку снова'), 'editing Explain retains a labeled previous result without issuing another query')
|
|
|
|
await render(<AgentOverrideSheet agentId="fixture" open onOpenChange={() => {}} />)
|
|
await act(async () => { await flush() })
|
|
check(document.body.textContent?.includes('Fixture author') && document.body.textContent?.includes('Истёк'), 'override history shows server-owned author and expired state')
|
|
await input('ov-cidr', '203.0.113.10'); await choose('ov-expiry-mode', 'На время'); await input('ov-ttl', '0'); await submit()
|
|
check(overridePosts.length === 0 && document.getElementById('ov-ttl')?.getAttribute('aria-invalid') === 'true', 'override expiry validation blocks zero TTL')
|
|
await input('ov-ttl', '15')
|
|
await act(async () => { const reason = document.getElementById('ov-reason') as HTMLTextAreaElement; Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')!.set!.call(reason, 'Fixture reason'); reason.dispatchEvent(new Event('input', { bubbles: true })); await flush() })
|
|
await submit(); await act(async () => { await flush() })
|
|
check(overridePosts.length === 1 && (document.getElementById('ov-cidr') as HTMLInputElement).value === '203.0.113.10', 'failed override write preserves the dirty draft')
|
|
failOverride = false
|
|
await submit(); await act(async () => { await flush() })
|
|
check(overridePosts.length === 2 && overridePosts[1].ttl_seconds === 900 && overridePosts[1].reason === 'Fixture reason' && !('expires_at' in overridePosts[1]) && !('created_by_name' in overridePosts[1]), 'temporary override sends validated TTL and reason without author spoofing')
|
|
check((document.getElementById('ov-cidr') as HTMLInputElement).value === '' && document.body.textContent?.includes('следующей связи'), 'successful override resets the draft and retains next-sync expiry limits')
|
|
results.dataset.status = 'passed'
|
|
} catch (error) {
|
|
results.textContent = `${checks.join('\n')}\nFAIL ${error instanceof Error ? error.stack : String(error)}`
|
|
results.dataset.status = 'failed'
|
|
} finally {
|
|
await act(async () => root.unmount())
|
|
qc.clear()
|
|
window.fetch = originalFetch
|
|
reportMetrics()
|
|
}
|