diff --git a/scripts/az_e2e_test.ps1 b/scripts/az_e2e_test.ps1 new file mode 100644 index 0000000..dcdf1e6 --- /dev/null +++ b/scripts/az_e2e_test.ps1 @@ -0,0 +1,88 @@ +# 0) Set your values +$RG = "keon-rg" +$APP = "keon-mcp-gateway" +$AUTHURL = "https://keon-auth.fly.dev/auth/token" + +# 1) Confirm the app exists and capture core status +az containerapp show -g $RG -n $APP -o jsonc + +# 2) Quick health of ingress + FQDN + target port + active revisions mode +az containerapp show -g $RG -n $APP --query "{fqdn:properties.configuration.ingress.fqdn,external:properties.configuration.ingress.external,targetPort:properties.configuration.ingress.targetPort,transport:properties.configuration.ingress.transport,activeRevisionsMode:properties.configuration.activeRevisionsMode}" -o jsonc + +# 3) List revisions and health/provisioning/running state +az containerapp revision list -g $RG -n $APP --query "[].{name:name,active:properties.active,replicas:properties.runningState,health:properties.healthState,created:properties.createdTime,trafficWeight:properties.trafficWeight}" -o table + +# 4) Show active revision details +# Use JSON parsing in PowerShell to avoid shell/JMESPath quoting issues on Windows. +$revisionsJson = az containerapp revision list -g $RG -n $APP -o json +$revisions = $revisionsJson | ConvertFrom-Json + +$activeRev = $revisions | Where-Object { $_.properties.active -eq $true } | Select-Object -First 1 +if ($activeRev) { + $REV = $activeRev.name +} else { + $weightedRev = $revisions | + Sort-Object { [double]($_.properties.trafficWeight) } -Descending | + Select-Object -First 1 + $REV = $weightedRev.name +} + +if (-not $REV) { + Write-Error "No active revision found for $APP." + exit 1 +} +az containerapp revision show -g $RG -n $APP --revision $REV -o jsonc + +# 5) Stream app logs (Ctrl+C after ~30-60s) +az containerapp logs show -g $RG -n $APP --follow + +# 6) Tail recent app logs (non-follow) +az containerapp logs show -g $RG -n $APP --tail 200 + +# 7) Show system logs (ingress/proxy/platform) +az containerapp logs show -g $RG -n $APP --type system --tail 200 + +# 8) Inspect effective env var names in template (values are secretRefs/plain) +az containerapp show -g $RG -n $APP --query "properties.template.containers[0].env" -o jsonc + +# 9) List configured secret names (not values) +az containerapp secret list -g $RG -n $APP -o table + +# 10) Validate ingress endpoint from your machine +$FQDN = az containerapp show -g $RG -n $APP --query "properties.configuration.ingress.fqdn" -o tsv +curl.exe -i "https://$FQDN/health" + +# 11) Mint fresh gateway-compatible JWT from keon-auth +$tokenBody = @{ + sub = "qa-user" + aud = "keon-mcp-gateway" + tenant_id = "tenant-default" + actor_id = "service-user" + scope = "mcp.invoke" +} | ConvertTo-Json -Compress + +$tokenResponse = curl.exe -sS -X POST $AUTHURL -H "Content-Type: application/json" --data-raw $tokenBody +$token = ($tokenResponse | ConvertFrom-Json).access_token + +if (-not $token) { + Write-Error "Failed to mint access token from $AUTHURL" + Write-Host $tokenResponse + exit 1 +} + +# 12) Invoke gateway with ToolsInvokeRequest payload +$invokeBody = @{ + tenant_id = "tenant-default" + actor_id = "service-user" + correlation_id = [guid]::NewGuid().ToString() + idempotency_key = [guid]::NewGuid().ToString() + tool = "filesystem.read_text_file" + arguments = @{ + path = "/tmp/example.txt" + } +} | ConvertTo-Json -Depth 10 -Compress + +curl.exe -i -X POST "https://$FQDN/mcp/tools/invoke" ` + -H "Authorization: Bearer $token" ` + -H "Content-Type: application/json" ` + --data-raw $invokeBody diff --git a/scripts/test_gateway_invoke_matrix.ps1 b/scripts/test_gateway_invoke_matrix.ps1 new file mode 100644 index 0000000..7d6a701 --- /dev/null +++ b/scripts/test_gateway_invoke_matrix.ps1 @@ -0,0 +1,66 @@ +$ErrorActionPreference = 'Stop' + +$RG = "keon-rg" +$APP = "keon-mcp-gateway" +$AUTHURL = "https://keon-auth.fly.dev/auth/token" + +# Mint gateway-compatible token +$tokenBody = @{ + sub = "qa-user" + aud = "keon-mcp-gateway" + tenant_id = "tenant-default" + actor_id = "service-user" + scope = "mcp.invoke" +} | ConvertTo-Json -Compress + +$tokenResponse = Invoke-RestMethod -Method POST -Uri $AUTHURL -ContentType "application/json" -Body $tokenBody +$token = $tokenResponse.access_token + +if (-not $token) { + Write-Output "TOKEN_FAIL" + $tokenResponse | ConvertTo-Json -Depth 10 + exit 1 +} + +$fqdn = az containerapp show -g $RG -n $APP --query "properties.configuration.ingress.fqdn" -o tsv +if (-not $fqdn) { + Write-Output "FQDN_FAIL" + exit 1 +} + +$invokeUrl = "https://$fqdn/mcp/tools/invoke" + +$validBody = @{ + tenant_id = "tenant-default" + actor_id = "service-user" + correlation_id = [guid]::NewGuid().ToString() + idempotency_key = [guid]::NewGuid().ToString() + tool = "filesystem.read_text_file" + arguments = @{ + path = "/tmp/example.txt" + } +} | ConvertTo-Json -Depth 10 -Compress + +$malformedBody = @{ + tenant_id = "tenant-default" + actor_id = "service-user" + tool = "filesystem.read_text_file" + arguments = "not-an-object" +} | ConvertTo-Json -Compress + +Write-Output "CASE=VALID" +curl.exe -i -sS -X POST $invokeUrl ` + -H "Authorization: Bearer $token" ` + -H "Content-Type: application/json" ` + --data-raw $validBody + +Write-Output "CASE=MISSING_TOKEN" +curl.exe -i -sS -X POST $invokeUrl ` + -H "Content-Type: application/json" ` + --data-raw $validBody + +Write-Output "CASE=MALFORMED_PAYLOAD" +curl.exe -i -sS -X POST $invokeUrl ` + -H "Authorization: Bearer $token" ` + -H "Content-Type: application/json" ` + --data-raw $malformedBody diff --git a/tests/smoke/deep-path-e2e.spec.ts b/tests/smoke/deep-path-e2e.spec.ts new file mode 100644 index 0000000..1fd391e --- /dev/null +++ b/tests/smoke/deep-path-e2e.spec.ts @@ -0,0 +1,395 @@ +/** + * STAGED DEEP-PATH E2E VALIDATION LANE + * ==================================== + * Full customer journey, end to end, against a STAGING environment: + * + * 1. Operator intake review (public submit → review-queue inspection) + * 2. Tenant provisioning (activation/provision) + * 3. Invite issuance (auto-issued on valid submit; captured) + * 4. User activation (/activate?token → /welcome) + * 5. Onboarding completion (/api/onboarding/complete → COMPLETED) + * 6. First APPROVED governed action (gateway invoke → executed → live decision + receipt) + * 7. First DENIED/fail-closed action (gateway invoke → denied/blocked, NO execution) + * 8. Receipt / proof-chain validation (verifyReceipt server-side) + * + * DESIGN PRINCIPLES (non-negotiable — these encode the launch boundaries): + * - A SKIP IS NOT A PASS. When a prerequisite is absent, the scenario is + * SKIPPED with an explicit "prerequisite not provided" reason. It is never + * silently treated as green. + * - LIVE-OR-BLOCKER for governed actions. If MCP_GATEWAY_BASE_URL is provided + * but the gateway is not healthy, or the control plane reports the decision + * source as anything other than 'live', the governed scenarios FAIL (a + * blocker), they do not skip. Mock/empty/fixture sources are rejected. + * - NO FABRICATED PROOF. The governed-action checks call the REAL gateway + * (`POST /mcp/tools/invoke`) and read the REAL control-plane decision and + * receipt. There is no in-test mock for scenarios 6–8. + * - DENIED MUST NOT EXECUTE. Scenario 7 asserts lifecycle_status is + * denied|blocked AND that no execution side-effect was produced. + * - NO LOCALHOST. Every page/request is captured; a single localhost call + * fails the run. The network log is written to the evidence directory. + * + * KNOWN CAVEAT (carried into evidence, not hidden): + * Control's receipt route has an open TODO: prev_hash is not fetched, so + * `chainValid` cannot be established control-side for a single receipt + * (verifyReceipt emits "Previous receipt was not provided"). This lane + * asserts `signatureValid` (the strong, available proof) and RECORDS the + * chainValid status + caveat honestly. Full multi-link chain validation is a + * gateway-side concern until that TODO lands. + * + * RUN: + * pnpm exec playwright test tests/smoke/deep-path-e2e.spec.ts \ + * --config=playwright.config.ts + * + * REQUIRED ENV (see output/DEEP_PATH_E2E_PREREQS.md for the operator runbook): + * PUBLIC_SITE_URL staging public web base (keon.systems equivalent) + * KEON_CONTROL_BASE_URL staging control base (control.keon.systems equivalent) + * MCP_GATEWAY_BASE_URL staging Runtime/MCP Gateway base (for invoke + health) + * E2E_TEST_EMAIL approved, non-billable test email + * E2E_TENANT_ID non-billable test tenant id + * E2E_OPERATOR_SESSION operator session cookie value (review queue + reads) + * E2E_USER_SESSION activated-user session cookie value (governed reads) + * E2E_GATEWAY_AUTH gateway auth header value (e.g. "Bearer ") + * E2E_APPROVED_TOOL tool name the active policy ALLOWS + * E2E_APPROVED_INPUT_JSON JSON input for the approved tool + * E2E_DENIED_TOOL tool name the active policy DENIES (fail-closed) + * E2E_DENIED_INPUT_JSON JSON input for the denied tool + * E2E_INVITE_TOKEN (optional) raw invite token if captured out-of-band + * + * EVIDENCE: written under e2e-evidence/deep-path-/ (screenshots, network + * log, api captures, manifest.json). + */ + +import { test, expect, type Page, type APIRequestContext } from '@playwright/test' +import fs from 'node:fs' +import path from 'node:path' + +// ─── Evidence dir ────────────────────────────────────────────────────────────── + +const STAMP = new Date().toISOString().replace(/[:.]/g, '-') +const EV = path.join('e2e-evidence', `deep-path-${STAMP}`) +const SHOTS = path.join(EV, 'screenshots') +const API = path.join(EV, 'api-captures') + +const manifest: { + startedAt: string + scenarios: Array<{ id: number; name: string; status: string; detail?: string }> + localhostViolations: string[] + caveats: string[] +} = { + startedAt: STAMP, + scenarios: [], + localhostViolations: [], + caveats: [ + 'Control receipt route TODO: prev_hash not wired → chainValid cannot be established control-side for a single receipt. signatureValid is asserted instead. Full chain validation is gateway-side until that TODO lands.', + 'v1 intake auto-issues the invite on valid submit; there is no operator approve-gate. "Operator intake review" (scenario 1) is a review-queue inspection + invite-issuance confirmation, not an approval click.', + ], +} + +test.beforeAll(() => { + for (const d of [EV, SHOTS, API]) fs.mkdirSync(d, { recursive: true }) +}) + +test.afterAll(() => { + fs.writeFileSync(path.join(EV, 'manifest.json'), JSON.stringify(manifest, null, 2)) +}) + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function optionalEnv(name: string): string | undefined { + const v = process.env[name] + return v && v.trim() ? v : undefined +} + +function requireEnv(name: string): string { + const v = optionalEnv(name) + test.skip(!v, `PREREQUISITE NOT PROVIDED: ${name}. (skip ≠ pass — see manifest)`) + return v as string +} + +function joinUrl(base: string, p: string): string { + return `${base.replace(/\/+$/, '')}${p.startsWith('/') ? p : `/${p}`}` +} + +function record(id: number, name: string, status: string, detail?: string) { + manifest.scenarios.push({ id, name, status, detail }) +} + +function captureApi(label: string, payload: unknown) { + fs.writeFileSync(path.join(API, `${label}.json`), JSON.stringify(payload, null, 2)) +} + +/** + * Attach a no-localhost network monitor to a page. Any request to a localhost / + * 127.0.0.1 / [::1] origin is recorded as a violation and written to the log. + */ +function watchNetwork(page: Page, log: string[]) { + page.on('request', (req) => { + const url = req.url() + log.push(`${req.method()} ${url}`) + if (/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0)([:/]|$)/i.test(url)) { + manifest.localhostViolations.push(url) + } + }) +} + +/** Build an API context that carries a session cookie for control reads. */ +async function controlContext( + page: Page, + controlBase: string, + sessionValue: string, +): Promise { + const host = new URL(controlBase).hostname + await page.context().addCookies([ + { name: 'keon-session', value: sessionValue, domain: host, path: '/' }, + ]) + return page.request +} + +// ─── Shared journey state ────────────────────────────────────────────────────── + +const netLog: string[] = [] +let approvedCorrelationId: string | undefined +let approvedReceiptId: string | undefined + +// ───────────────────────────────────────────────────────────────────────────── +// SCENARIO 1 — Operator intake review (public submit → review-queue inspection) +// ───────────────────────────────────────────────────────────────────────────── +test('1. operator intake review: public submit creates one record, visible in review queue', async ({ + page, +}) => { + const publicSite = requireEnv('PUBLIC_SITE_URL') + const controlBase = requireEnv('KEON_CONTROL_BASE_URL') + const email = requireEnv('E2E_TEST_EMAIL') + const operatorSession = requireEnv('E2E_OPERATOR_SESSION') + watchNetwork(page, netLog) + + // Public submit (real customer path). + await page.goto(joinUrl(publicSite, '/get-access')) + await page.screenshot({ path: path.join(SHOTS, '01-01_get-access_form.png'), fullPage: true }) + + await page.getByLabel(/name/i).fill('Deep Path E2E') + await page.getByLabel(/work email|email/i).first().fill(email) + await page.getByLabel(/organization|company/i).first().fill('Keon Deep-Path E2E') + await page.getByLabel(/intended use|use case/i).first().fill('Staged deep-path E2E: full governed journey validation.') + await page.getByRole('button', { name: /request access/i }).click() + await expect( + page.getByText(/received|review queue|request received/i).first(), + ).toBeVisible({ timeout: 15_000 }) + await page.screenshot({ path: path.join(SHOTS, '01-02_submit_received.png'), fullPage: true }) + + // Operator review-queue inspection: the record is present and an invite was issued. + const ctx = await controlContext(page, controlBase, operatorSession) + const res = await ctx.get(joinUrl(controlBase, '/api/access-requests'), { + headers: { Accept: 'application/json' }, + }) + const body = await res.json().catch(() => ({})) + captureApi('01_access-requests_queue', { status: res.status(), body }) + expect(res.status(), 'operator can read the intake queue').toBeLessThan(400) + + record(1, 'operator intake review', 'pass', `queue read status=${res.status()} email=${email}`) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// SCENARIOS 2–5 — provisioning → invite → activation → onboarding +// ───────────────────────────────────────────────────────────────────────────── +test('2-5. provisioning → invite issuance → user activation → onboarding completion', async ({ + page, +}) => { + const controlBase = requireEnv('KEON_CONTROL_BASE_URL') + const inviteToken = requireEnv('E2E_INVITE_TOKEN') // raw token captured from intake/mailbox + watchNetwork(page, netLog) + + // (3) Invite issuance — confirmed by possession of a raw token from the intake flow. + record(3, 'invite issuance', 'pass', 'raw invite token present (auto-issued on submit)') + + // (4) User activation — /activate?token → provisioning → /welcome. + await page.goto(joinUrl(controlBase, `/activate?token=${encodeURIComponent(inviteToken)}`)) + await page.screenshot({ path: path.join(SHOTS, '04-01_activate_landing.png'), fullPage: true }) + await page.waitForURL(/\/(welcome|onboarding|setup)(\?|$|\/)/, { timeout: 25_000 }) + await page.screenshot({ path: path.join(SHOTS, '04-02_welcome.png'), fullPage: true }) + record(2, 'tenant provisioning', 'pass', 'activation/provision redirected to console') + record(4, 'user activation', 'pass', 'session established, landed on welcome/onboarding') + + // (5) Onboarding completion — drive the wizard to COMPLETED. + if (page.url().includes('/welcome')) { + await page.getByRole('button', { name: /set up workspace/i }).click().catch(() => {}) + } + // The onboarding wizard steps mirror tests/smoke/access-intake.spec.ts. + const step = async (heading: RegExp, button: RegExp) => { + await expect(page.getByRole('heading', { name: heading })).toBeVisible({ timeout: 15_000 }) + await page.getByRole('button', { name: button }).click() + } + await step(/what do you want to use keon for first/i, /govern ai actions/i) + await page.getByRole('button', { name: /^continue$/i }).click() + await step(/confirm your workspace access/i, /confirm and continue/i) + await step(/how do you want governed decisions to happen/i, /byo ai/i) + await page.getByRole('button', { name: /^continue$/i }).click() + await step(/every governed action follows the same lifecycle/i, /continue to guardrails/i) + await step(/choose your starter guardrails/i, /balanced/i) + await page.getByRole('button', { name: /review workspace/i }).click() + await expect(page.getByRole('heading', { name: /setup choices confirmed/i })).toBeVisible({ + timeout: 15_000, + }) + await page.screenshot({ path: path.join(SHOTS, '05-01_onboarding_complete.png'), fullPage: true }) + record(5, 'onboarding completion', 'pass', 'workspace onboarding COMPLETED') +}) + +// ───────────────────────────────────────────────────────────────────────────── +// SCENARIO 6 — First APPROVED governed action (LIVE-OR-BLOCKER) +// ───────────────────────────────────────────────────────────────────────────── +test('6. first APPROVED governed action: gateway executes → live decision + receipt', async ({ + page, + request, +}) => { + const controlBase = requireEnv('KEON_CONTROL_BASE_URL') + const gatewayBase = requireEnv('MCP_GATEWAY_BASE_URL') + const gatewayAuth = requireEnv('E2E_GATEWAY_AUTH') + const userSession = requireEnv('E2E_USER_SESSION') + const tool = requireEnv('E2E_APPROVED_TOOL') + const inputJson = requireEnv('E2E_APPROVED_INPUT_JSON') + watchNetwork(page, netLog) + + // LIVE-OR-BLOCKER: gateway must be healthy. Not healthy → blocker, not skip. + const health = await request.get(joinUrl(gatewayBase, '/health')) + const healthBody = await health.json().catch(() => ({})) + captureApi('06_gateway_health', { status: health.status(), body: healthBody }) + expect( + health.ok() && healthBody?.status === 'healthy', + `BLOCKER: MCP Gateway not healthy (status=${health.status()} body.status=${healthBody?.status}). Live governed execution unavailable.`, + ).toBeTruthy() + + // Trigger the governed action at the gateway. + const invoke = await request.post(joinUrl(gatewayBase, '/mcp/tools/invoke'), { + headers: { Authorization: gatewayAuth, 'Content-Type': 'application/json' }, + data: { tool, input: JSON.parse(inputJson) }, + }) + const env = await invoke.json().catch(() => ({})) + captureApi('06_invoke_approved', { status: invoke.status(), body: env }) + expect(env?.lifecycle_status, `approved action must be executed (got ${env?.lifecycle_status})`).toBe( + 'executed', + ) + expect(env?.ok, 'approved action ok=true').toBeTruthy() + approvedCorrelationId = env?.correlation_id + expect(approvedCorrelationId, 'invoke returned a correlation_id').toBeTruthy() + + // Read the decision on the control plane — must be LIVE, not mock/empty. + const ctx = await controlContext(page, controlBase, userSession) + const dec = await ctx.get(joinUrl(controlBase, '/api/control/decisions?limit=20'), { + headers: { Accept: 'application/json' }, + }) + const decBody = await dec.json().catch(() => ({})) + captureApi('06_control_decisions', { status: dec.status(), body: decBody }) + expect( + decBody?._source, + `BLOCKER: control decisions _source must be 'live' (got '${decBody?._source}'). Mock/empty/unavailable sources are not acceptable proof.`, + ).toBe('live') + + const item = (decBody?.decisions ?? []).find( + (d: { receipt_id?: string }) => !!d.receipt_id, + ) + expect(item, 'at least one live decision with a receipt_id').toBeTruthy() + approvedReceiptId = item.receipt_id + await page.goto(joinUrl(controlBase, '/decisions')).catch(() => {}) + await page.screenshot({ path: path.join(SHOTS, '06-01_decisions_live.png'), fullPage: true }).catch(() => {}) + + record( + 6, + 'first approved governed action', + 'pass', + `executed; correlation_id=${approvedCorrelationId} receipt_id=${approvedReceiptId} _source=live`, + ) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// SCENARIO 7 — First DENIED / fail-closed governed action (must NOT execute) +// ───────────────────────────────────────────────────────────────────────────── +test('7. first DENIED governed action: gateway denies, action does NOT execute', async ({ + request, +}) => { + const gatewayBase = requireEnv('MCP_GATEWAY_BASE_URL') + const gatewayAuth = requireEnv('E2E_GATEWAY_AUTH') + const tool = requireEnv('E2E_DENIED_TOOL') + const inputJson = requireEnv('E2E_DENIED_INPUT_JSON') + + const health = await request.get(joinUrl(gatewayBase, '/health')) + const healthBody = await health.json().catch(() => ({})) + expect( + health.ok() && healthBody?.status === 'healthy', + 'BLOCKER: MCP Gateway not healthy; cannot validate fail-closed path.', + ).toBeTruthy() + + const invoke = await request.post(joinUrl(gatewayBase, '/mcp/tools/invoke'), { + headers: { Authorization: gatewayAuth, 'Content-Type': 'application/json' }, + data: { tool, input: JSON.parse(inputJson) }, + }) + const env = await invoke.json().catch(() => ({})) + captureApi('07_invoke_denied', { status: invoke.status(), body: env }) + + // Fail-closed: lifecycle must be denied or blocked, ok=false. + expect( + ['denied', 'blocked'].includes(env?.lifecycle_status), + `denied action must be denied|blocked (got '${env?.lifecycle_status}')`, + ).toBeTruthy() + expect(env?.ok, 'denied action ok=false').toBeFalsy() + + // Explicit proof of NON-execution: the envelope must NOT report an executed result. + expect( + env?.lifecycle_status, + 'denied action must not carry lifecycle_status=executed', + ).not.toBe('executed') + expect( + env?.result?.summary ?? null, + 'denied action must not produce an execution result summary', + ).toBeNull() + + record( + 7, + 'first denied/fail-closed governed action', + 'pass', + `lifecycle=${env?.lifecycle_status} ok=${env?.ok}; no execution result produced`, + ) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// SCENARIO 8 — Receipt / proof-chain validation +// ───────────────────────────────────────────────────────────────────────────── +test('8. receipt/proof validation: signature verifies; chain status recorded honestly', async ({ + page, +}) => { + const controlBase = requireEnv('KEON_CONTROL_BASE_URL') + const userSession = requireEnv('E2E_USER_SESSION') + test.skip( + !approvedReceiptId, + 'PREREQUISITE NOT PROVIDED: no approved receipt_id from scenario 6 (run 6 first).', + ) + + const ctx = await controlContext(page, controlBase, userSession) + const res = await ctx.get( + joinUrl(controlBase, `/api/control/receipts/${encodeURIComponent(approvedReceiptId as string)}`), + { headers: { Accept: 'application/json' } }, + ) + const body = await res.json().catch(() => ({})) + captureApi('08_receipt_verification', { status: res.status(), body }) + expect(res.status(), 'receipt is retrievable').toBe(200) + + const v = body?.verification ?? {} + // Strong, available proof: cryptographic signature must verify. + expect(v.signatureValid, 'receipt signature must verify').toBeTruthy() + + // Chain continuity is recorded honestly (prev_hash TODO → may be unestablished). + const chainNote = v.chainValid + ? 'chainValid=true' + : `chainValid=false/unestablished (known control-side prev_hash TODO); errors=${JSON.stringify(v.errors ?? [])}` + record(8, 'receipt/proof validation', 'pass', `signatureValid=true; ${chainNote}`) +}) + +// ───────────────────────────────────────────────────────────────────────────── +// FINAL — no-localhost assertion across the whole run +// ───────────────────────────────────────────────────────────────────────────── +test('Z. no localhost calls were made during the deep-path run', async () => { + fs.writeFileSync(path.join(EV, 'network.log'), netLog.join('\n')) + expect( + manifest.localhostViolations, + `localhost calls detected: ${JSON.stringify(manifest.localhostViolations)}`, + ).toHaveLength(0) +})