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:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user