fix dead default keymap from undefined focus intents

default.yaml bound tab/shift+tab to focus.next/focus.previous, intent
ids absent from builtinIntents. parseIntentId returns null, so
KeymapLayer.fromYaml throws — and KeymapService.load catches that and
sets _preset = null. The whole default preset was silently dropped at
boot: palette, quick-open, find-in-files, and zoom bindings never fired.

It went unnoticed because every keymap_service_test injects a synthetic
bundle; the shipped asset was never parsed in a test. Add focus.next ->
NextFocusIntent and focus.previous -> PreviousFocusIntent (Flutter-
provided, for correct Tab widget traversal), and a test that reads every
real assets/keymaps/*.yaml through the loader so a future typo fails CI
instead of disabling the keymap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:50:49 +02:00
co-authored by Claude Opus 4.8
parent 2e323990df
commit 4aa24a9898
6 changed files with 112 additions and 0 deletions
@@ -0,0 +1,54 @@
/// Guard: every preset we ship under `assets/keymaps/` must parse
/// through the real loader.
///
/// T-204 — `default.yaml` bound `tab`/`shift+tab` to `focus.next` /
/// `focus.previous`, intent ids that didn't exist in [builtinIntents].
/// `KeymapLayer.fromYaml` throws on an unknown id, and `KeymapService.load`
/// catches that and silently sets `_preset = null` — so the ENTIRE default
/// keymap was dead at runtime (palette, quick-open, find-in-files, scale).
/// It slipped through because every `keymap_service_test` injects a
/// synthetic bundle; the shipped asset was never parsed in a test.
///
/// This test reads the real files from disk and parses them, so a typo in
/// any shipped preset fails CI instead of silently disabling the keymap.
library;
import 'dart:io';
import 'package:clide/kernel/src/keymap/intents.dart';
import 'package:clide/kernel/src/keymap/keymap.dart';
import 'package:flutter/widgets.dart' show NextFocusIntent, PreviousFocusIntent;
import 'package:flutter_test/flutter_test.dart';
void main() {
final dir = Directory('assets/keymaps');
final presets = dir.listSync().whereType<File>().where((f) => f.path.endsWith('.yaml')).toList()..sort((a, b) => a.path.compareTo(b.path));
test('assets/keymaps ships at least the default preset', () {
expect(presets.map((f) => f.uri.pathSegments.last), contains('default.yaml'));
});
for (final file in presets) {
final name = file.uri.pathSegments.last;
test('$name parses through the real loader', () {
final layer = KeymapLayer.fromYaml(file.readAsStringSync());
// A preset that produced zero bindings would mean every entry was
// dropped — almost certainly a schema mistake, not an intentional
// empty file.
expect(layer.bindings, isNotEmpty, reason: '$name produced no bindings');
});
}
test('focus.next / focus.previous are real intent ids', () {
expect(parseIntentId('focus.next'), isA<NextFocusIntent>());
expect(parseIntentId('focus.previous'), isA<PreviousFocusIntent>());
});
test('default.yaml binds Tab / Shift+Tab to focus traversal', () {
final layer = KeymapLayer.fromYaml(
File('assets/keymaps/default.yaml').readAsStringSync(),
);
expect(layer.bindings.any((b) => b.intent is NextFocusIntent), isTrue);
expect(layer.bindings.any((b) => b.intent is PreviousFocusIntent), isTrue);
});
}