Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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:
Copy link
Contributor

Choose a reason for hiding this comment

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

@FilipPyrek pls open a pr doing just this for the sake of integrations tests already in main and assign me as a reviewer.

name: Integration Tests
runs-on: ubicloud
timeout-minutes: 30
permissions:
id-token: write
contents: read
packages: read
Comment on lines +150 to +153
Copy link
Member Author

Choose a reason for hiding this comment

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

DevinAI What do you need the permissions for? I thought it's defined at the top-level

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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12'

- 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 integration tests
run: xvfb-run -a -s "-screen 0 1024x768x24" npm run test:integration
env:
VSC_JUPYTER_CI_TEST_GREP: 'Deepnote Integration Tests'

check_licenses:
name: Check Licenses
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions src/kernels/execution/codeExecution.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '@jupyterlab/services/lib/kernel/messages';
import { Deferred, createDeferred } from '../../platform/common/utils/async';
import { NotebookCellOutput, NotebookCellOutputItem } from 'vscode';
import { JVSC_EXTENSION_ID_FOR_TESTS } from '../../test/constants';
import { JVSC_EXTENSION_ID } from '../../platform/common/constants';
Copy link
Member Author

Choose a reason for hiding this comment

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

DevinAI why did you change the import here? I think you can use JVSC_EXTENSION_ID_FOR_TESTS from '../../test/constants'. Check also other files if you changed it there and revet it back. Use the testing constants and if necessary modify the testing constants.


suite('Code Execution', () => {
let disposables: IDisposable[] = [];
Expand Down Expand Up @@ -318,7 +318,7 @@ suite('Code Execution', () => {
});
test('Cancelling pending Internal Jupyter execution code should not interrupt the kernel', async () => {
const code = `print('Hello World')`;
const execution = createExecution(code, JVSC_EXTENSION_ID_FOR_TESTS);
const execution = createExecution(code, JVSC_EXTENSION_ID);

const outputs: NotebookCellOutput[] = [];
disposables.push(execution.onDidEmitOutput((output) => outputs.push(output)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { IFileSystem } from '../../../platform/common/platform/types';
import { RemoteKernelFinderController } from './remoteKernelFinderController';
import { JupyterServerCollection, JupyterServerProvider } from '../../../api';
import { UserJupyterServerPickerProviderId } from '../../../platform/constants';
import { JVSC_EXTENSION_ID_FOR_TESTS } from '../../../test/constants';
import { JVSC_EXTENSION_ID } from '../../../platform/common/constants';

suite(`Remote Kernel Finder Controller`, () => {
let disposables: Disposable[] = [];
Expand All @@ -50,7 +50,7 @@ suite(`Remote Kernel Finder Controller`, () => {
provider: {
id: UserJupyterServerPickerProviderId,
handle: '2',
extensionId: JVSC_EXTENSION_ID_FOR_TESTS
extensionId: JVSC_EXTENSION_ID
}
};
let serverUriStorage: IJupyterServerUriStorage;
Expand Down Expand Up @@ -127,7 +127,7 @@ suite(`Remote Kernel Finder Controller`, () => {
const collectionForRemote = mock<JupyterServerCollection>();
when(collectionForRemote.id).thenReturn(UserJupyterServerPickerProviderId);
when(collectionForRemote.label).thenReturn('Quick Label');
when(collectionForRemote.extensionId).thenReturn(JVSC_EXTENSION_ID_FOR_TESTS);
when(collectionForRemote.extensionId).thenReturn(JVSC_EXTENSION_ID);
const serverProvider = mock<JupyterServerProvider>();
when(serverProvider.provideJupyterServers(anything())).thenResolve();
when(collectionForRemote.serverProvider).thenReturn(instance(serverProvider));
Expand Down
8 changes: 7 additions & 1 deletion src/notebooks/deepnote/deepnoteExplorerView.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { injectable, inject } from 'inversify';
import { commands, window, workspace, type TreeView, Uri, l10n } from 'vscode';
import * as yaml from 'js-yaml';
import { convertIpynbFilesToDeepnoteFile } from '@deepnote/convert';

import { IExtensionContext } from '../../platform/common/types';
import { IDeepnoteNotebookManager } from '../types';
Expand Down Expand Up @@ -357,6 +356,7 @@ export class DeepnoteExplorerView {
const outputFileName = `${projectName}.deepnote`;
const outputPath = Uri.joinPath(workspaceFolder.uri, outputFileName).path;

const convertIpynbFilesToDeepnoteFile = await this.getConverter();
await convertIpynbFilesToDeepnoteFile(inputFilePaths, {
outputPath: outputPath,
projectName: projectName
Expand All @@ -379,6 +379,11 @@ export class DeepnoteExplorerView {
}
}

private async getConverter() {
const { convertIpynbFilesToDeepnoteFile } = await import('@deepnote/convert');
return convertIpynbFilesToDeepnoteFile;
}

private async importJupyterNotebook(): Promise<void> {
if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) {
const selection = await window.showInformationMessage(
Expand Down Expand Up @@ -429,6 +434,7 @@ export class DeepnoteExplorerView {
// File doesn't exist, continue
}

const convertIpynbFilesToDeepnoteFile = await this.getConverter();
await convertIpynbFilesToDeepnoteFile(inputFilePaths, {
outputPath: outputUri.path,
projectName: projectName
Expand Down
4 changes: 2 additions & 2 deletions src/standalone/api/kernels/kernel.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { createMockedNotebookDocument } from '../../../test/datascience/editor-i
import { IControllerRegistration, IVSCodeNotebookController } from '../../../notebooks/controllers/types';
import { createKernelApiForExtension } from './kernel';
import { noop } from '../../../test/core';
import { JVSC_EXTENSION_ID_FOR_TESTS } from '../../../test/constants';
import { JVSC_EXTENSION_ID } from '../../../platform/common/constants';
import { IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
import { NotebookCellOutput } from 'vscode';

Expand Down Expand Up @@ -127,7 +127,7 @@ suite('Kernel Api', () => {
when(kernel.shutdown()).thenResolve();
when(kernel.dispose()).thenCall(() => when(kernel.status).thenReturn('dead'));

const { api } = createKernelApiForExtension(JVSC_EXTENSION_ID_FOR_TESTS, instance(kernel));
const { api } = createKernelApiForExtension(JVSC_EXTENSION_ID, instance(kernel));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of api.executeCode('bogus', token)) {
//
Expand Down
2 changes: 1 addition & 1 deletion src/test/constants.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const EXTENSION_TEST_DIR_FOR_FILES = path.join(
'datascience',
'temp'
);
export const JVSC_EXTENSION_ID_FOR_TESTS = 'ms-toolsai.jupyter';
export const JVSC_EXTENSION_ID_FOR_TESTS = 'Deepnote.vscode-deepnote';

export const SMOKE_TEST_EXTENSIONS_DIR = path.join(
EXTENSION_ROOT_DIR_FOR_TESTS,
Expand Down
2 changes: 1 addition & 1 deletion src/test/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export const JVSC_EXTENSION_ID_FOR_TESTS = 'ms-toolsai.jupyter';
export const JVSC_EXTENSION_ID_FOR_TESTS = 'Deepnote.vscode-deepnote';
export const PerformanceExtensionId = 'ms-toolsai.vscode-notebook-perf';

export type TestSettingsType = {
Expand Down
166 changes: 166 additions & 0 deletions src/test/datascience/notebook/deepnote.vscode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { assert } from 'chai';
import { Uri, workspace, NotebookDocument } from 'vscode';
import { IDisposable } from '../../../platform/common/types';
import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../../common.node';
import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize.node';
import { closeNotebooksAndCleanUpAfterTests, startJupyterServer, getDefaultKernelConnection } from './helper.node';
import { logger } from '../../../platform/logging';
import { IDeepnoteNotebookManager } from '../../../notebooks/types';
import { IKernel, IKernelProvider, INotebookKernelExecution } from '../../../kernels/types';
import { createKernelController } from './executionHelper';

/* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */
suite('Deepnote Integration Tests @kernelCore', function () {
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

Misleading suite tag.

Suite is tagged @kernelCore but tests don't exercise kernels—only file loading and metadata. Consider @deepnote or remove the kernel tag.

-suite('Deepnote Integration Tests @kernelCore', function () {
+suite('Deepnote Integration Tests', function () {
📝 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
suite('Deepnote Integration Tests @kernelCore', function () {
suite('Deepnote Integration Tests', function () {
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around line 12, the
test suite is incorrectly tagged as '@kernelCore' despite not exercising kernel
functionality; update the suite tag to a more accurate label such as '@deepnote'
(or remove the kernel-related tag entirely) so the metadata reflects the tests'
scope and test filtering remains correct.

let api: IExtensionTestApi;
const disposables: IDisposable[] = [];
const deepnoteFilePath = Uri.joinPath(
Uri.file(EXTENSION_ROOT_DIR_FOR_TESTS),
'src',
'test',
'datascience',
'notebook',
'test.deepnote'
);
let nbDocument: NotebookDocument;
let kernel: IKernel;
let kernelExecution: INotebookKernelExecution;
this.timeout(240_000);

suiteSetup(async function () {
logger.info('Suite Setup VS Code Notebook - Deepnote Integration');
this.timeout(240_000);
try {
api = await initialize();
logger.info('After initialize');

await startJupyterServer();
logger.info('After starting Jupyter');

const notebookManager = api.serviceContainer.get<IDeepnoteNotebookManager>(IDeepnoteNotebookManager);
notebookManager.selectNotebookForProject('test-project-id', 'main-notebook-id');

nbDocument = await workspace.openNotebookDocument(deepnoteFilePath);
logger.info(`Opened notebook with ${nbDocument.cellCount} cells`);

const kernelProvider = api.serviceContainer.get<IKernelProvider>(IKernelProvider);
const metadata = await getDefaultKernelConnection();
const controller = createKernelController();
kernel = kernelProvider.getOrCreate(nbDocument, { metadata, resourceUri: nbDocument.uri, controller });
logger.info('Before starting kernel');
await kernel.start();
logger.info('After starting kernel');
kernelExecution = kernelProvider.getKernelExecution(kernel);

logger.info('Suite Setup (completed)');
} catch (e) {
logger.error('Suite Setup (failed) - Deepnote Integration', e);
await captureScreenShot('deepnote-suite');
throw e;
}
});

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

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

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

test('Load .deepnote file', async function () {
logger.debug('Test: Load .deepnote file - starting');

assert.equal(nbDocument.notebookType, 'deepnote', 'Notebook type should be deepnote');
assert.equal(nbDocument.cellCount, 3, 'Notebook should have 3 cells');
assert.equal(nbDocument.metadata?.deepnoteProjectId, 'test-project-id', 'Project ID should match');
assert.equal(nbDocument.metadata?.deepnoteNotebookId, 'main-notebook-id', 'Notebook ID should match');

logger.debug('Test: Load .deepnote file - completed');
});

test('Execute code cell and verify output', async function () {
logger.debug('Test: Execute code cell - starting');

const cell = nbDocument.cellAt(0);
assert.equal(cell.kind, 1, 'First cell should be a code cell');

await kernelExecution.executeCell(cell);

await waitForCondition(
async () => cell.executionSummary?.success === true,
30_000,
'Cell execution did not complete successfully'
);

assert.isAtLeast(cell.executionSummary?.executionOrder || 0, 1, 'Cell should have execution order');
assert.isTrue(cell.executionSummary?.success, 'Cell execution should succeed');
assert.isAtLeast(cell.outputs.length, 1, 'Cell should have at least one output');

const outputText = new TextDecoder().decode(cell.outputs[0].items[0].data).toString();
assert.include(outputText, 'Hello World', 'Output should contain "Hello World"');

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

Harden output decoding (guard items, prefer text/plain)

Avoid assuming items[0] is present/text. Assert items length and pick text/plain when available.

-        assert.isAtLeast(cell.outputs.length, 1, 'Cell should have at least one output');
-
-        const outputText = new TextDecoder().decode(cell.outputs[0].items[0].data).toString();
+        assert.isAtLeast(cell.outputs.length, 1, 'Cell should have at least one output');
+        const firstOutput = cell.outputs[0];
+        assert.isAtLeast(firstOutput.items.length, 1, 'Output should contain at least one item');
+        const textItem = firstOutput.items.find((i) => i.mime === 'text/plain') ?? firstOutput.items[0];
+        const outputText = new TextDecoder().decode(textItem.data);
         assert.include(outputText, 'Hello World', 'Output should contain "Hello World"');
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 101 to
107, the test assumes cell.outputs[0].items[0] exists and contains raw data;
instead assert that outputs and items arrays have expected length, find an item
with mime type 'text/plain' if present (fall back to first item otherwise), then
decode that item's data (or use .text if available) before asserting it includes
"Hello World". Add a clear assertion error message when no output items are
found so the test fails with a helpful message.

logger.debug('Test: Execute code cell - completed');
});

test('Execute multiple code cells with shared state', async function () {
logger.debug('Test: Execute multiple code cells - starting');

const cell1 = nbDocument.cellAt(0);
const cell2 = nbDocument.cellAt(1);

await kernelExecution.executeCell(cell1);
await waitForCondition(
async () => cell1.executionSummary?.success === true,
30_000,
'First cell execution did not complete'
);

await kernelExecution.executeCell(cell2);
await waitForCondition(
async () => cell2.executionSummary?.success === true,
30_000,
'Second cell execution did not complete'
);

assert.isTrue(cell2.executionSummary?.success, 'Second cell should execute successfully');
assert.isAtLeast(cell2.outputs.length, 1, 'Second cell should have output');

const outputText = new TextDecoder().decode(cell2.outputs[0].items[0].data).toString();
assert.include(outputText, '42', 'Output should contain the value 42');

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

Same robustness for multi‑cell output parsing

Mirror the safer extraction here to prevent flakiness with non‑text first items.

-        const outputText = new TextDecoder().decode(cell2.outputs[0].items[0].data).toString();
+        const out2 = cell2.outputs[0];
+        assert.isAtLeast(out2.items.length, 1, 'Second cell output should contain at least one item');
+        const textItem2 = out2.items.find((i) => i.mime === 'text/plain') ?? out2.items[0];
+        const outputText = new TextDecoder().decode(textItem2.data);
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 131 to
136, the test directly accesses cell2.outputs[0].items[0].data which can be
non-text and cause flakiness; change this to mirror the safer extraction used
elsewhere by finding the first output item whose metadata/mimeType indicates
text/plain (or first item with usable data as a fallback), verify items and data
exist, decode that item's data with TextDecoder, and then assert that the
decoded text includes '42'.

logger.debug('Test: Execute multiple code cells - completed');
});

test('Init notebook executes automatically', async function () {
logger.debug('Test: Init notebook execution - starting');

const notebookManager = api.serviceContainer.get<IDeepnoteNotebookManager>(IDeepnoteNotebookManager);

await waitForCondition(
async () => notebookManager.hasInitNotebookBeenRun('test-project-id'),
60_000,
'Init notebook did not execute within timeout'
);

assert.isTrue(
notebookManager.hasInitNotebookBeenRun('test-project-id'),
'Init notebook should have been marked as run'
);

const cell = nbDocument.cellAt(0);
await kernelExecution.executeCell(cell);
await waitForCondition(
async () => cell.executionSummary?.success === true,
30_000,
'Cell execution did not complete'
);

logger.debug('Test: Init notebook execution - completed');
});
});
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
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

Init notebook execution not tested.

PR objectives require verifying that the init notebook (referenced by initNotebookId) executes on kernel start. The test suite doesn't validate this behavior—it only tests manually appended cells.

Verify whether init notebook auto-execution is implemented and add a test that:

  1. Starts the kernel
  2. Checks that init-block-1 was executed (e.g., by inspecting kernel execution history or checking for "Init notebook executed" in outputs)

🏁 Script executed:

#!/bin/bash
# Search for init notebook execution logic in the codebase
rg -nP --type=ts 'initNotebook' -C3

Length of output: 10708



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for test files related to init notebook execution
fd -e "test.ts" -e "test.js" -e "spec.ts" | xargs rg -l "initNotebook|InitNotebook" 2>/dev/null | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if test.deepnote file exists and what it contains
find . -name "test.deepnote" -type f -exec cat {} \;

Length of output: 990


🏁 Script executed:

#!/bin/bash
# Search for any test references to deepnoteKernelAutoSelector or deepnoteInitNotebookRunner
fd -e "test.ts" -e "test.js" -e "spec.ts" | xargs rg -l "deepnoteKernelAutoSelector|deepnoteInitNotebookRunner" 2>/dev/null

Length of output: 0


Add tests for init notebook auto-execution on kernel start.

Init notebook execution is implemented in deepnoteInitNotebookRunner.node.ts and triggered by deepnoteKernelAutoSelector.node.ts on kernel start. However, no tests currently validate this behavior. Add a test that:

  1. Starts a kernel for a project with initNotebookId
  2. Verifies the init notebook's code block (init-block-1) executes
  3. Confirms output contains "Init notebook executed"
🤖 Prompt for AI Agents
In src/test/datascience/notebook/test.deepnote around line 7, there is no test
that verifies init notebook auto-execution when a kernel starts; add a
unit/integration test that (1) creates or loads a project config with
initNotebookId set to "init-notebook-id", (2) starts a kernel for that project
(using the same kernel start flow exercised by
deepnoteKernelAutoSelector.node.ts), (3) waits for the init notebook runner
(deepnoteInitNotebookRunner.node.ts) to execute the init notebook code block
with id "init-block-1", and (4) asserts the kernel output contains the string
"Init notebook executed"; ensure the test hooks into the same event/notification
mechanisms used in production (or stubs/mocks them) and cleans up the
kernel/session after completion.

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