From def7b3157fcb4416847b46fbebe1649cdc1ff103 Mon Sep 17 00:00:00 2001 From: Anton Pascal Date: Sun, 9 Aug 2026 04:10:04 +0000 Subject: [PATCH] fix(core): keep numeric sanitize diagnostics fail-safe --- .../core/src/store/actions/node-actions.ts | 43 +++++++++++++------ .../actions/node-mutation-sanitize.test.ts | 39 +++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/packages/core/src/store/actions/node-actions.ts b/packages/core/src/store/actions/node-actions.ts index e56623ec63..8ef122f8d3 100644 --- a/packages/core/src/store/actions/node-actions.ts +++ b/packages/core/src/store/actions/node-actions.ts @@ -459,14 +459,24 @@ function formatNumericValue(value: number) { return String(value) } -function numericSanitizeIssuesToMessage(issues: NumericSanitizeIssue[]): string { - return issues - .map((issue) => { - const path = issue.path.map(String).join('.') || '' - const to = issue.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}` - return `${path}: ${formatNumericValue(issue.from)} ${issue.action}${to}` - }) - .join('; ') +export function numericSanitizeIssuesToMessage( + issues: NumericSanitizeIssue[] | null | undefined, +): string { + if (!Array.isArray(issues)) return '' + + try { + return issues + .map((issue) => { + const path = Array.isArray(issue?.path) + ? issue.path.map(String).join('.') || '' + : '' + const to = issue?.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}` + return `${path}: ${formatNumericValue(issue?.from)} ${issue?.action ?? 'sanitized'}${to}` + }) + .join('; ') + } catch { + return '' + } } function warnSanitizedNodeMutation( @@ -474,11 +484,18 @@ function warnSanitizedNodeMutation( nodeId: AnyNodeId, issues: NumericSanitizeIssue[], ) { - console.warn( - `[Scene] Sanitized invalid numeric node ${mutation}`, - nodeId, - numericSanitizeIssuesToMessage(issues), - ) + let message = '' + try { + message = numericSanitizeIssuesToMessage(issues) + } catch { + // Reporting must never interrupt a node mutation. + } + + try { + console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, message) + } catch { + // A broken diagnostic sink must not interrupt a node mutation either. + } } function parseCreatedNode(node: AnyNode, parentId: AnyNodeId | null): AnyNode { diff --git a/packages/core/src/store/actions/node-mutation-sanitize.test.ts b/packages/core/src/store/actions/node-mutation-sanitize.test.ts index 3e54d6d497..091d70e98b 100644 --- a/packages/core/src/store/actions/node-mutation-sanitize.test.ts +++ b/packages/core/src/store/actions/node-mutation-sanitize.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, test } from 'bun:test' import type { AnyNode, AnyNodeId } from '../../schema/types' import useScene from '../use-scene' +import { numericSanitizeIssuesToMessage } from './node-actions' type RafFn = (cb: (t: number) => void) => number ;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (( @@ -87,6 +88,21 @@ function shelf() { return useScene.getState().nodes[SHELF_ID] as Extract } +describe('numeric sanitization diagnostics', () => { + test('formats missing and non-array issue paths defensively', () => { + const issues = [ + { from: Infinity, action: 'dropped' }, + { path: 'width', from: Number.NaN, action: 'dropped' }, + ] as never + + expect(numericSanitizeIssuesToMessage(issues)).toBe( + ': Infinity dropped; : NaN dropped', + ) + expect(numericSanitizeIssuesToMessage(null)).toBe('') + expect(numericSanitizeIssuesToMessage(undefined)).toBe('') + }) +}) + describe('node mutation numeric sanitization', () => { beforeEach(() => { useScene.setState({ @@ -171,6 +187,29 @@ describe('node mutation numeric sanitization', () => { expect(panel.name).toBe('Updated panel') }) + test('updateNodes continues through schema-invalid numeric updates when reporting throws', () => { + const originalConsoleWarn = console.warn + console.warn = () => { + throw new Error('diagnostic sink failed') + } + + try { + useScene.getState().updateNodes([ + { id: SHELF_ID, data: { width: Infinity } as Partial }, + { + id: SOLAR_PANEL_ID, + data: { name: 'Updated after invalid numeric value' } as Partial, + }, + ]) + } finally { + console.warn = originalConsoleWarn + } + + expect(shelf().width).toBe(1.2) + const panel = useScene.getState().nodes[SOLAR_PANEL_ID] as { name?: string } + expect(panel.name).toBe('Updated after invalid numeric value') + }) + test('sanitizes non-finite numeric values during create', () => { const createdId = 'shelf_created' as AnyNodeId