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