1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ import { assert } from 'chai' ;
5+ import * as sinon from 'sinon' ;
6+ import * as vscode from 'vscode' ;
7+ import { anything , instance , mock , when , verify , reset } from 'ts-mockito' ;
8+ import { InstallPackagesTool } from './installPackageTool.node' ;
9+ import { IKernelProvider } from '../../kernels/types' ;
10+ import { IControllerRegistration } from '../../notebooks/controllers/types' ;
11+ import { IInstallationChannelManager } from '../../platform/interpreter/installer/types' ;
12+
13+ suite ( 'InstallPackagesTool Unit Tests' , ( ) => {
14+ let installPackagesTool : InstallPackagesTool ;
15+ let kernelProvider : IKernelProvider ;
16+ let controllerRegistration : IControllerRegistration ;
17+ let installationManager : IInstallationChannelManager ;
18+
19+ // Mock VS Code objects
20+ let mockNotebook : vscode . NotebookDocument ;
21+ let mockWorkspace : typeof vscode . workspace ;
22+ let mockCells : vscode . NotebookCell [ ] ;
23+
24+ setup ( ( ) => {
25+ kernelProvider = mock < IKernelProvider > ( ) ;
26+ controllerRegistration = mock < IControllerRegistration > ( ) ;
27+ installationManager = mock < IInstallationChannelManager > ( ) ;
28+
29+ // Create mock notebook and cells
30+ mockNotebook = {
31+ uri : vscode . Uri . file ( '/test/notebook.ipynb' ) ,
32+ getCells : sinon . stub ( )
33+ } as any ;
34+
35+ mockCells = [ ] ;
36+ ( mockNotebook . getCells as sinon . SinonStub ) . returns ( mockCells ) ;
37+
38+ // Mock vscode.workspace
39+ mockWorkspace = {
40+ notebookDocuments : [ mockNotebook ] ,
41+ onDidChangeNotebookDocument : sinon . stub ( ) . returns ( { dispose : sinon . stub ( ) } )
42+ } as any ;
43+
44+ // Replace the actual workspace with our mock
45+ sinon . stub ( vscode , 'workspace' ) . value ( mockWorkspace ) ;
46+ } ) ;
47+
48+ teardown ( ( ) => {
49+ if ( installPackagesTool ) {
50+ installPackagesTool . dispose ( ) ;
51+ }
52+ sinon . restore ( ) ;
53+ reset ( kernelProvider ) ;
54+ reset ( controllerRegistration ) ;
55+ reset ( installationManager ) ;
56+ } ) ;
57+
58+ test ( 'Should initialize without executed cells' , ( ) => {
59+ // Create tool with no executed cells
60+ installPackagesTool = new InstallPackagesTool (
61+ instance ( kernelProvider ) ,
62+ instance ( controllerRegistration ) ,
63+ instance ( installationManager )
64+ ) ;
65+
66+ // hasExecutedCells should return false for notebook with no executed cells
67+ const hasExecuted = ( installPackagesTool as any ) . hasExecutedCells ( mockNotebook ) ;
68+ assert . isFalse ( hasExecuted , 'Should return false for notebook with no executed cells' ) ;
69+ } ) ;
70+
71+ test ( 'Should detect already executed cells during initialization' , ( ) => {
72+ // Setup notebook with executed cells
73+ const mockExecutedCell = {
74+ kind : vscode . NotebookCellKind . Code ,
75+ executionSummary : { executionOrder : 1 }
76+ } as vscode . NotebookCell ;
77+
78+ mockCells . push ( mockExecutedCell ) ;
79+
80+ // Create tool - should detect the already executed cell
81+ installPackagesTool = new InstallPackagesTool (
82+ instance ( kernelProvider ) ,
83+ instance ( controllerRegistration ) ,
84+ instance ( installationManager )
85+ ) ;
86+
87+ // hasExecutedCells should return true for notebook with executed cells
88+ const hasExecuted = ( installPackagesTool as any ) . hasExecutedCells ( mockNotebook ) ;
89+ assert . isTrue ( hasExecuted , 'Should return true for notebook with executed cells' ) ;
90+ } ) ;
91+
92+ test ( 'Should track cell execution via event listener' , ( ) => {
93+ let onDidChangeCallback : ( e : vscode . NotebookDocumentChangeEvent ) => void ;
94+
95+ // Capture the callback passed to onDidChangeNotebookDocument
96+ ( mockWorkspace . onDidChangeNotebookDocument as sinon . SinonStub ) . callsFake ( ( callback ) => {
97+ onDidChangeCallback = callback ;
98+ return { dispose : sinon . stub ( ) } ;
99+ } ) ;
100+
101+ installPackagesTool = new InstallPackagesTool (
102+ instance ( kernelProvider ) ,
103+ instance ( controllerRegistration ) ,
104+ instance ( installationManager )
105+ ) ;
106+
107+ // Initially no executed cells
108+ assert . isFalse ( ( installPackagesTool as any ) . hasExecutedCells ( mockNotebook ) ) ;
109+
110+ // Simulate cell execution event
111+ const mockExecutedCell = {
112+ kind : vscode . NotebookCellKind . Code ,
113+ executionSummary : { executionOrder : 1 }
114+ } as vscode . NotebookCell ;
115+
116+ const changeEvent : vscode . NotebookDocumentChangeEvent = {
117+ notebook : mockNotebook ,
118+ cellChanges : [ {
119+ cell : mockExecutedCell ,
120+ document : undefined ,
121+ executionSummary : undefined ,
122+ metadata : undefined ,
123+ outputs : undefined
124+ } ] ,
125+ contentChanges : [ ]
126+ } ;
127+
128+ // Trigger the event
129+ onDidChangeCallback ( changeEvent ) ;
130+
131+ // Now should detect executed cells
132+ assert . isTrue ( ( installPackagesTool as any ) . hasExecutedCells ( mockNotebook ) ) ;
133+ } ) ;
134+
135+ test ( 'Should ignore non-code cells in execution tracking' , ( ) => {
136+ let onDidChangeCallback : ( e : vscode . NotebookDocumentChangeEvent ) => void ;
137+
138+ ( mockWorkspace . onDidChangeNotebookDocument as sinon . SinonStub ) . callsFake ( ( callback ) => {
139+ onDidChangeCallback = callback ;
140+ return { dispose : sinon . stub ( ) } ;
141+ } ) ;
142+
143+ installPackagesTool = new InstallPackagesTool (
144+ instance ( kernelProvider ) ,
145+ instance ( controllerRegistration ) ,
146+ instance ( installationManager )
147+ ) ;
148+
149+ // Simulate markdown cell change
150+ const mockMarkdownCell = {
151+ kind : vscode . NotebookCellKind . Markup ,
152+ executionSummary : { executionOrder : 1 }
153+ } as vscode . NotebookCell ;
154+
155+ const changeEvent : vscode . NotebookDocumentChangeEvent = {
156+ notebook : mockNotebook ,
157+ cellChanges : [ {
158+ cell : mockMarkdownCell ,
159+ document : undefined ,
160+ executionSummary : undefined ,
161+ metadata : undefined ,
162+ outputs : undefined
163+ } ] ,
164+ contentChanges : [ ]
165+ } ;
166+
167+ onDidChangeCallback ( changeEvent ) ;
168+
169+ // Should still return false since only non-code cells were changed
170+ assert . isFalse ( ( installPackagesTool as any ) . hasExecutedCells ( mockNotebook ) ) ;
171+ } ) ;
172+
173+ test ( 'Should dispose of event listeners properly' , ( ) => {
174+ const disposeSpy = sinon . spy ( ) ;
175+ ( mockWorkspace . onDidChangeNotebookDocument as sinon . SinonStub ) . returns ( { dispose : disposeSpy } ) ;
176+
177+ installPackagesTool = new InstallPackagesTool (
178+ instance ( kernelProvider ) ,
179+ instance ( controllerRegistration ) ,
180+ instance ( installationManager )
181+ ) ;
182+
183+ installPackagesTool . dispose ( ) ;
184+
185+ // Verify dispose was called
186+ assert . isTrue ( disposeSpy . calledOnce , 'Event listener dispose should be called' ) ;
187+ } ) ;
188+ } ) ;
0 commit comments