@@ -1470,6 +1470,136 @@ suite('DeepnoteExplorerView - Empty State Commands', () => {
14701470 // Verify error message was shown
14711471 verify ( mockedVSCodeNamespaces . window . showErrorMessage ( anything ( ) ) ) . once ( ) ;
14721472 } ) ;
1473+
1474+ test ( 'should deep clone blocks to prevent shared references' , async ( ) => {
1475+ // This test verifies that duplicating a notebook creates truly independent copies
1476+ // of nested objects like outputs and metadata, not just shallow references
1477+ const projectData : DeepnoteFile = {
1478+ version : '1.0' ,
1479+ metadata : {
1480+ createdAt : '2024-01-01T00:00:00Z' ,
1481+ modifiedAt : '2024-01-01T00:00:00Z'
1482+ } ,
1483+ project : {
1484+ id : 'test-project-id' ,
1485+ name : 'Test Project' ,
1486+ notebooks : [
1487+ {
1488+ id : 'original-notebook-id' ,
1489+ name : 'Original Notebook' ,
1490+ blocks : [
1491+ {
1492+ id : 'block-1' ,
1493+ blockGroup : 'group-1' ,
1494+ type : 'code' ,
1495+ content : 'print("test")' ,
1496+ sortingKey : '0' ,
1497+ version : 1 ,
1498+ executionCount : 5 ,
1499+ outputs : [ { type : 'stream' , text : 'test output' } ] ,
1500+ metadata : { cellId : 'cell-123' , custom : { nested : 'value' } }
1501+ }
1502+ ] ,
1503+ executionMode : 'block'
1504+ }
1505+ ]
1506+ }
1507+ } ;
1508+
1509+ const mockTreeItem : Partial < DeepnoteTreeItem > = {
1510+ type : DeepnoteTreeItemType . Notebook ,
1511+ context : {
1512+ filePath : '/workspace/test-project.deepnote' ,
1513+ projectId : 'test-project-id' ,
1514+ notebookId : 'original-notebook-id'
1515+ } ,
1516+ data : projectData . project . notebooks [ 0 ]
1517+ } ;
1518+
1519+ // Mock file system
1520+ const mockFS = mock < typeof workspace . fs > ( ) ;
1521+ const yamlContent = yaml . dump ( projectData ) ;
1522+ when ( mockFS . readFile ( anything ( ) ) ) . thenReturn ( Promise . resolve ( Buffer . from ( yamlContent , 'utf-8' ) ) ) ;
1523+
1524+ let capturedWriteContent : Uint8Array | undefined ;
1525+ when ( mockFS . writeFile ( anything ( ) , anything ( ) ) ) . thenCall ( ( _uri : Uri , content : Uint8Array ) => {
1526+ capturedWriteContent = content ;
1527+ return Promise . resolve ( ) ;
1528+ } ) ;
1529+
1530+ when ( mockedVSCodeNamespaces . workspace . fs ) . thenReturn ( instance ( mockFS ) ) ;
1531+
1532+ // Stub generateUuid to return predictable IDs
1533+ const generateUuidStub = sinon . stub ( uuidModule , 'generateUuid' ) ;
1534+ generateUuidStub . onCall ( 0 ) . returns ( 'duplicate-notebook-id' ) ;
1535+ generateUuidStub . onCall ( 1 ) . returns ( 'duplicate-block-id' ) ;
1536+ generateUuidStub . onCall ( 2 ) . returns ( 'duplicate-blockgroup-id' ) ;
1537+
1538+ // Execute duplication
1539+ await explorerView . duplicateNotebook ( mockTreeItem as DeepnoteTreeItem ) ;
1540+
1541+ // Parse the written data
1542+ assert . isDefined ( capturedWriteContent , 'File should have been written' ) ;
1543+ const writtenYaml = Buffer . from ( capturedWriteContent ! ) . toString ( 'utf-8' ) ;
1544+ const updatedProjectData = yaml . load ( writtenYaml ) as DeepnoteFile ;
1545+
1546+ // Find original and duplicated notebooks
1547+ const originalNotebook = updatedProjectData . project . notebooks . find (
1548+ ( nb ) => nb . id === 'original-notebook-id'
1549+ ) ;
1550+ const duplicateNotebook = updatedProjectData . project . notebooks . find (
1551+ ( nb ) => nb . id === 'duplicate-notebook-id'
1552+ ) ;
1553+
1554+ assert . isDefined ( originalNotebook , 'Original notebook should exist' ) ;
1555+ assert . isDefined ( duplicateNotebook , 'Duplicate notebook should exist' ) ;
1556+
1557+ // Verify the blocks are truly independent (deep clone)
1558+ const originalBlock = originalNotebook ! . blocks [ 0 ] ;
1559+ const duplicateBlock = duplicateNotebook ! . blocks [ 0 ] ;
1560+
1561+ // Test 1: Verify outputs are not the same reference
1562+ assert . notStrictEqual (
1563+ originalBlock . outputs ,
1564+ duplicateBlock . outputs ,
1565+ 'Outputs should be different array instances'
1566+ ) ;
1567+
1568+ // Test 2: Verify metadata is not the same reference
1569+ if ( originalBlock . metadata && duplicateBlock . metadata ) {
1570+ assert . notStrictEqual (
1571+ originalBlock . metadata ,
1572+ duplicateBlock . metadata ,
1573+ 'Metadata should be different object instances'
1574+ ) ;
1575+
1576+ // Test 3: Verify nested metadata properties are not shared
1577+ if (
1578+ typeof originalBlock . metadata === 'object' &&
1579+ 'custom' in originalBlock . metadata &&
1580+ typeof duplicateBlock . metadata === 'object' &&
1581+ 'custom' in duplicateBlock . metadata
1582+ ) {
1583+ assert . notStrictEqual (
1584+ ( originalBlock . metadata as any ) . custom ,
1585+ ( duplicateBlock . metadata as any ) . custom ,
1586+ 'Nested metadata objects should be different instances'
1587+ ) ;
1588+ }
1589+ }
1590+
1591+ // Test 4: Verify that modifying duplicate doesn't affect original
1592+ // (This would fail with shallow copy)
1593+ duplicateBlock . outputs ! . push ( { type : 'stream' , text : 'new output' } ) ;
1594+ assert . strictEqual (
1595+ originalBlock . outputs ! . length ,
1596+ 1 ,
1597+ 'Original outputs should not be affected by changes to duplicate'
1598+ ) ;
1599+ assert . strictEqual ( duplicateBlock . outputs ! . length , 2 , 'Duplicate outputs should have the new item' ) ;
1600+
1601+ generateUuidStub . restore ( ) ;
1602+ } ) ;
14731603 } ) ;
14741604
14751605 suite ( 'renameProject' , ( ) => {
0 commit comments