[TEST] Test Runner v2: aggiunto L6 AI Features (Cross-Analysis, Normative, Whistleblowing)

- Livello L6 con 6 test: portfolio, history, analyze AI, 403 role check, normative, whistleblowing
- CSS badge lvl-l6 verde smeraldo
- Coverage table: 27 → 35 endpoint (8 nuovi L6)
- Full Suite aggiornata L1→L6 con step L3 e L6 funzionanti
- Header doc aggiornato

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-03-09 09:01:28 +01:00
parent 19a9e5622d
commit fca3ab3cf8

View File

@ -9,6 +9,7 @@
* L3 Compliance Core
* L4 B2B & Services API
* L5 Export & Reports
* L6 AI Features (Cross-Analysis, Workflow, Normative)
* SIM Simulazioni scenari reali
*/
@ -187,6 +188,50 @@ function getCommands(): array
'cwd' => $root, 'timeout' => 60, 'continue_on_fail' => true,
],
// ── L6: AI FEATURES ─────────────────────────────────────────────────
'l6-ai' => [
'label' => 'L6 — AI Features (Cross-Analysis)',
'level' => 'l6',
'bash' => implode(' && ', [
// Login come consultant (ha accesso a cross-analysis)
"echo '━━━ L6.0 Setup: login consultant ━━━'",
"TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && echo \"Token consultant: \${TOKEN_CONS:0:25}...\" || echo '[SKIP] consultant non trovato — eseguire SIM-01 prima'",
"echo ''",
// L6.1: portfolio (senza AI, solo dati aggregati)
"echo '━━━ L6.1 Cross-Analysis Portfolio (no AI) ━━━'",
"TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/portfolio | python3 -m json.tool || echo '[SKIP] token non disponibile'",
"echo ''",
// L6.2: history (vuota, ma endpoint deve rispondere 200)
"echo '━━━ L6.2 Cross-Analysis History ━━━'",
"TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/history | python3 -c \"import sys,json; d=json.load(sys.stdin); h=d.get('data',{}).get('history',[]); print(f'History entries: {len(h)} — HTTP OK')\" || echo '[SKIP]'",
"echo ''",
// L6.3: analyze con domanda breve (chiama AI Anthropic)
"echo '━━━ L6.3 Cross-Analysis AI Analyze ━━━'",
"TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -X POST -H \"Authorization: Bearer \$TOKEN_CONS\" -H 'Content-Type: application/json' -d '{\"question\":\"Qual e il livello medio di compliance e quali sono le categorie NIS2 piu deboli nel portfolio?\"}' {$api}/cross-analysis/analyze | python3 -c \"import sys,json; d=json.load(sys.stdin); r=d.get('data',{}); ans=r.get('result',{}).get('answer',''); orgs=r.get('org_count',0); print(f'Org analizzate: {orgs}'); print(f'Risposta AI ({len(ans)} chars): {ans[:300]}...' if len(ans)>300 else f'Risposta: {ans}')\" || echo '[SKIP/ERRORE]'",
"echo ''",
// L6.4: accesso negato a utente normale (403)
"echo '━━━ L6.4 Cross-Analysis 403 per utente non-consultant ━━━'",
"TOKEN_EMP=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_EMP\" ] && curl -sf -o /dev/null -w 'GET /cross-analysis/portfolio (org_admin) → HTTP %{http_code}\\n' -H \"Authorization: Bearer \$TOKEN_EMP\" {$api}/cross-analysis/portfolio || echo '[SKIP]'",
"echo ''",
// L6.5: normative feed
"echo '━━━ L6.5 Normative Feed ━━━'",
"TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/normative/list | python3 -c \"import sys,json; d=json.load(sys.stdin); items=d.get('data',[]); print(f'Aggiornamenti normativi: {len(items)}')\" || echo '[SKIP]'",
"echo ''",
// L6.6: whistleblowing stats
"echo '━━━ L6.6 Whistleblowing Stats ━━━'",
"TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/whistleblowing/stats | python3 -m json.tool || echo '[SKIP]'",
"echo '[OK] L6 AI Features completato'",
]),
'cwd' => $root, 'timeout' => 120, 'continue_on_fail' => true,
],
// ── SMOKE ───────────────────────────────────────────────────────────
'smoke' => [
'label' => 'Smoke Tests (curl rapido)',
@ -259,16 +304,20 @@ function getCommands(): array
'cwd' => $root, 'timeout' => 30, 'continue_on_fail' => false,
],
'full-suite' => [
'label' => 'Full Suite L1+L2+L3+L4+L5',
'label' => 'Full Suite L1+L2+L3+L4+L5+L6',
'level' => 'infra',
'bash' => implode(' && ', [
"echo '════════════ L1 AUTH ════════════'",
"curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore.demo\",\"password\":\"Demo2026!\"}' | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Login:', 'OK' if d.get('success') else 'FAIL')\" || echo 'L1 SKIP'",
"echo '════════════ L2 TENANT ════════════'",
"curl -sf {$api}/../api-status.php | python3 -c \"import sys,json; d=json.load(sys.stdin); print('API:', d.get('status','?'))\"",
"TOKEN={$getToken} && curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(f'Orgs: {len(orgs)}')\" || echo 'L2 SKIP'",
"echo '════════════ L3 COMPLIANCE ════════════'",
"TOKEN={$getToken} && ORG_ID=\$(curl -sf -H \"Authorization: Bearer \$TOKEN\" {$api}/organizations/list 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); orgs=d.get('data',[]); print(orgs[0]['id'] if orgs else '')\" 2>/dev/null) && [ -n \"\$ORG_ID\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN\" -H \"X-Organization-Id: \$ORG_ID\" {$api}/dashboard/compliance-score | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Score:', d.get('data',{}).get('score','?'))\" || echo 'L3 SKIP'",
"echo '════════════ L5 EXPORT ════════════'",
"curl -sf {$api}/../api-status.php | python3 -m json.tool",
"echo '[OK] Full Suite completata'",
"curl -sf {$api}/../api-status.php | python3 -c \"import sys,json; d=json.load(sys.stdin); print('API:', d.get('status','?'))\"",
"echo '════════════ L6 AI CROSS ════════════'",
"TOKEN_CONS=\$(curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"consultant@nis2agile.demo\",\"password\":\"Demo2026!\"}' 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('access_token',''))\" 2>/dev/null) && [ -n \"\$TOKEN_CONS\" ] && curl -sf -H \"Authorization: Bearer \$TOKEN_CONS\" {$api}/cross-analysis/portfolio | python3 -c \"import sys,json; d=json.load(sys.stdin); print('Portfolio orgs:', d.get('data',{}).get('org_count','?'))\" || echo 'L6 SKIP (eseguire SIM-01 prima)'",
"echo '[OK] Full Suite L1→L6 completata'",
]),
'cwd' => $root, 'timeout' => 300, 'continue_on_fail' => true,
],
@ -576,8 +625,9 @@ function serveUI(): void
'l3-compliance' => ['L3', 'Compliance Core', 'l3'],
'l4-b2b' => ['L4', 'B2B & Services', 'l4'],
'l5-export' => ['L5', 'Export & Reports', 'l5'],
'l6-ai' => ['L6', 'AI Features', 'l6'],
'chain-verify' => ['—', 'Hash Chain Verify', 'infra'],
'full-suite' => ['ALL', 'Full Suite L1→L5', 'infra'],
'full-suite' => ['ALL', 'Full Suite L1→L6', 'infra'],
'reset' => ['⚠', 'Reset Dati Demo', 'danger'],
];
foreach ($levelDefs as $id => [$badge, $label, $cls]) {
@ -615,6 +665,13 @@ function serveUI(): void
['NCR', 'GET /api/ncr/stats', 'L4', 'ok'],
['Onboard', 'POST /api/onboarding/complete', 'sim','ok'],
['Admin', 'GET /api/admin/stats', 'admin','skip'],
['CrossAI', 'GET /api/cross-analysis/portfolio', 'L6', 'ok'],
['CrossAI', 'GET /api/cross-analysis/history', 'L6', 'ok'],
['CrossAI', 'POST /api/cross-analysis/analyze', 'L6', 'ok'],
['Normative','GET /api/normative/list', 'L6', 'ok'],
['Normative','POST /api/normative/{id}/ack', 'L6', 'sim'],
['Whistle', 'GET /api/whistleblowing/stats', 'L6', 'ok'],
['Whistle', 'POST /api/whistleblowing/submit', 'L6', 'sim'],
];
$covRows = '';
foreach ($coverageEndpoints as [$module, $ep, $level, $status]) {
@ -728,6 +785,7 @@ body { display: flex; height: 100vh; background: var(--navy); color: var(--text)
.lvl-l3 { background: rgba(249,115,22,.15); color: var(--orange); }
.lvl-l4 { background: rgba(168,85,247,.15); color: var(--purple); }
.lvl-l5 { background: rgba(234,179,8,.15); color: var(--yellow); }
.lvl-l6 { background: rgba(16,185,129,.15); color: #10b981; }
.lvl-infra { background: rgba(100,116,139,.15);color: var(--muted); }
.lvl-sim { background: rgba(6,182,212,.1); color: var(--cyan); }
.lvl-admin { background: rgba(239,68,68,.1); color: var(--red); }
@ -885,7 +943,7 @@ body { display: flex; height: 100vh; background: var(--navy); color: var(--text)
<div id="tab-cov" class="tab-panel">
<div style="padding:.5rem .65rem; overflow-x:auto;">
<p style="font-size:.68rem;color:var(--muted);margin-bottom:.5rem">
Copertura endpoint API (27 totali)
Copertura endpoint API (35 totali)
</p>
<table class="cov-table">
<thead><tr><th>Modulo</th><th>Endpoint</th><th>Lvl</th><th>Status</th></tr></thead>