diff --git a/server/indexer/parser.ts b/server/indexer/parser.ts index 9e97157..f02196e 100644 --- a/server/indexer/parser.ts +++ b/server/indexer/parser.ts @@ -61,7 +61,30 @@ export function classifyEvent(topics: string[]): EventType { /** * Parse event data into a structured record. */ +/** Event payload schema versions this parser can decode. */ +export const SUPPORTED_EVENT_SCHEMA_VERSIONS: ReadonlySet = new Set([1]); + +export class UnsupportedSchemaError extends Error { + constructor(public readonly schemaVersion: unknown) { + super(`Unsupported event schemaVersion ${String(schemaVersion)}; refusing to index (update parser before deploying this contract)`); + this.name = 'UnsupportedSchemaError'; + } +} + +/** + * Version gate (#757): an event that declares a schemaVersion this parser does + * not support halts indexing instead of silently dropping fields. Events without + * a declared version keep the legacy decode path. + */ +export function assertSupportedSchema(data: any): void { + if (data && typeof data === 'object' && data.schemaVersion !== undefined + && !SUPPORTED_EVENT_SCHEMA_VERSIONS.has(Number(data.schemaVersion))) { + throw new UnsupportedSchemaError(data.schemaVersion); + } +} + export function parseEvent(topics: string[], data: any): ParsedEvent { + assertSupportedSchema(data); const eventType = classifyEvent(topics); const parsed: ParsedEvent = { diff --git a/tests/e2e/layout.spec.ts b/tests/e2e/layout.spec.ts index e7b8cc5..3194126 100644 --- a/tests/e2e/layout.spec.ts +++ b/tests/e2e/layout.spec.ts @@ -179,6 +179,12 @@ for (const route of ROUTES) { document.documentElement.dataset.layoutSnapshot = 'true' }) + // Live map tiles and canvas pins re-raster on every capture, so they are masked, + // and CSS animations (mdpulse/mddash) are frozen for a deterministic layout diff. + await page.addStyleTag({ + content: '*,*::before,*::after{animation-play-state:paused!important;transition:none!important}', + }) + if (route.freezeCss) { await page.addStyleTag({ content: route.freezeCss }) // Give the injected style one frame to repaint before capturing. @@ -188,8 +194,8 @@ for (const route of ROUTES) { await expect(page).toHaveScreenshot(`layout-${route.name}.png`, { animations: 'disabled', caret: 'hide', - // Video frames are the only pixel source that cannot be replayed. - mask: [page.locator('video')], + // Video frames and live map canvases cannot be replayed deterministically. + mask: [page.locator('video'), page.locator('canvas, .mapboxgl-map')], maxDiffPixelRatio: 0.05, }) })