test(coverage): lift os/code-block/deeplink for gate headroom

The three worst-covered files were genuinely untested, not edge cases:
os.dart 27%→~85% (inject the process runner so openURL/reveal don't spawn a
real browser), clide_code_block 39%→~90% (expose the byte→char span mapper
as a top-level fn + render tests), deeplink 29%→~75% (the confirm-opens and
not-activated paths). Buys buffer above the 95% floor so a feature batch
doesn't immediately trip the gate. 95.01% → 95.22%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:01:08 +02:00
co-authored by Claude Opus 4.8
parent 1d9000988d
commit 1e8ee2c412
5 changed files with 207 additions and 46 deletions
+22
View File
@@ -5,6 +5,7 @@ library;
import 'dart:async';
import 'package:clide/builtin/deeplink/deeplink.dart';
import 'package:clide/extension/extension.dart' show CommandContribution;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -53,4 +54,25 @@ void main() {
expect(f.services.dialog.isOpen, isFalse);
expect(r.data['status'], 'rejected');
});
testWidgets('confirming an allowlisted link opens the file (T-56)', (tester) async {
await tester.pumpWidget(harness(f, const SizedBox()));
await tester.pump();
final future = f.services.commands.execute('deeplink.invoke', args: ['clide://open?path=/x.dart&line=5']);
await tester.pump();
expect(f.services.dialog.isOpen, isTrue);
f.services.dialog.dismiss(true); // confirm → the handler runs editor.open
await tester.pumpAndSettle();
final r = await future;
expect(r.data['status'], 'opened');
expect(r.data['path'], '/x.dart');
});
test('reports not-activated when invoked before activation', () async {
final ext = DeepLinkExtension(); // never activated → no context
final run = (ext.contributions.single as CommandContribution).run;
expect((await run(['clide://open?path=/x'])).data['status'], 'not-activated');
});
}
+58
View File
@@ -0,0 +1,58 @@
import 'dart:io';
import 'package:clide/kernel/src/events/bus.dart';
import 'package:clide/kernel/src/log.dart';
import 'package:clide/kernel/src/os.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
OsBridge bridge(OsProcessRunner run) => OsBridge(log: Logger(), events: DaemonBus(), run: run);
group('openURL', () {
test('runs the platform open command with the url and returns true on exit 0', () async {
final calls = <List<String>>[];
final os = bridge((exe, args) async {
calls.add([exe, ...args]);
return ProcessResult(0, 0, '', '');
});
expect(await os.openURL('https://example.com'), isTrue);
expect(calls.single.last, 'https://example.com');
expect(calls.single.first, isNotEmpty); // xdg-open / open / start
});
test('returns false on a non-zero exit', () async {
expect(await bridge((_, _) async => ProcessResult(0, 1, '', 'nope')).openURL('x'), isFalse);
});
test('returns false (never throws) when the runner fails', () async {
expect(await bridge((_, _) async => throw 'boom').openURL('x'), isFalse);
});
});
group('reveal', () {
test('runs the platform reveal command and returns true on exit 0', () async {
final calls = <List<String>>[];
final os = bridge((exe, args) async {
calls.add([exe, ...args]);
return ProcessResult(0, 0, '', '');
});
expect(await os.reveal('/tmp/some/file.txt'), isTrue);
expect(calls.single.first, isNotEmpty);
});
test('returns false (never throws) when the runner fails', () async {
expect(await bridge((_, _) async => throw 'x').reveal('/tmp/x'), isFalse);
});
});
test('fire emits an OS lifecycle event', () async {
final bus = DaemonBus();
final seen = <OsLifecycleEvent>[];
final sub = bus.on<OsLifecycleEvent>().listen(seen.add);
OsBridge(log: Logger(), events: bus).fire('resume');
await Future<void>.delayed(Duration.zero);
await sub.cancel();
expect(seen.single.kind, 'resume');
expect(seen.single.subsystem, 'os');
});
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:clide/kernel/src/syntax/syntax_result.dart' show SyntaxSpan;
import 'package:clide/kernel/src/theme/tokens.dart' show SurfaceTokens;
import 'package:clide/widgets/src/clide_code_block.dart';
import 'package:clide/widgets/src/clide_settings.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../helpers/kernel_fixture.dart';
import '../helpers/widget_harness.dart';
void main() {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
Future<SurfaceTokens> pumpForTokens(WidgetTester tester) async {
late SurfaceTokens tokens;
await tester.pumpWidget(
anchoredHarness(
f,
Builder(
builder: (ctx) {
tokens = ClideSettings.theme.of(ctx).surface;
return const SizedBox();
},
),
),
);
return tokens;
}
testWidgets('renders plain source when no language is given', (tester) async {
await tester.pumpWidget(anchoredHarness(f, const ClideCodeBlock(source: 'hello world')));
await tester.pump();
expect(find.byType(ClideCodeBlock), findsOneWidget);
});
testWidgets('renders plain source for an unknown language (no grammar)', (tester) async {
await tester.pumpWidget(anchoredHarness(f, const ClideCodeBlock(source: 'x = 1', language: 'nolang')));
await tester.pump();
expect(find.byType(ClideCodeBlock), findsOneWidget);
});
testWidgets('buildHighlightedSpan maps byte spans to char ranges across multi-byte + surrogate chars', (tester) async {
final tokens = await pumpForTokens(tester);
// 'h😀é' — h=1 byte, 😀=surrogate pair (4 bytes / 2 code units), é=2 bytes.
// Spans on the h (bytes 01) and the é (bytes 57); the emoji gap is filled
// as plain text. The children must reconstruct the whole source — no bytes
// dropped or mis-mapped.
const src = 'h😀é';
final span = buildHighlightedSpan(
src,
const [SyntaxSpan(start: 0, end: 1, role: 'keyword'), SyntaxSpan(start: 5, end: 7, role: 'string')],
const TextStyle(),
tokens,
);
final joined = span.children!.map((c) => (c as TextSpan).text ?? '').join();
expect(joined, src);
});
testWidgets('buildHighlightedSpan clips overlapping spans without duplicating text', (tester) async {
final tokens = await pumpForTokens(tester);
final span = buildHighlightedSpan(
'abcd',
const [SyntaxSpan(start: 0, end: 3, role: 'a'), SyntaxSpan(start: 1, end: 4, role: 'b')], // overlap
const TextStyle(),
tokens,
);
final joined = span.children!.map((c) => (c as TextSpan).text ?? '').join();
expect(joined, 'abcd');
});
}