test: add deterministic resilience matrix

This commit is contained in:
2026-09-02 07:24:48 +03:00
parent 5abd1c19ae
commit 8089d061ec
21 changed files with 809 additions and 22 deletions

View File

@@ -10,6 +10,9 @@ import { createAudioController } from '@/audio/create-audio-controller';
import { createBrowserSynthEngine } from '@/audio/synth-engine';
import type { AudioController, AudioUiState } from '@/audio/types';
declare const __PORTFOLIO_TEST_BUILD__: boolean;
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;
export interface AudioEngineSources {
intensity: Readonly<Ref<number>>;
volume: Readonly<Ref<number>>;
@@ -57,10 +60,13 @@ export function useAudioEngine(sources: AudioEngineSources): AudioEngineBinding
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('pagehide', onPageHide);
window.addEventListener('pageshow', onPageShow);
const onTestInterruption = (): void => { void controller?.noteInterruption(); };
if (TEST_BUILD) window.addEventListener('portfolio-test-audio-interruption', onTestInterruption);
removePageListeners = () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('pagehide', onPageHide);
window.removeEventListener('pageshow', onPageShow);
if (TEST_BUILD) window.removeEventListener('portfolio-test-audio-interruption', onTestInterruption);
};
});

View File

@@ -11,6 +11,10 @@ import { createSceneController } from '@/scene/create-scene-controller';
import type { JourneySnapshot } from '@/journey/types';
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
import type { SceneController, SceneControllerSnapshot } from '@/scene/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__;
export type SceneHostState = 'idle' | 'loading' | 'running' | 'paused' | 'static' | 'failed' | 'disposed';
@@ -72,7 +76,16 @@ export function useSceneController(sources: SceneControllerSources): SceneContro
return;
}
if (canvas === null) {
disposeActive(error.value ? 'failed' : 'idle');
const injectedStatic = TEST_BUILD && (
isTestFault('F1-scene-chunk')
|| isTestFault('F1-shader')
|| isTestFault('F1-car')
|| isTestFault('F1-texture')
|| isTestFault('F2-first-frame-timeout')
|| isTestFault('F3-post-first-frame-exception')
|| isTestFault('F6-repeated-context-loss')
);
disposeActive(error.value ? (injectedStatic ? 'static' : 'failed') : 'idle');
return;
}
@@ -92,6 +105,10 @@ export function useSceneController(sources: SceneControllerSources): SceneContro
error.value = false;
state.value = 'loading';
try {
if (TEST_BUILD && isTestFault('F1-scene-chunk')) {
recordTestFaultEvent('scene-chunk-rejected');
throw new Error('Injected scene chunk failure');
}
const { createProceduralSceneGeneration } = await import('@/scene/world/environment');
if (
abort.signal.aborted
@@ -102,12 +119,20 @@ export function useSceneController(sources: SceneControllerSources): SceneContro
|| sources.canvas.value !== canvas
) return;
const next = createSceneController({
let next!: SceneController;
next = createSceneController({
canvas,
quality: sources.quality.value,
build: createProceduralSceneGeneration,
motionAllowed: () => sources.policy.value.allowSceneRequest,
onFrameRendered: sources.sampleFrame,
onStateChange: (nextState) => {
queueMicrotask(() => {
if (controller !== next) return;
error.value = nextState.kind === 'static';
publish();
});
},
signal: abort.signal,
});
controller = next;
@@ -120,10 +145,20 @@ export function useSceneController(sources: SceneControllerSources): SceneContro
const kind = next.snapshot().state.kind;
error.value = kind === 'static';
publish();
if (TEST_BUILD && isTestFault('F2-first-frame-timeout')) {
window.setTimeout(() => {
if (controller === next && next.snapshot().state.kind === 'running') {
recordTestFaultEvent('first-frame-deadline');
next.setStatic('first-frame-timeout');
error.value = true;
publish();
}
}, 5_000);
}
} catch {
if (!abort.signal.aborted && currentGeneration === generation) {
error.value = true;
disposeActive('failed');
disposeActive(TEST_BUILD && isTestFault('F1-scene-chunk') ? 'static' : 'failed');
}
} finally {
if (importAbort === abort) importAbort = null;