Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
109 changes: 109 additions & 0 deletions src/test/datascience/notebook/deepnote.vscode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
import { assert } from 'chai';
import { Uri, workspace } from 'vscode';
import { IDisposable } from '../../../platform/common/types';
import { captureScreenShot, IExtensionTestApi } from '../../common.node';
import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize.node';
import { closeNotebooksAndCleanUpAfterTests } from './helper.node';
import { logger } from '../../../platform/logging';
import { IDeepnoteNotebookManager } from '../../../notebooks/types';

/* 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'
);
this.timeout(240_000);
Comment on lines +13 to +23
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

Add blank line after declarations.

Per coding guidelines, add a blank line between the declaration group and this.timeout().

     const deepnoteFilePath = Uri.joinPath(
         Uri.file(EXTENSION_ROOT_DIR_FOR_TESTS),
         'src',
         'test',
         'datascience',
         'notebook',
         'test.deepnote'
     );
+
     this.timeout(240_000);

As per coding guidelines.

🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 13 to 23,
the group of declarations (let api: IExtensionTestApi; const disposables:
IDisposable[] = []; const deepnoteFilePath = Uri.joinPath(...);) should be
followed by a single blank line before the this.timeout(240_000); call; insert
that blank line so there is one empty line separating the declaration block from
the timeout statement to satisfy the coding guidelines.


suiteSetup(async function () {
logger.info('Suite Setup VS Code Notebook - Deepnote Integration');
this.timeout(240_000);
try {
api = await initialize();
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 and verify structure', async function () {
logger.debug('Test: Load .deepnote file - starting');

const notebookManager = api.serviceContainer.get<IDeepnoteNotebookManager>(IDeepnoteNotebookManager);
assert.isOk(notebookManager, 'Notebook manager should be available');

notebookManager.selectNotebookForProject('test-project-id', 'main-notebook-id');

const nbDocument = await workspace.openNotebookDocument(deepnoteFilePath);

logger.debug(`Opened notebook with type: ${nbDocument.notebookType}, cells: ${nbDocument.cellCount}`);

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('Verify notebook cells are correctly deserialized', async function () {
logger.debug('Test: Verify cells - starting');

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

const nbDocument = await workspace.openNotebookDocument(deepnoteFilePath);

assert.isAtLeast(nbDocument.cellCount, 3, 'Notebook should have at least 3 cells');

let hasCodeCell = false;
let hasMarkdownCell = false;

for (let i = 0; i < nbDocument.cellCount; i++) {
const cell = nbDocument.cellAt(i);
if (cell.kind === 1) {
hasCodeCell = true;
}
if (cell.kind === 2) {
hasMarkdownCell = true;
}
}
Comment on lines +85 to +93
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

Use NotebookCellKind enum for clarity.

Magic numbers 1 and 2 reduce readability. Import and use NotebookCellKind.Code and NotebookCellKind.Markup.

Import NotebookCellKind:

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

Then use the enum:

         for (let i = 0; i < nbDocument.cellCount; i++) {
             const cell = nbDocument.cellAt(i);
-            if (cell.kind === 1) {
+            if (cell.kind === NotebookCellKind.Code) {
                 hasCodeCell = true;
             }
-            if (cell.kind === 2) {
+            if (cell.kind === NotebookCellKind.Markup) {
                 hasMarkdownCell = true;
             }
         }
📝 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
for (let i = 0; i < nbDocument.cellCount; i++) {
const cell = nbDocument.cellAt(i);
if (cell.kind === 1) {
hasCodeCell = true;
}
if (cell.kind === 2) {
hasMarkdownCell = true;
}
}
for (let i = 0; i < nbDocument.cellCount; i++) {
const cell = nbDocument.cellAt(i);
if (cell.kind === NotebookCellKind.Code) {
hasCodeCell = true;
}
if (cell.kind === NotebookCellKind.Markup) {
hasMarkdownCell = true;
}
}
Suggested change
for (let i = 0; i < nbDocument.cellCount; i++) {
const cell = nbDocument.cellAt(i);
if (cell.kind === 1) {
hasCodeCell = true;
}
if (cell.kind === 2) {
hasMarkdownCell = true;
}
}
import { NotebookCellKind, Uri, workspace } from 'vscode';
🤖 Prompt for AI Agents
In src/test/datascience/notebook/deepnote.vscode.test.ts around lines 85 to 93,
the loop uses magic numbers 1 and 2 for cell kinds; import NotebookCellKind from
'vscode' (or the correct module) and replace the numeric checks with
NotebookCellKind.Code and NotebookCellKind.Markup respectively; update the
file's imports to include NotebookCellKind and change the if conditions to
compare against those enum members to improve readability.


assert.isTrue(hasCodeCell, 'Notebook should have at least one code cell');
assert.isTrue(hasMarkdownCell, 'Notebook should have at least one markdown cell');

logger.debug('Test: Verify cells - completed');
});

test('Extension services are available', async function () {
logger.debug('Test: Extension services are available - starting');

const notebookManager = api.serviceContainer.get<IDeepnoteNotebookManager>(IDeepnoteNotebookManager);
assert.isOk(notebookManager, 'Notebook manager should be available');

logger.debug('Test: Extension services are available - 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'