Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
f0de6cd
refactor(deepnote): re-key notebook manager per (projectId, notebookI…
tkislan Jun 23, 2026
005accf
refactor(deepnote): render single notebook + serialize by metadata, d…
tkislan Jun 23, 2026
0ee7f53
feat(deepnote): split legacy multi-notebook files into single-noteboo…
tkislan Jun 23, 2026
8e27889
feat(deepnote): propagate project metadata across sibling files on disk
tkislan Jun 23, 2026
3995237
feat(deepnote): group the explorer by project and add a notebook stat…
tkislan Jun 24, 2026
3e16e20
refactor(deepnote): key the Jupyter server and environment per notebook
tkislan Jun 24, 2026
83d4e91
feat(deepnote): scope snapshots per notebook with a backward-compatib…
tkislan Jun 24, 2026
faa8432
feat(deepnote): run the init notebook per kernel from its sibling file
tkislan Jun 24, 2026
4a43a7d
fix(deepnote): satisfy lint and spell-check on the migration branch
tkislan Jun 24, 2026
f92d173
test(deepnote): use the real @deepnote/convert in unit tests instead …
tkislan Jun 24, 2026
6532d96
fix(deepnote): address code-review findings on the snapshot/serialize…
tkislan Jun 24, 2026
6d46725
fix(deepnote): exact snapshot lookup in the file watcher + close-canc…
tkislan Jun 26, 2026
21611db
refactor(deepnote): remove metadata propagation and project-level exp…
tkislan Jun 30, 2026
7814c6b
Merge origin/main into tk/single-notebook
tkislan Jun 30, 2026
663d31d
Revert unnecessary renamings
tkislan Jun 30, 2026
1643ef5
fix(deepnote): trim indented bullet content before stripping markdown
tkislan Jun 30, 2026
be64263
refactor(deepnote): import convert helpers directly, drop snapshotFil…
tkislan Jun 30, 2026
589845d
refactor(deepnote): update buildSnapshotPath to use object destructuring
tkislan Jun 30, 2026
3a910c1
test(deepnote): remove redundant subagent-written tests
tkislan Jun 30, 2026
dc0b2dd
Merge remote-tracking branch 'origin/main' into tk/single-notebook
tkislan Jul 1, 2026
ee38cf6
feat(deepnote): retire the split-away original as a .legacy backup
tkislan Jul 1, 2026
8c341e0
test(e2e): add multi-notebook split test with per-spec screenshots
tkislan Jul 1, 2026
c0cb36a
test(e2e): add single-notebook-open and init-notebook split/runner su…
tkislan Jul 1, 2026
9395179
test(e2e): add Explorer project-grouping suite
tkislan Jul 1, 2026
05fc076
fix(e2e): open the workspace folder reliably across multiple suites
tkislan Jul 1, 2026
4615f56
test(e2e): add active-notebook status bar suite
tkislan Jul 1, 2026
97e433a
test(e2e): add notebook-management command suite
tkislan Jul 1, 2026
7fba16e
test(e2e): add integrations UI suite
tkislan Jul 1, 2026
ebef08b
test(e2e): add project-rename fan-out suite
tkislan Jul 1, 2026
fc7c4eb
test(e2e): add init-only file edge-case suite
tkislan Jul 1, 2026
f38f3b0
test(e2e): add split-prompt safety suite
tkislan Jul 1, 2026
e6cfcda
test(e2e): add snapshots suite
tkislan Jul 1, 2026
892c625
test(e2e): add environment suite
tkislan Jul 1, 2026
af467d4
test(e2e): implement G2 — env delete stops a closed notebook's server
tkislan Jul 2, 2026
54b5592
fix(deepnote): honour files.enableTrash when deleting a notebook
tkislan Jul 2, 2026
683b87d
test(e2e): strengthen assertions flagged by review (F1-F3, F6-F7)
tkislan Jul 2, 2026
d82c185
docs(deepnote): trim redundant comments across the PR
tkislan Jul 2, 2026
743b1af
test(e2e): add file-watcher suite
tkislan Jul 2, 2026
f8065e1
refactor(deepnote): update file key handling and clean up comments
tkislan Jul 2, 2026
5502f34
test(deepnote): stub isServerRunning with sinon instead of manual rea…
tkislan Jul 2, 2026
81399f8
fix(deepnote): render init-only files as openable leaves and reveal t…
tkislan Jul 2, 2026
96f4b10
fix(deepnote): key the tree file caches by uri.toString() for cross-p…
tkislan Jul 2, 2026
54cc7e6
fix(deepnote): harden Jupyter import and report partial rename failures
tkislan Jul 2, 2026
ffc3ff8
fix(deepnote): clean up orphaned sibling files when a split write fails
tkislan Jul 2, 2026
c49d234
fix(deepnote): harden kernel and snapshot lifecycle
tkislan Jul 2, 2026
ac49c7d
fix(deepnote): mention notebook ID in the integrations error message
tkislan Jul 2, 2026
f9b4874
test(e2e): extract magic delays and dedup shared helpers
tkislan Jul 2, 2026
2bd8f3e
test(e2e): assert init-only file renders as an openable notebook leaf
tkislan Jul 3, 2026
8994093
fix(deepnote): unregister the old server handle only after a switch s…
tkislan Jul 3, 2026
9534176
fix(deepnote): resolve the init notebook for init-only leaf commands
tkislan Jul 3, 2026
cd0d8e2
fix(deepnote): implement getParent so Reveal in Explorer works
tkislan Jul 3, 2026
36fc854
fix(deepnote): report the accurate imported-notebook count on partial…
tkislan Jul 3, 2026
cc43096
test(e2e): use NOTEBOOK_NAME in the init-only delete toast assertion
tkislan Jul 3, 2026
854c6cb
docs(deepnote): trim comments in the latest review-fix changes
tkislan Jul 7, 2026
f5d7b08
docs: sweep comments across the full PR diff
tkislan Jul 8, 2026
b306773
chore: restore preLaunchTask in launch.json
tkislan Jul 8, 2026
d9c313e
chore(cspell): drop the PR's dictionary additions and reword instead
tkislan Jul 8, 2026
67ca8e7
docs: revert comment churn on unchanged code
tkislan Jul 8, 2026
14769f9
test(deepnote): type the server-starter test's private access through…
tkislan Jul 8, 2026
5c74813
test(deepnote): replace force casts with typed seams across the PR's …
tkislan Jul 8, 2026
a90c632
refactor(deepnote): shrink test mirrors — publicize helper, extract b…
tkislan Jul 9, 2026
951daf6
refactor(deepnote): extract ExecutionMetadataTracker from SnapshotSer…
tkislan Jul 9, 2026
ea42807
refactor(deepnote): extract IServerHandleRegistry from the kernel aut…
tkislan Jul 9, 2026
72677c4
test(deepnote): use deepStrictEqual for tracker summary assertions
tkislan Jul 9, 2026
ffd1bc7
test(deepnote): replace runtime-core inline mock with a typed mock mo…
tkislan Jul 11, 2026
aad512a
test(deepnote): drive unit tests through public seams, remove interna…
tkislan Jul 13, 2026
0f91bd7
chore(deepnote): server-config handle keying, canonical DeepnoteFile,…
tkislan Jul 13, 2026
951ba87
style(deepnote): reword 'unstubbed' comments to pass spell check
tkislan Jul 13, 2026
818bde0
Merge remote-tracking branch 'origin/main' into tk/single-notebook
tkislan Jul 13, 2026
ad5b594
refactor(deepnote): replace inline start function with direct serverS…
tkislan Jul 13, 2026
9dc85df
refactor(deepnote): unify tree item structure with extra property
tkislan Jul 13, 2026
f1d6627
chore(deepnote): remove unused DeepnoteKernelStatusIndicator
tkislan Jul 13, 2026
4a8f5ae
refactor(deepnote): replace notebook URI string conversion with getNo…
tkislan Jul 13, 2026
e936f59
refactor(deepnote): simplify DeepnoteInitNotebookRunner interface
tkislan Jul 13, 2026
9b9ece1
refactor(deepnote): import platform file helpers directly, drop re-ex…
tkislan Jul 13, 2026
b9086c1
refactor(deepnote): read snapshot files via readDeepnoteProjectFile
tkislan Jul 13, 2026
48a3098
test(deepnote): count e2e notebooks via schema parse instead of regex
tkislan Jul 13, 2026
f732f8f
refactor(deepnote): drop deprecated DeepnoteProject alias for Deepnot…
tkislan Jul 13, 2026
9d0a0fa
fix(deepnote): clone integrations per cached sibling to prevent share…
tkislan Jul 13, 2026
097d478
test(deepnote): assert exact notebook pair in snapshot save stubs
tkislan Jul 13, 2026
13af342
feat(deepnote): add .deepnote.env.yaml integrations file loader
tkislan Jul 14, 2026
baca484
feat(deepnote): merge .deepnote.env.yaml configs into SQL integration…
tkislan Jul 14, 2026
bc9e7e5
feat(deepnote): restart toolkit server when integration env files change
tkislan Jul 14, 2026
7843ac3
feat(deepnote): register env-file integrations services + add injecti…
tkislan Jul 14, 2026
1a23218
test(deepnote): add shared DeepnoteFile fixtures and adopt across tests
tkislan Jul 14, 2026
c46572e
refactor(deepnote): extract shared flushNotebookDocumentIfDirty guard
tkislan Jul 14, 2026
193c0c8
fix(deepnote): save open dirty notebooks before rename to prevent edi…
tkislan Jul 14, 2026
b7ec400
fix(deepnote): persist integration changes to every sibling file
tkislan Jul 14, 2026
19ab61a
fix(deepnote): roll back split side effects and report failures accur…
tkislan Jul 14, 2026
201f146
fix(deepnote): scope snapshot discovery to the notebook before the fi…
tkislan Jul 14, 2026
9689d5c
chore(deepnote): reword the split prompt to flag the legacy layout
tkislan Jul 14, 2026
2f3e1d7
fix(deepnote): keep Add Notebook from collapsing the tree and compoun…
tkislan Jul 15, 2026
9db519b
refactor(deepnote): inject DeepnoteTreeDataProvider into the explorer
tkislan Jul 15, 2026
bd84273
chore(deepnote): reword split test comment for spell check
tkislan Jul 15, 2026
9742682
test(deepnote): cover the open-dirty sibling in the project-rename e2e
tkislan Jul 15, 2026
c684269
fix(deepnote): give notebook tree items clearer icons and flag legacy…
tkislan Jul 15, 2026
470c4b8
fix(deepnote): harden env-file restart lifecycle (F1–F3)
tkislan Jul 15, 2026
f22af8c
refactor(deepnote): drop env-file restart prompt sequencing
tkislan Jul 15, 2026
7ac83eb
Merge origin/tk/single-notebook into tk/integrations-yaml-file
tkislan Jul 15, 2026
bb30c7e
refactor(deepnote): tidy env-file e2e comments + type the Cancellatio…
tkislan Jul 15, 2026
e94dff9
feat(deepnote): add local integration-env endpoint + live-refresher
tkislan Jul 16, 2026
26ae4d0
feat(deepnote): point toolkit at the local integration-env endpoint
tkislan Jul 16, 2026
f7fe532
refactor(deepnote): live-refresh integration env instead of restarting
tkislan Jul 16, 2026
9e54033
fix(deepnote): set toolkit project secret + validate endpoint token; …
tkislan Jul 16, 2026
61aba0d
style(deepnote): trim comments on the live env-refresh code
tkislan Jul 16, 2026
a881697
refactor(deepnote): rename IntegrationsEnvVarsEndpoint to UserpodApiE…
tkislan Jul 16, 2026
9f42ffb
fix(deepnote): address code-review findings for env-file integrations
tkislan Jul 17, 2026
14d4db1
Merge origin/main into tk/integrations-yaml-file
tkislan Jul 17, 2026
666d584
fix(deepnote): resolve lint and spell-check CI failures
tkislan Jul 17, 2026
2ed997c
fix(deepnote): reword dedup/unstarted instead of dictionary-ignoring …
tkislan Jul 17, 2026
a700a14
refactor(deepnote): inject env in file-config provider instead of esl…
tkislan Jul 17, 2026
1c6d81f
fix(deepnote): address verified code-review findings for env-file int…
tkislan Jul 18, 2026
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
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
"Unconfigured",
"unuse",
"unittests",
"userpod",
"Userpod",
"vegalite",
"venv",
"Venv",
Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,12 @@
"description": "When enabled, outputs are saved to separate snapshot files in a 'snapshots' folder instead of the main .deepnote file.",
"scope": "resource"
},
"deepnote.integrations.envFile.enabled": {
"type": "boolean",
"default": true,
"description": "When enabled, integration credentials are also loaded from a '.deepnote.env.yaml' file (with 'env:' references resolved against '.env' and environment variables) next to the .deepnote file or at the workspace root.",
"scope": "resource"
},
"deepnote.experiments.enabled": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -2703,6 +2709,7 @@
"clsx": "^2.1.1",
"cross-fetch": "^3.1.5",
"d3-format": "^3.1.0",
"dotenv": "^17.2.3",
"encoding": "^0.1.13",
"express": "^5.2.1",
"fast-deep-equal": "^2.0.1",
Expand Down
30 changes: 20 additions & 10 deletions src/kernels/deepnote/deepnoteLspClientManager.node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import { CancellationError } from 'vscode';
import { inject, injectable } from 'inversify';
import { inject, injectable, optional } from 'inversify';
import type {
LanguageClient as LanguageClientType,
LanguageClientOptions,
Expand All @@ -26,7 +26,8 @@ import { noop } from '../../platform/common/utils/misc';
import {
IIntegrationStorage,
IPlatformNotebookEditorProvider,
IPlatformDeepnoteNotebookManager
IPlatformDeepnoteNotebookManager,
ISqlIntegrationEnvVarsProvider
} from '../../platform/notebooks/deepnote/types';
import { ConfigurableDatabaseIntegrationConfig } from '../../platform/notebooks/deepnote/integrationTypes';
import { SqlLspConnection, isSupportedBySqlLsp, convertToSqlLspConnection } from './sqlLspConnectionUtils';
Expand Down Expand Up @@ -64,7 +65,10 @@ export class DeepnoteLspClientManager
@inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage,
@inject(IPlatformNotebookEditorProvider)
private readonly notebookEditorProvider: IPlatformNotebookEditorProvider,
@inject(IPlatformDeepnoteNotebookManager) private readonly notebookManager: IPlatformDeepnoteNotebookManager
@inject(IPlatformDeepnoteNotebookManager) private readonly notebookManager: IPlatformDeepnoteNotebookManager,
@inject(ISqlIntegrationEnvVarsProvider)
@optional()
private readonly sqlIntegrationEnvVars?: ISqlIntegrationEnvVarsProvider
) {
this.disposables.push(this);
}
Expand Down Expand Up @@ -623,13 +627,19 @@ export class DeepnoteLspClientManager

logger.trace(`SQL LSP: Found ${projectIntegrations.length} integrations in project ${projectId}`);

const projectIntegrationConfigs = (
await Promise.all(
projectIntegrations.map((integration) =>
this.integrationStorage.getIntegrationConfig(integration.id)
)
)
).filter((config): config is ConfigurableDatabaseIntegrationConfig => config != null);
// Prefer the merged (SecretStorage + `.deepnote.env.yaml`) configs so file-configured databases also get
// LSP autocomplete/schema (F13); fall back to SecretStorage-only when the merged provider is unavailable (e.g. web).
const projectIntegrationConfigs = this.sqlIntegrationEnvVars
? (await this.sqlIntegrationEnvVars.getMergedConfigs(notebookUri)).filter(
(config): config is ConfigurableDatabaseIntegrationConfig => config.type !== 'pandas-dataframe'
)
: (
await Promise.all(
projectIntegrations.map((integration) =>
this.integrationStorage.getIntegrationConfig(integration.id)
)
)
).filter((config): config is ConfigurableDatabaseIntegrationConfig => config != null);

const connections = projectIntegrationConfigs
.filter((config) => isSupportedBySqlLsp(config.type))
Expand Down
44 changes: 42 additions & 2 deletions src/kernels/deepnote/deepnoteServerStarter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { IProcessServiceFactory } from '../../platform/common/process/types.node
import { IAsyncDisposableRegistry, IDisposable, IOutputChannel } from '../../platform/common/types';
import { sleep } from '../../platform/common/utils/async';
import { generateUuid } from '../../platform/common/uuid';
import { resolveProjectIdForFile } from '../../platform/deepnote/deepnoteProjectIdResolver';
import { DeepnoteServerStartupError } from '../../platform/errors/deepnoteKernelErrors';
import { logger } from '../../platform/logging';
import { ISqlIntegrationEnvVarsProvider } from '../../platform/notebooks/deepnote/types';
import { ISqlIntegrationEnvVarsProvider, IUserpodApiEndpoints } from '../../platform/notebooks/deepnote/types';
import { PythonEnvironment } from '../../platform/pythonEnvironments/info';
import * as path from '../../platform/vscode-path/path';
import { DeepnoteServerInfo, IDeepnoteServerStarter, IDeepnoteToolkitInstaller } from './types';
Expand Down Expand Up @@ -77,7 +78,10 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
@inject(ISqlIntegrationEnvVarsProvider)
@optional()
private readonly sqlIntegrationEnvVars?: ISqlIntegrationEnvVarsProvider
private readonly sqlIntegrationEnvVars?: ISqlIntegrationEnvVarsProvider,
@inject(IUserpodApiEndpoints)
@optional()
private readonly userpodApiEndpoints?: IUserpodApiEndpoints
) {
asyncRegistry.push(this);
}
Expand Down Expand Up @@ -262,6 +266,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
this.outputChannel.appendLine(l10n.t('Starting Deepnote server...'));

const extraEnv = await this.gatherSqlIntegrationEnvVars(deepnoteFileUri, environmentId, token);
await this.applyIntegrationEndpointEnv(extraEnv, deepnoteFileUri);

// Initialize output tracking for error reporting
this.serverOutputByFile.set(fileKey, { stdout: '', stderr: '' });
Expand Down Expand Up @@ -397,6 +402,41 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
return extraEnv;
}

// Skipped unless the endpoint is up and the file has a project id — else the toolkit raises on an unreachable URL.
private async applyIntegrationEndpointEnv(extraEnv: Record<string, string>, deepnoteFileUri: Uri): Promise<void> {
const endpoint = this.userpodApiEndpoints;

if (!endpoint) {
return;
}

// Wait for the initial bind so a kernel starting before the loopback endpoint is listening still gets the env (F3).
await endpoint.ready;

const baseUrl = endpoint.baseUrl;
if (!baseUrl) {
logger.warn(
'DeepnoteServerStarter: integration endpoint is not listening; skipping live integration env injection.'
);

return;
}

const projectId = await resolveProjectIdForFile(deepnoteFileUri);

if (!projectId) {
return;
}

extraEnv['DEEPNOTE_RUNTIME__ENV_INTEGRATION_ENABLED'] = 'true';
extraEnv['DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE'] = 'true';
extraEnv['DEEPNOTE_RUNTIME__WEBAPP_URL'] = baseUrl;
// 2.1.1 dereferences project_secret without a null-check in detached mode; also the endpoint's per-project bearer token.
extraEnv['DEEPNOTE_RUNTIME__PROJECT_SECRET'] = endpoint.getAuthToken(projectId);
// Legacy key (not __PROJECT_ID): also satisfies set_notebook_path's has_env check, avoiding a session-name parse.
extraEnv['DEEPNOTE_PROJECT_ID'] = projectId;
}

/**
* Stream stdout/stderr from the server process to the VSCode output channel.
*/
Expand Down
156 changes: 154 additions & 2 deletions src/kernels/deepnote/deepnoteServerStarter.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { assert } from 'chai';
import * as fakeTimers from '@sinonjs/fake-timers';
import * as sinon from 'sinon';
import { anything, instance, mock, when } from 'ts-mockito';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import { CancellationError, Uri } from 'vscode';

import { serializeDeepnoteFile, type DeepnoteFile } from '@deepnote/blocks';

import { DeepnoteAgentSkillsManager } from './deepnoteAgentSkillsManager.node';
import { DeepnoteServerStarter } from './deepnoteServerStarter.node';
import { IProcessServiceFactory } from '../../platform/common/process/types.node';
import { IAsyncDisposableRegistry, IOutputChannel } from '../../platform/common/types';
import { IDeepnoteToolkitInstaller } from './types';
import { ISqlIntegrationEnvVarsProvider } from '../../platform/notebooks/deepnote/types';
import { ISqlIntegrationEnvVarsProvider, IUserpodApiEndpoints } from '../../platform/notebooks/deepnote/types';
import { PythonEnvironment } from '../../platform/pythonEnvironments/info';
import {
__getStartServerCalls,
__getStopServerCalls,
__resetRuntimeCoreMock
} from '../../test/mocks/deepnoteRuntimeCore';
import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock';

/**
* Unit tests for DeepnoteServerStarter.
Expand Down Expand Up @@ -264,4 +267,153 @@ suite('DeepnoteServerStarter', () => {
});
});
});

/**
* Direct tests for the private `applyIntegrationEndpointEnv`, exercised via a cast so we mutate a
* plain env object rather than intercepting the third-party `startServer`. The four integration env
* vars are only injected when the loopback endpoint is listening AND the file resolves to a project
* id; every other path degrades gracefully to whatever SQL_* vars were already gathered.
*/
suite('applyIntegrationEndpointEnv (integration endpoint env injection)', () => {
const projectFileUri = Uri.file('/workspace/project/notebook-a.deepnote');
const baseUrl = 'http://127.0.0.1:5555';
// A pre-seeded SQL_* var stands in for the env already gathered before the endpoint step runs.
const sqlEnvKey = 'SQL_DEEPNOTE_INTEGRATION_ABC';
const sqlEnvValue = 'postgres://localhost:5432/db';

type WithApplyIntegrationEndpointEnv = {
applyIntegrationEndpointEnv(extraEnv: Record<string, string>, deepnoteFileUri: Uri): Promise<void>;
};

// Starters built with an endpoint arg are tracked so each is disposed after its test.
let extraStarters: DeepnoteServerStarter[];

setup(() => {
resetVSCodeMocks();
extraStarters = [];
});

teardown(async () => {
await Promise.all(extraStarters.map((starter) => starter.dispose()));
});

/**
* Builds a starter wired with the integration endpoint (optional ctor arg at positional index 6),
* whose `baseUrl` getter yields `endpointBaseUrl`. Tracked for disposal.
*/
function createStarterWithEndpoint(endpointBaseUrl: string | undefined): DeepnoteServerStarter {
const endpoint: IUserpodApiEndpoints = {
baseUrl: endpointBaseUrl,
ready: Promise.resolve(),
getAuthToken: () => 'endpoint-token'
};
const starter = new DeepnoteServerStarter(
instance(mockProcessServiceFactory),
instance(mockToolkitInstaller),
instance(mockAgentSkillsManager),
instance(mockOutputChannel),
instance(mockAsyncRegistry),
instance(mockSqlIntegrationEnvVars),
endpoint
);
extraStarters.push(starter);

return starter;
}

function applyIntegrationEndpointEnv(
starter: DeepnoteServerStarter,
extraEnv: Record<string, string>,
deepnoteFileUri: Uri
): Promise<void> {
return (starter as unknown as WithApplyIntegrationEndpointEnv).applyIntegrationEndpointEnv(
extraEnv,
deepnoteFileUri
);
}

/**
* Stubs `workspace.fs.readFile` — the read behind `resolveProjectIdForFile` — to yield the
* serialized `.deepnote` bytes. Returns the mock so tests can assert whether the read happened.
*/
function stubReadFile(fileContents: string): typeof import('vscode').workspace.fs {
const mockFs = mock<typeof import('vscode').workspace.fs>();

when(mockFs.readFile(anything())).thenReturn(Promise.resolve(new TextEncoder().encode(fileContents)));
when(mockedVSCodeNamespaces.workspace.fs).thenReturn(instance(mockFs));

return mockFs;
}

function serializeProjectFile(projectId: string): string {
const file: DeepnoteFile = {
metadata: {
createdAt: '2023-01-01T00:00:00Z',
modifiedAt: '2023-01-02T00:00:00Z'
},
project: {
id: projectId,
name: 'Project',
notebooks: [{ id: 'notebook-1', name: 'Notebook One', blocks: [] }],
settings: {}
},
version: '1.0.0'
};

return serializeDeepnoteFile(file);
}

test('injects all five integration env vars (preserving pre-seeded SQL_* keys) when the endpoint is listening and the file has a project id', async () => {
const mockFs = stubReadFile(serializeProjectFile('the-project-id'));
const starter = createStarterWithEndpoint(baseUrl);

const extraEnv: Record<string, string> = { [sqlEnvKey]: sqlEnvValue };
await applyIntegrationEndpointEnv(starter, extraEnv, projectFileUri);

assert.deepStrictEqual(extraEnv, {
[sqlEnvKey]: sqlEnvValue,
DEEPNOTE_RUNTIME__ENV_INTEGRATION_ENABLED: 'true',
DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE: 'true',
DEEPNOTE_RUNTIME__WEBAPP_URL: baseUrl,
DEEPNOTE_RUNTIME__PROJECT_SECRET: 'endpoint-token',
DEEPNOTE_PROJECT_ID: 'the-project-id'
});
// The enabled path must resolve the project id from the file.
verify(mockFs.readFile(anything())).once();
});

test('injects nothing and does NOT read the file when the endpoint has no baseUrl', async () => {
const mockFs = stubReadFile(serializeProjectFile('the-project-id'));
const starter = createStarterWithEndpoint(undefined);

const extraEnv: Record<string, string> = { [sqlEnvKey]: sqlEnvValue };
await applyIntegrationEndpointEnv(starter, extraEnv, projectFileUri);

assert.deepStrictEqual(extraEnv, { [sqlEnvKey]: sqlEnvValue });
// A missing baseUrl short-circuits BEFORE resolving the project id — no file read.
verify(mockFs.readFile(anything())).never();
});

test('injects nothing and does NOT read the file when no endpoint was injected at all', async () => {
// The outer-suite `serverStarter` is constructed WITHOUT the optional endpoint arg.
const mockFs = stubReadFile(serializeProjectFile('the-project-id'));

const extraEnv: Record<string, string> = { [sqlEnvKey]: sqlEnvValue };
await applyIntegrationEndpointEnv(serverStarter, extraEnv, projectFileUri);

assert.deepStrictEqual(extraEnv, { [sqlEnvKey]: sqlEnvValue });
verify(mockFs.readFile(anything())).never();
});

test('injects nothing when the endpoint is listening but the file resolves to no project id', async () => {
// A schema-valid `.deepnote` whose project.id is empty — `resolveProjectIdForFile` yields a falsy id.
stubReadFile(serializeProjectFile(''));
const starter = createStarterWithEndpoint(baseUrl);

const extraEnv: Record<string, string> = { [sqlEnvKey]: sqlEnvValue };
await applyIntegrationEndpointEnv(starter, extraEnv, projectFileUri);

assert.deepStrictEqual(extraEnv, { [sqlEnvKey]: sqlEnvValue });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IFederatedAuthTokenStorage } from '../types';

/**
* Node-only bridge that restarts kernels when a federated integration's token changes, clearing stale
* `os.environ` mutations and kernel globals. Separate from {@link IntegrationKernelRestartHandler} because
* `os.environ` mutations and kernel globals. Separate from {@link IntegrationEnvRefreshHandler} because
* {@link IFederatedAuthTokenStorage} is node-only.
*/
@injectable()
Expand Down
Loading
Loading