test sweep: kernel extensions + scheduler residuals (T-91)

Cover the remaining catch / no-op branches in
kernel/src/extensions_manager.dart (duplicate register, unknown
activate, activate-throws, deactivate-throws, keybinding unbind
on deactivate, `all` getter passthrough) and exercise the
SchedulerService event surface (ProjectOpened starts the ticker,
ProjectClosed stops it cleanly). Pushes line coverage to 92.73%.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-14 14:39:45 +02:00
co-authored by Claude
parent b74ab54765
commit a3a10cddf3
2 changed files with 82 additions and 0 deletions
@@ -243,6 +243,62 @@ void main() {
await f.services.extensions.deactivate('preset-only');
});
test('duplicate register() is a silent no-op (warns + skips)', () async {
final ext = _Ext(id: 'dup');
f.services.extensions.register(ext);
f.services.extensions.register(ext); // second call → warn + skip
expect(f.services.extensions.all.where((e) => e.id == 'dup'), hasLength(1));
});
test('activate of an unknown id is a silent no-op (warns + returns)', () async {
await f.services.extensions.activate('nope.no-such');
expect(f.services.extensions.isActivated('nope.no-such'), isFalse);
});
test('activate failure is caught and logged (extension survives)', () async {
f.services.extensions.register(_Ext(
id: 'throws-on-activate',
onActivate: (_) async => throw StateError('kaboom'),
));
await f.services.extensions.activateAll();
expect(f.services.extensions.isActivated('throws-on-activate'), isFalse);
});
test('deactivate failure is caught and logged', () async {
f.services.extensions.register(_Ext(
id: 'throws-on-deactivate',
onDeactivate: () async => throw StateError('kaboom'),
));
await f.services.extensions.activateAll();
expect(f.services.extensions.isActivated('throws-on-deactivate'), isTrue);
await f.services.extensions.deactivate('throws-on-deactivate');
});
test('deactivating a CommandContribution removes its keybinding', () async {
f.services.extensions.register(_Ext(
id: 'with-bound-cmd',
contributions: [
CommandContribution(
id: 'with-bound-cmd.cmd',
command: 'bound.cmd',
defaultBinding: 'ctrl+alt+j',
run: (_) async => IpcResponse.ok(id: '', data: const {}),
),
],
));
await f.services.extensions.activateAll();
expect(f.services.keybindings.commandFor(Keybinding.parse('ctrl+alt+j')), 'bound.cmd');
await f.services.extensions.deactivate('with-bound-cmd');
expect(f.services.keybindings.commandFor(Keybinding.parse('ctrl+alt+j')), isNull);
});
test('all getter yields every registered extension', () async {
f.services.extensions.register(_Ext(id: 'a-iter'));
f.services.extensions.register(_Ext(id: 'b-iter'));
final ids = f.services.extensions.all.map((e) => e.id).toSet();
expect(ids, containsAll(['a-iter', 'b-iter']));
});
test('extension context exposes every kernel service via passthrough getters', () async {
ClideExtensionContext? captured;
f.services.extensions.register(_Ext(
+26
View File
@@ -5,6 +5,7 @@ library;
import 'package:clide/kernel/src/dialog.dart';
import 'package:clide/kernel/src/events/bus.dart';
import 'package:clide/kernel/src/events/types.dart';
import 'package:clide/kernel/src/files.dart';
import 'package:clide/kernel/src/log.dart';
import 'package:clide/kernel/src/os.dart';
@@ -194,5 +195,30 @@ void main() {
s.start();
s.dispose();
});
test('ProjectOpened triggers the initial-cycle ticks', () async {
final bus = DaemonBus();
final s = SchedulerService(bus);
addTearDown(s.dispose);
s.start();
final ticks = <SchedulerTier>[];
final sub = bus.on<SchedulerTick>().listen((e) => ticks.add(e.tier));
addTearDown(sub.cancel);
bus.emit(const ProjectOpened(path: '/tmp/x'));
// The first stagger is 0 ms; pump to flush it onto the queue.
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(ticks, isNotEmpty);
});
test('ProjectClosed stops the ticker without throwing', () async {
final bus = DaemonBus();
final s = SchedulerService(bus);
addTearDown(s.dispose);
s.start();
bus.emit(const ProjectOpened(path: '/tmp/x'));
await Future<void>.delayed(const Duration(milliseconds: 50));
bus.emit(const ProjectClosed());
// Smoke — no throw.
});
});
}