1+ import { DeepnoteFile , serializeDeepnoteFile } from '@deepnote/blocks' ;
12import { assert } from 'chai' ;
23import * as sinon from 'sinon' ;
3- import { anything , instance , mock , when } from 'ts-mockito' ;
4- import { Disposable , EventEmitter , FileSystemWatcher , NotebookCellKind , NotebookDocument , Uri } from 'vscode' ;
4+ import { anything , instance , mock , reset , resetCalls , verify , when } from 'ts-mockito' ;
5+ import {
6+ Disposable ,
7+ EventEmitter ,
8+ FileSystemWatcher ,
9+ NotebookCellKind ,
10+ NotebookDocument ,
11+ NotebookEdit ,
12+ Uri ,
13+ WorkspaceEdit
14+ } from 'vscode' ;
515
616import type { IControllerRegistration } from '../controllers/types' ;
717import type { IDisposableRegistry } from '../../platform/common/types' ;
@@ -11,7 +21,7 @@ import { IDeepnoteNotebookManager } from '../types';
1121import { DeepnoteFileChangeWatcher } from './deepnoteFileChangeWatcher' ;
1222import { SnapshotService } from './snapshots/snapshotService' ;
1323
14- const validProject = {
24+ const validProject : DeepnoteFile = {
1525 version : '1.0.0' ,
1626 metadata : { createdAt : '2025-01-01T00:00:00Z' } ,
1727 project : {
@@ -21,11 +31,61 @@ const validProject = {
2131 {
2232 id : 'notebook-1' ,
2333 name : 'Notebook 1' ,
24- blocks : [ { id : 'block-1' , type : 'code' , sortingKey : 'a0' , blockGroup : '1' , content : 'print("hello")' } ]
34+ blocks : [
35+ {
36+ id : 'block-1' ,
37+ type : 'code' ,
38+ sortingKey : 'a0' ,
39+ blockGroup : '1' ,
40+ content : 'print("hello")' ,
41+ metadata : { }
42+ }
43+ ]
44+ }
45+ ]
46+ }
47+ } ;
48+
49+ const multiNotebookProject : DeepnoteFile = {
50+ version : '1.0.0' ,
51+ metadata : { createdAt : '2025-01-01T00:00:00Z' } ,
52+ project : {
53+ id : 'project-1' ,
54+ name : 'Multi Notebook Project' ,
55+ notebooks : [
56+ {
57+ id : 'notebook-1' ,
58+ name : 'Notebook 1' ,
59+ blocks : [
60+ {
61+ id : 'block-nb1' ,
62+ type : 'code' ,
63+ sortingKey : 'a0' ,
64+ blockGroup : '1' ,
65+ content : 'print("nb1-new")' ,
66+ metadata : { }
67+ }
68+ ]
69+ } ,
70+ {
71+ id : 'notebook-2' ,
72+ name : 'Notebook 2' ,
73+ blocks : [
74+ {
75+ id : 'block-nb2' ,
76+ type : 'code' ,
77+ sortingKey : 'a0' ,
78+ blockGroup : '1' ,
79+ content : 'print("nb2-new")' ,
80+ metadata : { }
81+ }
82+ ]
2583 }
2684 ]
2785 }
28- } as DeepnoteProject ;
86+ } ;
87+
88+ const multiNotebookYaml = serializeDeepnoteFile ( multiNotebookProject ) ;
2989
3090const waitForTimeoutMs = 5000 ;
3191const waitForIntervalMs = 50 ;
@@ -73,6 +133,7 @@ suite('DeepnoteFileChangeWatcher', () => {
73133 mockedNotebookManager = mock < IDeepnoteNotebookManager > ( ) ;
74134 when ( mockedNotebookManager . getOriginalProject ( anything ( ) ) ) . thenReturn ( validProject ) ;
75135 when ( mockedNotebookManager . getTheSelectedNotebookForAProject ( anything ( ) ) ) . thenReturn ( 'notebook-1' ) ;
136+ when ( mockedNotebookManager . clearNotebookSelection ( anything ( ) ) ) . thenReturn ( ) ;
76137 mockNotebookManager = instance ( mockedNotebookManager ) ;
77138
78139 // Set up FileSystemWatcher mock
@@ -1200,4 +1261,195 @@ project:
12001261 fbOnDidCreate . dispose ( ) ;
12011262 } ) ;
12021263 } ) ;
1264+
1265+ suite ( 'multi-notebook file sync' , ( ) => {
1266+ let workspaceSetCaptures : { uriKey : string ; cellSourceJoined : string } [ ] = [ ] ;
1267+ let workspaceEditSetStub : sinon . SinonStub | undefined ;
1268+
1269+ setup ( ( ) => {
1270+ reset ( mockedNotebookManager ) ;
1271+ when ( mockedNotebookManager . getOriginalProject ( anything ( ) ) ) . thenReturn ( multiNotebookProject ) ;
1272+ when ( mockedNotebookManager . getTheSelectedNotebookForAProject ( anything ( ) ) ) . thenReturn ( 'notebook-1' ) ;
1273+ when ( mockedNotebookManager . clearNotebookSelection ( anything ( ) ) ) . thenReturn ( ) ;
1274+ resetCalls ( mockedNotebookManager ) ;
1275+ workspaceSetCaptures = [ ] ;
1276+ workspaceEditSetStub = sinon . stub ( WorkspaceEdit . prototype , 'set' ) . callsFake ( ( uri : Uri , edits : unknown ) => {
1277+ if ( ! Array . isArray ( edits ) || edits . length === 0 ) {
1278+ return ;
1279+ }
1280+
1281+ const firstEdit = edits [ 0 ] as NotebookEdit ;
1282+ if ( firstEdit ?. newCells && firstEdit . newCells . length > 0 ) {
1283+ workspaceSetCaptures . push ( {
1284+ uriKey : uri . toString ( ) ,
1285+ cellSourceJoined : firstEdit . newCells . map ( ( c ) => c . value ) . join ( '\n' )
1286+ } ) ;
1287+ }
1288+ } ) ;
1289+ } ) ;
1290+
1291+ teardown ( ( ) => {
1292+ workspaceEditSetStub ?. restore ( ) ;
1293+ workspaceEditSetStub = undefined ;
1294+ } ) ;
1295+
1296+ test ( 'should reload each notebook with its own content when multiple notebooks are open' , async ( ) => {
1297+ const basePath = Uri . file ( '/workspace/multi.deepnote' ) ;
1298+ const uriNb1 = basePath . with ( { query : 'view=1' } ) ;
1299+ const uriNb2 = basePath . with ( { query : 'view=2' } ) ;
1300+
1301+ const notebook1 = createMockNotebook ( {
1302+ uri : uriNb1 ,
1303+ metadata : {
1304+ deepnoteProjectId : 'project-1' ,
1305+ deepnoteNotebookId : 'notebook-1'
1306+ } ,
1307+ cells : [
1308+ {
1309+ metadata : { id : 'block-nb1' , type : 'code' } ,
1310+ outputs : [ ] ,
1311+ kind : NotebookCellKind . Code ,
1312+ document : { getText : ( ) => 'print("nb1-old")' , languageId : 'python' }
1313+ }
1314+ ]
1315+ } ) ;
1316+
1317+ const notebook2 = createMockNotebook ( {
1318+ uri : uriNb2 ,
1319+ metadata : {
1320+ deepnoteProjectId : 'project-1' ,
1321+ deepnoteNotebookId : 'notebook-2'
1322+ } ,
1323+ cells : [
1324+ {
1325+ metadata : { id : 'block-nb2' , type : 'code' } ,
1326+ outputs : [ ] ,
1327+ kind : NotebookCellKind . Code ,
1328+ document : { getText : ( ) => 'print("nb2-old")' , languageId : 'python' }
1329+ }
1330+ ]
1331+ } ) ;
1332+
1333+ when ( mockedVSCodeNamespaces . workspace . notebookDocuments ) . thenReturn ( [ notebook1 , notebook2 ] ) ;
1334+ setupMockFs ( multiNotebookYaml ) ;
1335+
1336+ onDidChangeFile . fire ( basePath ) ;
1337+
1338+ await waitFor ( ( ) => applyEditCount >= 2 ) ;
1339+
1340+ assert . strictEqual ( applyEditCount , 2 , 'applyEdit should run once per open notebook' ) ;
1341+ assert . strictEqual ( workspaceSetCaptures . length , 2 , 'each notebook should get a replaceCells edit' ) ;
1342+
1343+ const byUri = new Map ( workspaceSetCaptures . map ( ( c ) => [ c . uriKey , c . cellSourceJoined ] ) ) ;
1344+
1345+ assert . include ( byUri . get ( uriNb1 . toString ( ) ) ?? '' , 'nb1-new' ) ;
1346+ assert . notInclude ( byUri . get ( uriNb1 . toString ( ) ) ?? '' , 'nb2-new' ) ;
1347+ assert . include ( byUri . get ( uriNb2 . toString ( ) ) ?? '' , 'nb2-new' ) ;
1348+ assert . notInclude ( byUri . get ( uriNb2 . toString ( ) ) ?? '' , 'nb1-new' ) ;
1349+ } ) ;
1350+
1351+ test ( 'should clear notebook selection before processing file change' , async ( ) => {
1352+ const basePath = Uri . file ( '/workspace/multi.deepnote' ) ;
1353+ const uriNb1 = basePath . with ( { query : 'a=1' } ) ;
1354+ const uriNb2 = basePath . with ( { query : 'b=2' } ) ;
1355+
1356+ const notebook1 = createMockNotebook ( {
1357+ uri : uriNb1 ,
1358+ metadata : {
1359+ deepnoteProjectId : 'project-1' ,
1360+ deepnoteNotebookId : 'notebook-1'
1361+ } ,
1362+ cells : [
1363+ {
1364+ metadata : { id : 'block-nb1' } ,
1365+ outputs : [ ] ,
1366+ kind : NotebookCellKind . Code ,
1367+ document : { getText : ( ) => 'print("nb1-old")' }
1368+ }
1369+ ]
1370+ } ) ;
1371+
1372+ const notebook2 = createMockNotebook ( {
1373+ uri : uriNb2 ,
1374+ metadata : {
1375+ deepnoteProjectId : 'project-1' ,
1376+ deepnoteNotebookId : 'notebook-2'
1377+ } ,
1378+ cells : [
1379+ {
1380+ metadata : { id : 'block-nb2' } ,
1381+ outputs : [ ] ,
1382+ kind : NotebookCellKind . Code ,
1383+ document : { getText : ( ) => 'print("nb2-old")' }
1384+ }
1385+ ]
1386+ } ) ;
1387+
1388+ when ( mockedVSCodeNamespaces . workspace . notebookDocuments ) . thenReturn ( [ notebook1 , notebook2 ] ) ;
1389+ setupMockFs ( multiNotebookYaml ) ;
1390+
1391+ onDidChangeFile . fire ( basePath ) ;
1392+
1393+ await waitFor ( ( ) => applyEditCount >= 2 ) ;
1394+
1395+ verify ( mockedNotebookManager . clearNotebookSelection ( 'project-1' ) ) . once ( ) ;
1396+ } ) ;
1397+
1398+ test ( 'should not corrupt other notebooks when one notebook triggers a file change' , async ( ) => {
1399+ const basePath = Uri . file ( '/workspace/multi.deepnote' ) ;
1400+ const uriNb1 = basePath . with ( { query : 'n=1' } ) ;
1401+ const uriNb2 = basePath . with ( { query : 'n=2' } ) ;
1402+
1403+ const notebook1 = createMockNotebook ( {
1404+ uri : uriNb1 ,
1405+ metadata : {
1406+ deepnoteProjectId : 'project-1' ,
1407+ deepnoteNotebookId : 'notebook-1'
1408+ } ,
1409+ cells : [
1410+ {
1411+ metadata : { id : 'block-nb1' } ,
1412+ outputs : [ ] ,
1413+ kind : NotebookCellKind . Code ,
1414+ document : { getText : ( ) => 'print("nb1-old")' }
1415+ }
1416+ ]
1417+ } ) ;
1418+
1419+ const notebook2 = createMockNotebook ( {
1420+ uri : uriNb2 ,
1421+ metadata : {
1422+ deepnoteProjectId : 'project-1' ,
1423+ deepnoteNotebookId : 'notebook-2'
1424+ } ,
1425+ cells : [
1426+ {
1427+ metadata : { id : 'block-nb2' } ,
1428+ outputs : [ ] ,
1429+ kind : NotebookCellKind . Code ,
1430+ document : { getText : ( ) => 'print("nb2-old")' }
1431+ }
1432+ ]
1433+ } ) ;
1434+
1435+ when ( mockedVSCodeNamespaces . workspace . notebookDocuments ) . thenReturn ( [ notebook1 , notebook2 ] ) ;
1436+ setupMockFs ( multiNotebookYaml ) ;
1437+
1438+ onDidChangeFile . fire ( basePath ) ;
1439+
1440+ await waitFor ( ( ) => applyEditCount >= 2 ) ;
1441+
1442+ const nb1Cells = workspaceSetCaptures . find ( ( c ) => c . uriKey === uriNb1 . toString ( ) ) ?. cellSourceJoined ;
1443+ const nb2Cells = workspaceSetCaptures . find ( ( c ) => c . uriKey === uriNb2 . toString ( ) ) ?. cellSourceJoined ;
1444+
1445+ assert . isDefined ( nb1Cells ) ;
1446+ assert . isDefined ( nb2Cells ) ;
1447+ assert . notStrictEqual ( nb1Cells , nb2Cells , 'each open notebook must receive distinct deserialized content' ) ;
1448+
1449+ assert . include ( nb1Cells ! , 'nb1-new' ) ;
1450+ assert . include ( nb2Cells ! , 'nb2-new' ) ;
1451+ assert . notInclude ( nb1Cells ! , 'nb2-new' , 'notebook-1 must not receive notebook-2 block content' ) ;
1452+ assert . notInclude ( nb2Cells ! , 'nb1-new' , 'notebook-2 must not receive notebook-1 block content' ) ;
1453+ } ) ;
1454+ } ) ;
12031455} ) ;
0 commit comments