tear down the previous workspace's services on project switch (T-367)

buildDispatcher composed a fresh PaneRegistry, FilesService,
SearchService, and EditorRegistry per workspace, but their shutdown()
methods had zero callers — every project switch left the old set's
file watcher emitting into the new workspace's bus and its PTYs
alive. The dispatcher now pairs with a teardown closure that the
serialized swap invokes after the old server stops; the same-path
reuse fast-path drops the unused new set without teardown since its
services are inert until a command starts them. SearchService gains
the shutdown() it was missing (cancels in-flight searches).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:44:24 +02:00
co-authored by Claude Fable 5
parent ba6ab51118
commit 664a8da72e
7 changed files with 97 additions and 11 deletions
+23 -1
View File
@@ -11,6 +11,7 @@ void main() {
late Directory sandbox;
late DaemonDispatcher dispatcher;
late FilesService files;
late RecordingEventSink sink;
setUp(() async {
sandbox = await Directory.systemTemp.createTemp('clide-files-test-');
@@ -22,7 +23,7 @@ void main() {
Directory('${sandbox.path}/.dart_tool').createSync();
File('${sandbox.path}/.dart_tool/hidden').writeAsStringSync('x');
final sink = RecordingEventSink();
sink = RecordingEventSink();
files = FilesService(
root: sandbox,
events: sink,
@@ -229,6 +230,27 @@ void main() {
// assertion is covered in test/files/watcher_test.dart.
});
test('shutdown stops the watcher delivering into the bus (T-367)', () async {
// Project switch tears the old workspace's services down; a leaked
// watcher would keep emitting the OLD workspace's events into the
// new one's bus.
final ack = await call('files.watch', const {});
expect(ack.ok, isTrue);
await Future<void>.delayed(const Duration(milliseconds: 50));
await files.shutdown();
final before = sink.ofKind('files.changed').length;
await File('${sandbox.path}/after-shutdown.txt').writeAsString('x');
await Future<void>.delayed(const Duration(milliseconds: 200));
expect(sink.ofKind('files.changed').length, before, reason: 'no events after shutdown');
});
test('shutdown is idempotent and re-watch works after it', () async {
await files.shutdown();
await files.shutdown();
final r = await call('files.watch', const {});
expect(r.ok, isTrue);
});
test('FilesService.atCwd walks parent dirs looking for .git, falls back to CWD if none', () async {
final deepNoGit = await Directory.systemTemp.createTemp('clide-no-git-');
addTearDown(() => deepNoGit.deleteSync(recursive: true));