Files
clide/test/daemon/search_commands_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

111 lines
4.4 KiB
Dart

/// Tests for the `search.*` command handlers (T-52 / D-79).
library;
import 'dart:io';
import 'package:clide/clide.dart';
import 'package:clide/src/daemon/search_commands.dart';
import 'package:test/test.dart';
void main() {
late Directory dir;
late RecordingEventSink sink;
late DaemonDispatcher d;
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);
d = DaemonDispatcher();
registerSearchCommands(d, service);
});
tearDown(() async => dir.delete(recursive: true));
Future<IpcResponse> call(String cmd, Map<String, Object?> args) => d.dispatch(IpcRequest(id: '1', cmd: cmd, args: args));
test('search.grep returns a searchId and streams match + done', () async {
// Subscribe before dispatching: the result events are broadcast and
// can fire before a post-call listener would attach.
final doneFuture = sink.stream.firstWhere((e) => e.kind == 'search.done');
final r = await call('search.grep', const {'pattern': 'answer'});
expect(r.ok, isTrue);
final id = r.data['searchId'] as String;
final done = await doneFuture;
expect(done.data['searchId'], id);
expect(done.data['cancelled'], isFalse);
final matches = sink.events.where((e) => e.kind == 'search.match').toList();
expect(matches, isNotEmpty);
final batch = (matches.first.data['matches'] as List).cast<Map>();
expect(batch.first['path'], 'a.dart');
expect(batch.first['line'], 1);
});
test('search.grep with no matches still emits done', () async {
final doneFuture = sink.stream.firstWhere((e) => e.kind == 'search.done');
await call('search.grep', const {'pattern': 'zzz-not-present'});
final done = await doneFuture;
expect(done.data['cancelled'], isFalse);
expect(sink.events.where((e) => e.kind == 'search.match'), isEmpty);
});
test('empty pattern is a userError', () async {
final r = await call('search.grep', const {'pattern': ''});
expect(r.ok, isFalse);
expect(r.error!.kind, IpcErrorKind.userError);
});
test('invalid regex emits a search.error event', () async {
final errFuture = sink.stream.firstWhere((e) => e.kind == 'search.error');
final r = await call('search.grep', const {'pattern': '(unclosed', 'regex': true});
expect(r.ok, isTrue); // the request is accepted; the error streams
final err = await errFuture;
expect(err.data['message'], contains('invalid regex'));
});
test('search.cancel requires a searchId', () async {
final r = await call('search.cancel', const {});
expect(r.ok, isFalse);
expect(r.error!.kind, IpcErrorKind.userError);
});
test('search.cancel acks a (possibly finished) id', () async {
final r = await call('search.cancel', const {'searchId': 'search-0'});
expect(r.ok, isTrue);
expect(r.data['cancelled'], 'search-0');
});
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);
expect(r.data['apply'], isFalse);
expect(r.data['fileCount'], 1);
expect(r.data['totalCount'], 1);
// File is untouched.
expect(File('${dir.path}/a.dart').readAsStringSync(), 'final answer = 42;\n');
});
test('search.replace apply rewrites the matching files', () async {
final r = await call('search.replace', const {'pattern': 'answer', 'replacement': 'result', 'apply': true});
expect(r.ok, isTrue);
expect(r.data['apply'], isTrue);
expect(r.data['filesChanged'], 1);
expect(File('${dir.path}/a.dart').readAsStringSync(), 'final result = 42;\n');
});
test('search.replace with an empty pattern is a userError', () async {
final r = await call('search.replace', const {'pattern': '', 'replacement': 'x'});
expect(r.ok, isFalse);
expect(r.error!.kind, IpcErrorKind.userError);
});
test('search.replace with an invalid regex is a userError', () async {
final r = await call('search.replace', const {'pattern': '(bad', 'regex': true, 'replacement': 'x'});
expect(r.ok, isFalse);
expect(r.error!.kind, IpcErrorKind.userError);
});
}