Skip to content

Commit 1380ec8

Browse files
CopilotDonJayamanne
andcommitted
Fix memory management and add initial state detection to InstallPackagesTool
Co-authored-by: DonJayamanne <[email protected]>
1 parent c357458 commit 1380ec8

File tree

2 files changed

+216
-2
lines changed

2 files changed

+216
-2
lines changed

src/standalone/chat/installPackageTool.node.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { isPythonKernelConnection } from '../../kernels/helpers';
1414
import { RestartKernelTool } from './restartKernelTool.node';
1515
import { BaseTool, IBaseToolParams } from './helper';
1616
import { WrappedError } from '../../platform/errors/types';
17+
import { IDisposable } from '../../platform/common/types';
1718

1819

19-
export class InstallPackagesTool extends BaseTool<IInstallPackageParams> {
20+
export class InstallPackagesTool extends BaseTool<IInstallPackageParams> implements IDisposable {
2021
public static toolName = 'notebook_install_packages';
2122
private readonly executedNotebooks = new WeakSet<vscode.NotebookDocument>();
2223
private readonly disposables: vscode.Disposable[] = [];
@@ -26,6 +27,10 @@ export class InstallPackagesTool extends BaseTool<IInstallPackageParams> {
2627
private readonly installationManager: IInstallationChannelManager
2728
) {
2829
super(InstallPackagesTool.toolName);
30+
31+
// Check existing notebooks for already-executed cells
32+
this.initializeExecutedNotebooks();
33+
2934
// Track cell execution for all notebooks
3035
this.disposables.push(
3136
vscode.workspace.onDidChangeNotebookDocument((e) => {
@@ -42,8 +47,29 @@ export class InstallPackagesTool extends BaseTool<IInstallPackageParams> {
4247
);
4348
}
4449

50+
dispose(): void {
51+
this.disposables.forEach(d => d.dispose());
52+
this.disposables.length = 0;
53+
}
54+
55+
private initializeExecutedNotebooks(): void {
56+
// Check all currently open notebooks for executed cells
57+
for (const notebook of vscode.workspace.notebookDocuments) {
58+
if (this.hasAnyExecutedCells(notebook)) {
59+
this.executedNotebooks.add(notebook);
60+
}
61+
}
62+
}
63+
64+
private hasAnyExecutedCells(notebook: vscode.NotebookDocument): boolean {
65+
return notebook.getCells().some(cell =>
66+
cell.kind === vscode.NotebookCellKind.Code &&
67+
typeof cell.executionSummary?.executionOrder === 'number'
68+
);
69+
}
70+
4571
private hasExecutedCells(notebook: vscode.NotebookDocument): boolean {
46-
return this.executedNotebooks.has(notebook);
72+
return this.executedNotebooks.has(notebook) || this.hasAnyExecutedCells(notebook);
4773
}
4874

4975
async invokeImpl(
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)