Skip to content
Closed
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
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ jobs:
- name: Check licenses
run: npm run check-licenses

integration-tests:
name: Integration Tests - Deepnote
runs-on: ubicloud-standard-2
timeout-minutes: 30
permissions:
id-token: write
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: 'npm'
node-version-file: '.nvmrc'
registry-url: 'https://npm.pkg.github.com'
scope: '@deepnote'

- name: Setup Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5
with:
python-version: '3.12'

Comment on lines +166 to +170
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Ensure Jupyter is available on the runner.

Kernel startup will fail if Jupyter/ipykernel aren’t present. Install minimal deps before tests.

       - name: Setup Python
         uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5
         with:
           python-version: '3.12'

+      - name: Install Python Jupyter deps
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install jupyter ipykernel
+
       - name: Install dependencies
         run: npm ci --prefer-offline --no-audit
         env:
           NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Also applies to: 171-175



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the full CI workflow to see what happens after Python setup
cat .github/workflows/ci.yml | grep -A 50 "Setup Python"

Length of output: 1472


🏁 Script executed:

#!/bin/bash
# Search for jupyter/ipykernel references in codebase
rg -i "jupyter|ipykernel" --type-list | head -20

Length of output: 389


🏁 Script executed:

#!/bin/bash
# Check package files for Jupyter dependencies
find . -name "package.json" -o -name "pyproject.toml" -o -name "requirements*.txt" -o -name "setup.py" | head -20

Length of output: 457


🏁 Script executed:

#!/bin/bash
# Search for kernel or Jupyter in test files
rg -i "kernel" -C3 --type ts --type js --type py | head -30

Length of output: 2003


🏁 Script executed:

#!/bin/bash
# Check pythonFiles/pyproject.toml to see what Python deps are specified
cat ./pythonFiles/pyproject.toml

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Check package.json test script
rg '"test"' ./package.json -A 2 -B 2

Length of output: 388


🏁 Script executed:

#!/bin/bash
# Search for Jupyter/ipykernel without type restriction
rg "jupyter|ipykernel" -i

Length of output: 262776


🏁 Script executed:

#!/bin/bash
# Look for Python kernel startup in test code
rg "startKernel|kernelspec|launch.*kernel" -i -C 3

Length of output: 280309


Install Jupyter + ipykernel on the runner before running tests.

Many tests start a Jupyter server or assert ipykernel is installed (startJupyterServer, multiple datascience tests). The workflow sets up Python but does not install Jupyter/ipykernel — kernel startup will fail during xvfb-run npm test. Add a step right after "Setup Python" to install minimal Python Jupyter deps (add jupyterlab if you need jupyter lab):

  • File: .github/workflows/ci.yml (after Setup Python step)
  • Suggested step:
    python -m pip install --upgrade pip
    python -m pip install jupyter ipykernel
🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 166-170: the workflow sets up Python but
does not install Jupyter/ipykernel, causing tests that start a Jupyter server or
require ipykernel to fail; add a new step immediately after the "Setup Python"
step that upgrades pip and installs the minimal Jupyter dependencies (at least
jupyter and ipykernel, optionally jupyterlab if needed) using python -m pip
install --upgrade pip and python -m pip install jupyter ipykernel so kernel
startup succeeds during tests.

- name: Install dependencies
run: npm ci --prefer-offline --no-audit
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Compile TypeScript
run: npm run compile

- name: Run Deepnote integration tests
run: xvfb-run -a npm test
env:
VSC_JUPYTER_CI_TEST_GREP: 'Deepnote Integration Tests'

Comment on lines +179 to +183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Capture diagnostics on failures (screenshots/logs).

Tests take screenshots on failure; upload them as artifacts to debug CI.

       - name: Run Deepnote integration tests
         run: xvfb-run -a npm test
         env:
           VSC_JUPYTER_CI_TEST_GREP: 'Deepnote Integration Tests'
+
+      - name: Upload screenshots (on failure)
+        if: failure()
+        uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4
+        with:
+          name: deepnote-integration-screenshots
+          path: |
+            **/*screenshot*.png
+            **/screenshots/**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run Deepnote integration tests
run: xvfb-run -a npm test
env:
VSC_JUPYTER_CI_TEST_GREP: 'Deepnote Integration Tests'
- name: Run Deepnote integration tests
run: xvfb-run -a npm test
env:
VSC_JUPYTER_CI_TEST_GREP: 'Deepnote Integration Tests'
- name: Upload screenshots (on failure)
if: failure()
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4
with:
name: deepnote-integration-screenshots
path: |
**/*screenshot*.png
**/screenshots/**
🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 179 to 183: the Deepnote integration test
step runs xvfb-run npm test but does not upload failure artifacts
(screenshots/logs); update the workflow to add a subsequent step that uses
actions/upload-artifact to upload the test failure artifacts (e.g., the
screenshots directory and any log files) with a clear artifact name, run it
conditioned to always() or at least on failure() so artifacts are preserved when
tests fail, and ensure paths match where tests write screenshots and logs and
that the step runs in the same job/workspace so the files are available.

check_licenses:
name: Check Licenses
runs-on: ubuntu-latest
Expand Down
161 changes: 161 additions & 0 deletions src/test/datascience/notebook/deepnote.vscode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove header from new test file (guidelines).

New files shouldn’t include the Microsoft copyright header.

As per coding guidelines.

-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 1 to 2,
remove the Microsoft copyright header present at the top of this new test file;
simply delete those header lines so the file follows project guidelines that new
files must not include the Microsoft copyright header.


/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { assert } from 'chai';
import * as path from '../../../platform/vscode-path/path';
import { Uri, workspace } from 'vscode';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Import NotebookCellKind and use constants, not magic numbers.

Avoid brittle numeric enums in tests.

-import { Uri, workspace } from 'vscode';
+import { NotebookCellKind, Uri, workspace } from 'vscode';

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around line 7, the test
currently imports Uri and workspace and uses magic numeric values for notebook
cell kinds; update the import to also include NotebookCellKind from 'vscode' and
replace any numeric literals (e.g. 0/1 or other numbers used to denote cell
kinds) with the appropriate NotebookCellKind constants (NotebookCellKind.Markup,
NotebookCellKind.Code) so the test uses named enum values instead of brittle
magic numbers.

import { IDisposable } from '../../../platform/common/types';
import { captureScreenShot, IExtensionTestApi } from '../../common.node';
import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize.node';
import {
closeNotebooksAndCleanUpAfterTests,
startJupyterServer,
defaultNotebookTestTimeout,
waitForExecutionCompletedSuccessfully,
getCellOutputs,
getDefaultKernelConnection
} from './helper.node';
import { logger } from '../../../platform/logging';
import { IKernel, IKernelProvider, INotebookKernelExecution } from '../../../kernels/types';
import { createKernelController, TestNotebookDocument } from './executionHelper';

/* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */
suite('Deepnote Integration Tests @kernelCore', function () {
let api: IExtensionTestApi;
const disposables: IDisposable[] = [];
const deepnoteFileUri = Uri.file(
path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'notebook', 'test.deepnote')
);
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use Uri.joinPath per guidelines.

Prefer Uri.joinPath over path.join + Uri.file.

As per coding guidelines.

-    const deepnoteFileUri = Uri.file(
-        path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'notebook', 'test.deepnote')
-    );
+    const deepnoteFileUri = Uri.joinPath(
+        Uri.file(EXTENSION_ROOT_DIR_FOR_TESTS),
+        'src',
+        'test',
+        'datascience',
+        'notebook',
+        'test.deepnote'
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deepnoteFileUri = Uri.file(
path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'notebook', 'test.deepnote')
);
const deepnoteFileUri = Uri.joinPath(
Uri.file(EXTENSION_ROOT_DIR_FOR_TESTS),
'src',
'test',
'datascience',
'notebook',
'test.deepnote'
);
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 27 to 29,
replace the Uri.file(path.join(...)) usage with Uri.joinPath to construct the
file URI; specifically, call Uri.joinPath starting from
Uri.file(EXTENSION_ROOT_DIR_FOR_TESTS) and pass the path segments
('src','test','datascience','notebook','test.deepnote') as separate arguments so
the URI is built using VS Code's recommended API.

this.timeout(120_000);
let notebook: TestNotebookDocument;
let kernel: IKernel;
let kernelExecution: INotebookKernelExecution;

suiteSetup(async function () {
logger.info('Suite Setup Deepnote Integration Tests');
this.timeout(120_000);
try {
api = await initialize();
logger.debug('Before starting Jupyter');
await startJupyterServer();
logger.debug('After starting Jupyter');

logger.debug('Opening .deepnote file');
const nbDocument = await workspace.openNotebookDocument(deepnoteFileUri);
logger.debug(`Opened .deepnote file with ${nbDocument.cellCount} cells`);

notebook = new TestNotebookDocument(nbDocument.uri, 'deepnote' as any, nbDocument.metadata as any, false);

await Promise.all(
nbDocument.getCells().map(async (cell) => {
if (cell.kind === 1) {
return notebook.appendCodeCell(
cell.document.getText(),
cell.document.languageId,
cell.metadata
);
} else {
return notebook.appendMarkdownCell(cell.document.getText(), cell.metadata);
}
Comment on lines +52 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace magic numbers with NotebookCellKind.

1 = Code, 2 = Markup today, but using constants is safer and clearer.

-                    if (cell.kind === 1) {
+                    if (cell.kind === NotebookCellKind.Code) {
                         return notebook.appendCodeCell(
                             cell.document.getText(),
                             cell.document.languageId,
                             cell.metadata
                         );
                     } else {
                         return notebook.appendMarkdownCell(cell.document.getText(), cell.metadata);
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (cell.kind === 1) {
return notebook.appendCodeCell(
cell.document.getText(),
cell.document.languageId,
cell.metadata
);
} else {
return notebook.appendMarkdownCell(cell.document.getText(), cell.metadata);
}
if (cell.kind === NotebookCellKind.Code) {
return notebook.appendCodeCell(
cell.document.getText(),
cell.document.languageId,
cell.metadata
);
} else {
return notebook.appendMarkdownCell(cell.document.getText(), cell.metadata);
}
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 52 to 60,
replace the magic numeric cell.kind checks with the NotebookCellKind enum to
make intent explicit and robust: use NotebookCellKind.Code instead of 1 and
NotebookCellKind.Markup (or NotebookCellKind.Markdown if your VSCode version
uses that name) instead of 2; ensure NotebookCellKind is imported from 'vscode'
at the top of the file if not already, and update the conditional branches to
compare against those enum members.

})
);

const kernelProvider = api.serviceContainer.get<IKernelProvider>(IKernelProvider);
logger.debug('Before creating kernel connection');
const metadata = await getDefaultKernelConnection();
logger.debug('After creating kernel connection');

const controller = createKernelController();
kernel = kernelProvider.getOrCreate(notebook, { metadata, resourceUri: notebook.uri, controller });
logger.debug('Before starting kernel');
await kernel.start();
logger.debug('After starting kernel');
kernelExecution = kernelProvider.getKernelExecution(kernel);
logger.info('Suite Setup (completed)');
} catch (e) {
logger.error('Suite Setup (failed) - Deepnote Integration Tests', e);
await captureScreenShot('deepnote-integration-suite');
throw e;
}
});

setup(function () {
logger.info(`Start Test ${this.currentTest?.title}`);
});

teardown(async function () {
if (this.currentTest?.isFailed()) {
await captureScreenShot(this);
}
logger.info(`Ended Test ${this.currentTest?.title}`);
});

suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables));

test('Load .deepnote file', async () => {
const nbDocument = await workspace.openNotebookDocument(deepnoteFileUri);
assert.equal(nbDocument.notebookType, 'deepnote');
assert.isAtLeast(nbDocument.cellCount, 1, 'Should have at least one cell');
});

test('Kernel starts for .deepnote file', async () => {
assert.isOk(kernel, 'Kernel should be created');
assert.isOk(kernel.session, 'Kernel session should exist');
});

test('Execute code block with output validation', async function () {
this.timeout(defaultNotebookTestTimeout);

const cell = notebook.cells.find((c) => c.document.getText().includes('print("Hello World")'));
assert.isOk(cell, 'Should find cell with print statement');

await kernelExecution.executeCell(cell!);

await waitForExecutionCompletedSuccessfully(cell!);

assert.isTrue(cell!.executionSummary?.success, 'Cell execution should succeed');
assert.isAtLeast(cell!.outputs.length, 0, 'Cell should have outputs');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Vacuous assertion; require at least one output.

Length >= 0 is always true.

-        assert.isAtLeast(cell!.outputs.length, 0, 'Cell should have outputs');
+        assert.isAtLeast(cell!.outputs.length, 1, 'Cell should have outputs');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert.isAtLeast(cell!.outputs.length, 0, 'Cell should have outputs');
assert.isAtLeast(cell!.outputs.length, 1, 'Cell should have outputs');
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around line 118, the
assertion assert.isAtLeast(cell!.outputs.length, 0, 'Cell should have outputs')
is vacuous (always true); change it to require at least one output by asserting
length >= 1 (e.g., use assert.isAtLeast(cell!.outputs.length, 1, 'Cell should
have at least one output') or assert.isAbove(cell!.outputs.length, 0, 'Cell
should have at least one output')).


const output = getCellOutputs(cell!);
assert.include(output, 'Hello World', 'Output should contain "Hello World"');
});

test('Execute multiple code blocks', async function () {
this.timeout(defaultNotebookTestTimeout);

const codeCells = notebook.cells.filter((c) => c.kind === 2); // Code cells
assert.isAtLeast(codeCells.length, 1, 'Should have at least one code cell');

Comment on lines +127 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Wrong kind constant; this filters markup cells, not code.

Use NotebookCellKind.Code.

-        const codeCells = notebook.cells.filter((c) => c.kind === 2); // Code cells
+        const codeCells = notebook.cells.filter((c) => c.kind === NotebookCellKind.Code); // Code cells
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const codeCells = notebook.cells.filter((c) => c.kind === 2); // Code cells
assert.isAtLeast(codeCells.length, 1, 'Should have at least one code cell');
const codeCells = notebook.cells.filter((c) => c.kind === NotebookCellKind.Code); // Code cells
assert.isAtLeast(codeCells.length, 1, 'Should have at least one code cell');
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 127 to
129, the test filters cells using the numeric constant 2 which corresponds to
markup cells; change the filter to use NotebookCellKind.Code instead (import
NotebookCellKind from 'vscode' if not already imported) so the test selects
actual code cells, and keep the assertion as-is.

const firstCell = codeCells[0];
await kernelExecution.executeCell(firstCell);
await waitForExecutionCompletedSuccessfully(firstCell);

assert.isTrue(firstCell.executionSummary?.success, 'First cell execution should succeed');
assert.isAtLeast(firstCell.executionSummary?.executionOrder || 0, 1, 'Should have execution order');
});

test('Verify cell execution order', async function () {
this.timeout(defaultNotebookTestTimeout);

const codeCells = notebook.cells.filter((c) => c.kind === 2 && c.document.getText().trim().length > 0);
if (codeCells.length < 2) {
this.skip();
}

Comment on lines +141 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same issue here; correct the code-cell filter.

Use NotebookCellKind.Code and keep non-empty check.

-        const codeCells = notebook.cells.filter((c) => c.kind === 2 && c.document.getText().trim().length > 0);
+        const codeCells = notebook.cells.filter(
+            (c) => c.kind === NotebookCellKind.Code && c.document.getText().trim().length > 0
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const codeCells = notebook.cells.filter((c) => c.kind === 2 && c.document.getText().trim().length > 0);
if (codeCells.length < 2) {
this.skip();
}
const codeCells = notebook.cells.filter(
(c) => c.kind === NotebookCellKind.Code && c.document.getText().trim().length > 0
);
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 141 to
145, the notebook cell filter uses a magic number (2) for the cell kind; replace
that with NotebookCellKind.Code and keep the non-empty document text check. If
NotebookCellKind is not already imported from 'vscode', add the import. Ensure
the filter reads something like: notebook.cells.filter(c => c.kind ===
NotebookCellKind.Code && c.document.getText().trim().length > 0) and run tests.

const cell1 = codeCells[0];
const cell2 = codeCells[1];

await kernelExecution.executeCell(cell1);
await waitForExecutionCompletedSuccessfully(cell1);

await kernelExecution.executeCell(cell2);
await waitForExecutionCompletedSuccessfully(cell2);

const order1 = cell1.executionSummary?.executionOrder || 0;
const order2 = cell2.executionSummary?.executionOrder || 0;

assert.isAtLeast(order1, 1, 'First cell should have execution order');
assert.isAtLeast(order2, order1 + 1, 'Second cell should have higher execution order');
});
});
39 changes: 39 additions & 0 deletions src/test/datascience/notebook/test.deepnote
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
metadata:
createdAt: '2025-01-01T00:00:00.000Z'
modifiedAt: '2025-01-01T00:00:00.000Z'
project:
id: test-project-id
name: Test Project
initNotebookId: init-notebook-id
notebooks:
- id: init-notebook-id
name: Init Notebook
blocks:
- id: init-block-1
type: code
sortingKey: '0001'
content: |
# This is the init notebook
init_variable = "initialized"
print("Init notebook executed")
- id: main-notebook-id
Comment on lines +11 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Validate fixture schema against the deserializer; consider adding language for code blocks.

If the deserializer keys differ (e.g., expects language), parsing may degrade. Add language to be explicit.

       blocks:
         - id: init-block-1
           type: code
+          language: python
           sortingKey: '0001'
           content: |
             # This is the init notebook
             init_variable = "initialized"
             print("Init notebook executed")
@@
         - id: block-1
           type: code
+          language: python
           sortingKey: '0001'
           content: |
             print("Hello World")
         - id: block-2
           type: code
+          language: python
           sortingKey: '0002'
           content: |
             x = 42
             print(f"The answer is {x}")

Also applies to: 22-33

🤖 Prompt for AI Agents
In src/test/datascience/notebook/test.deepnote around lines 11 to 19 (and also
apply the same change to 22-33), the fixture's code block objects lack an
explicit language key which can break the deserializer that expects a language
field; update each code block object to include a language property (e.g.,
language: "python") and ensure the fixture schema matches the deserializer's
expected keys, then run the deserializer unit tests to verify parsing succeeds.

name: Main Notebook
blocks:
- id: block-1
type: code
sortingKey: '0001'
content: |
print("Hello World")
- id: block-2
type: code
sortingKey: '0002'
content: |
x = 42
print(f"The answer is {x}")
- id: block-3
type: markdown
sortingKey: '0003'
content: |
# Test Markdown
This is a test markdown block.
version: '1.0'