From 8089d061ecfa9be4f3f33a9dcb5eed75655dc6d7 Mon Sep 17 00:00:00 2001 From: LucaP Date: Wed, 2 Sep 2026 07:24:48 +0300 Subject: [PATCH] test: add deterministic resilience matrix --- playwright.config.ts | 1 + scripts/verify-artifacts.mjs | 2 + src/audio/create-audio-controller.ts | 4 +- src/audio/synth-engine.ts | 34 ++++- src/components/AccessibleVideo.vue | 9 ++ src/components/DriveControls.vue | 2 +- src/components/SceneCanvas.vue | 10 +- src/composables/useAudioEngine.ts | 6 + src/composables/useSceneController.ts | 41 +++++- src/entry-client.ts | 15 +- src/scene/create-scene-controller.ts | 1 + src/scene/types.ts | 1 + src/scene/world/environment.ts | 26 ++++ src/testing/fault-hooks.ts | 126 +++++++++++++++++ tests/browser/README.md | 28 +++- tests/browser/audio-interruption.spec.ts | 89 ++++++++++++ tests/browser/context-loss.spec.ts | 76 +++++++++++ tests/browser/fault-matrix.spec.ts | 112 +++++++++++++++ tests/browser/media-failure.spec.ts | 50 +++++++ tests/helpers/test-build-auth.ts | 167 +++++++++++++++++++++++ vite.config.ts | 31 +++-- 21 files changed, 809 insertions(+), 22 deletions(-) create mode 100644 src/testing/fault-hooks.ts create mode 100644 tests/browser/audio-interruption.spec.ts create mode 100644 tests/browser/context-loss.spec.ts create mode 100644 tests/browser/fault-matrix.spec.ts create mode 100644 tests/browser/media-failure.spec.ts create mode 100644 tests/helpers/test-build-auth.ts diff --git a/playwright.config.ts b/playwright.config.ts index ff133fc..3876f12 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/browser', + globalSetup: './tests/helpers/test-build-auth.ts', workers: 4, use: { serviceWorkers: 'block', diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index 5c84a21..92a22ec 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -15,6 +15,8 @@ const canonicalSectionIds = [ ]; const prohibitedSurfaces = [ /__PORTFOLIO_INPUTS__/iu, + /__PORTFOLIO_TEST_HOOKS__/iu, + /PORTFOLIO_TEST_TOKEN/iu, /__PORTFOLIO_(?:TEST|INSTRUMENTATION)__/iu, /__portfolio(?:Journey)?Instrumentation/iu, /(?:evidenceId|sourcePath|sourceFingerprint|approvalOwner|privacyClassification|denylist|originalFilename)/iu, diff --git a/src/audio/create-audio-controller.ts b/src/audio/create-audio-controller.ts index 98d8339..71ed933 100644 --- a/src/audio/create-audio-controller.ts +++ b/src/audio/create-audio-controller.ts @@ -103,8 +103,8 @@ export function createAudioController(options: AudioControllerOptions): AudioCon if (!graph) return; try { await graph.resume(); - } catch { - if (firstStart) await fail(); + } catch (error) { + if (firstStart || (error instanceof Error && error.name === 'InvalidStateError')) await fail(); else { desired = 'suspended'; resumeIntent = 'user-required'; diff --git a/src/audio/synth-engine.ts b/src/audio/synth-engine.ts index ccbaa20..72df00d 100644 --- a/src/audio/synth-engine.ts +++ b/src/audio/synth-engine.ts @@ -1,12 +1,44 @@ import type { AudioContextLifecycleState, AudioGraph } from '@/audio/types'; +import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks'; + +declare const __PORTFOLIO_TEST_BUILD__: boolean; +const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; type AudioContextFactory = () => AudioContext; export function createBrowserSynthEngine(): AudioGraph { + if (TEST_BUILD && isTestFault('F10-audio-start-rejection')) { + recordTestFaultEvent('audio-start-rejected'); + throw new DOMException('Injected audio start rejection', 'NotAllowedError'); + } const AudioContextConstructor = window.AudioContext ?? (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioContextConstructor) throw new Error('Web Audio is unavailable'); - return createSynthEngine(() => new AudioContextConstructor()); + const graph = createSynthEngine(() => new AudioContextConstructor()); + if (!TEST_BUILD) return graph; + let resumeCount = 0; + return { + ...graph, + async resume() { + resumeCount += 1; + if (isTestFault('F11-audio-resume-rejection') && resumeCount > 1) { + recordTestFaultEvent('audio-resume-rejected'); + throw new DOMException('Injected audio resume rejection', 'NotAllowedError'); + } + if (isTestFault('F12-audio-graph-invalid') && resumeCount > 1) { + recordTestFaultEvent('audio-graph-invalid'); + throw new DOMException('Injected invalid audio graph', 'InvalidStateError'); + } + await graph.resume(); + }, + async suspend() { + if (isTestFault('F13-audio-suspend-rejection')) { + recordTestFaultEvent('audio-suspend-rejected'); + throw new DOMException('Injected audio suspend rejection', 'InvalidStateError'); + } + await graph.suspend(); + }, + }; } export function createSynthEngine(createContext: AudioContextFactory): AudioGraph { diff --git a/src/components/AccessibleVideo.vue b/src/components/AccessibleVideo.vue index 0806560..53258f3 100644 --- a/src/components/AccessibleVideo.vue +++ b/src/components/AccessibleVideo.vue @@ -1,6 +1,10 @@