Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/notebooks/deepnote/converters/inputConverters.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
6 changes: 4 additions & 2 deletions src/notebooks/deepnote/deepnoteSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading