From ea0346f4bdcd95fa72bbc8786d4b6bda024ad4b1 Mon Sep 17 00:00:00 2001 From: Marko Paleka Date: Mon, 8 Jun 2026 17:12:31 +0200 Subject: [PATCH] fix: default input-select select_type to 'from-options' New Input Select blocks were created with deepnote_variable_select_type set to null. The @deepnote/blocks serializer validates this field against a strict 'from-options' | 'from-variable' enum and rejects null, which made freshly-added Input Select blocks impossible to save. Default the field to 'from-options' so new blocks serialize cleanly, and add regression tests covering the default value and idempotent round-tripping of the cell value (guarding against repeated JSON escaping). --- .../converters/inputConverters.unit.test.ts | 42 +++++++++++++++++++ src/notebooks/deepnote/deepnoteSchemas.ts | 6 ++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts index 07e930f73b..551d179c79 100644 --- a/src/notebooks/deepnote/converters/inputConverters.unit.test.ts +++ b/src/notebooks/deepnote/converters/inputConverters.unit.test.ts @@ -13,6 +13,7 @@ import { ButtonBlockConverter } from './inputConverters'; import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants'; +import { DeepnoteSelectInputMetadataSchema } from '../deepnoteSchemas'; suite('InputTextBlockConverter', () => { let converter: InputTextBlockConverter; @@ -269,6 +270,47 @@ suite('InputTextareaBlockConverter', () => { }); }); +suite('Input Select block — save/serialize regression', () => { + test('new input-select metadata defaults select_type to a valid enum value, not null', () => { + // The @deepnote/blocks serializer validates deepnote_variable_select_type + // against a strict 'from-options' | 'from-variable' enum and rejects null. + // A null default made freshly-created Input Select blocks impossible to + // save, which also kicked off a runaway content-reformat loop. + const metadata = DeepnoteSelectInputMetadataSchema.parse({ deepnote_variable_name: 'input_1' }); + + assert.strictEqual(metadata.deepnote_variable_select_type, 'from-options'); + }); + + test('round-trips block -> cell -> block -> cell without growing the value (no runaway escaping)', () => { + const converter = new InputSelectBlockConverter(); + const block: DeepnoteBlock = { + blockGroup: 'g', + content: '', + id: 'b1', + metadata: { + deepnote_variable_name: 'input_1', + deepnote_variable_value: 'Option 1', + deepnote_variable_options: ['Option 1', 'Option 2'], + deepnote_variable_select_type: 'from-options', + deepnote_variable_custom_options: ['Option 1', 'Option 2'], + deepnote_variable_selected_variable: '' + }, + sortingKey: 'x', + type: 'input-select' + }; + + const firstCell = converter.convertToCell(block); + converter.applyChangesToBlock(block, firstCell); + const secondCell = converter.convertToCell(block); + + // Each pass must be idempotent: JSON.stringify must not re-escape an + // already-escaped value. Previously the value grew without bound and + // froze the renderer with megabytes of backslashes. + assert.strictEqual(firstCell.value, '"Option 1"'); + assert.strictEqual(secondCell.value, firstCell.value); + }); +}); + suite('InputSelectBlockConverter', () => { let converter: InputSelectBlockConverter; diff --git a/src/notebooks/deepnote/deepnoteSchemas.ts b/src/notebooks/deepnote/deepnoteSchemas.ts index 7c8712b467..39bf5ad433 100644 --- a/src/notebooks/deepnote/deepnoteSchemas.ts +++ b/src/notebooks/deepnote/deepnoteSchemas.ts @@ -117,9 +117,11 @@ export const DeepnoteSelectInputMetadataSchema = DeepnoteBaseInputWithLabelMetad .transform((val) => val ?? DEEPNOTE_SELECT_INPUT_DEFAULT_OPTIONS), deepnote_variable_select_type: z .enum(['from-options', 'from-variable']) - // .string() + // Default to 'from-options' (not null): the @deepnote/blocks serialize + // schema rejects null here, which previously made new Input Select blocks + // impossible to save and could trigger a content-reformat loop. .nullish() - .transform((val) => val ?? null), + .transform((val) => val ?? 'from-options'), deepnote_allow_multiple_values: z .boolean() .nullish()