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
+15 -1
View File
@@ -11,13 +11,14 @@ void main() {
late Directory dir;
late RecordingEventSink sink;
late DaemonDispatcher d;
late SearchService service;
setUp(() async {
dir = await Directory.systemTemp.createTemp('clide-search-cmd-');
File('${dir.path}/a.dart').writeAsStringSync('final answer = 42;\n');
File('${dir.path}/b.dart').writeAsStringSync('// no hits here\n');
sink = RecordingEventSink();
final service = SearchService(root: dir, ignore: IgnoreSet([]), events: sink, useIsolates: false);
service = SearchService(root: dir, ignore: IgnoreSet([]), events: sink, useIsolates: false);
d = DaemonDispatcher();
registerSearchCommands(d, service);
});
@@ -78,6 +79,19 @@ void main() {
expect(r.data['cancelled'], 'search-0');
});
test('shutdown cancels in-flight searches and is idempotent (T-367)', () async {
// Subscribe before dispatching — the done event is broadcast.
final doneFuture = sink.stream.firstWhere((e) => e.kind == 'search.done');
final r = await call('search.grep', const {'pattern': 'answer'});
expect(r.ok, isTrue);
await service.shutdown();
await service.shutdown();
// The search still terminates (cancelled or already complete,
// depending on timing) — shutdown must not wedge the stream.
final done = await doneFuture;
expect(done.data['searchId'], r.data['searchId']);
});
test('search.replace preview reports edits without touching disk', () async {
final r = await call('search.replace', const {'pattern': 'answer', 'replacement': 'result'});
expect(r.ok, isTrue);