test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m1s
Extends the four existing daemon command suites with the verbs + error paths the originals didn't reach: - editor_commands_test (12 new): unreadable-path FileSystemException catch, editor.active with no buffer, editor.activate requires + validates id, editor.read no-active / unknown-id, editor.set-selection no-active / clamped, editor.set-content with + without selection, editor.save no-active, editor.close requires + validates id. - files_commands_test (8 new): files.read happy + missing-path + empty-path + outside-root + missing-file, files.ls outside-root, files.watch idempotent, FilesService.atCwd resolver. - pane_commands_test (10 new): argv-non-string rejection, unknown kind rejection, env passthrough, close / write / focus / resize missing-id and unknown-id validations, write requires bytes_b64 or text, malformed base64 rejection. - pql_commands_test (14 new): pql.files glob + limit, pql.backlinks happy, pql.outlinks missing, pql.tags, pql.query + pql.search happy paths + missing-arg user_error, pql.decisions.read missing + happy, pql.decisions.show with --with-refs / --with-tickets, pql.decisions.list domain filter, pql.tickets.list multi-filter, pql.tickets.show missing + happy, pql.tickets.status missing + partial-args, pql.tickets.board with team. Coverage: src/daemon/editor_commands.dart 64/100 -> 88/100; files_commands.dart 33/70 -> 64/70 (91%); pane_commands.dart 66/92 -> 78/92 (85%); pql_commands.dart 62/149 -> 105/149 (70% — remaining 44 lines are the per-command PqlException catch branches that only fire when the pql subprocess itself fails mid-call). Total coverage 77.92% -> 79.26%; floor bumped to 79. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
4.5 KiB
Dart
132 lines
4.5 KiB
Dart
/// Tests for the `files.*` command handlers.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:clide/clide.dart';
|
|
import 'package:clide/src/daemon/files_commands.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
late Directory sandbox;
|
|
late DaemonDispatcher dispatcher;
|
|
late FilesService files;
|
|
|
|
setUp(() async {
|
|
sandbox = await Directory.systemTemp.createTemp('clide-files-test-');
|
|
// Two files + a subdir + an ignored dir.
|
|
File('${sandbox.path}/README.md').writeAsStringSync('hi');
|
|
File('${sandbox.path}/pubspec.yaml').writeAsStringSync('name: fake');
|
|
Directory('${sandbox.path}/lib').createSync();
|
|
File('${sandbox.path}/lib/main.dart').writeAsStringSync('void main(){}');
|
|
Directory('${sandbox.path}/.dart_tool').createSync();
|
|
File('${sandbox.path}/.dart_tool/hidden').writeAsStringSync('x');
|
|
|
|
final sink = RecordingEventSink();
|
|
files = FilesService(
|
|
root: sandbox,
|
|
events: sink,
|
|
// builtin ignore set hides .dart_tool/, which is what we want.
|
|
ignore: IgnoreSet.builtin(),
|
|
);
|
|
dispatcher = DaemonDispatcher();
|
|
registerFilesCommands(dispatcher, files);
|
|
});
|
|
|
|
tearDown(() async {
|
|
await files.shutdown();
|
|
if (sandbox.existsSync()) sandbox.deleteSync(recursive: true);
|
|
});
|
|
|
|
Future<IpcResponse> call(String cmd, Map<String, Object?> args) {
|
|
return dispatcher.dispatch(IpcRequest(id: '1', cmd: cmd, args: args));
|
|
}
|
|
|
|
test('files.root returns the configured root path', () async {
|
|
final r = await call('files.root', const {});
|
|
expect(r.ok, isTrue);
|
|
expect(r.data['path'], sandbox.absolute.path);
|
|
expect(r.data['ignorePatterns'], greaterThan(0));
|
|
});
|
|
|
|
test('files.ls lists the top-level directory', () async {
|
|
final r = await call('files.ls', const {'path': ''});
|
|
expect(r.ok, isTrue);
|
|
final entries = (r.data['entries'] as List).cast<Map>();
|
|
final names = entries.map((e) => e['name']).toList();
|
|
expect(names, containsAll(['lib', 'README.md', 'pubspec.yaml']));
|
|
expect(names, isNot(contains('.dart_tool')));
|
|
});
|
|
|
|
test('files.ls sorts directories first', () async {
|
|
final r = await call('files.ls', const {'path': ''});
|
|
final entries = (r.data['entries'] as List).cast<Map>();
|
|
expect(entries.first['isDirectory'], isTrue);
|
|
});
|
|
|
|
test('files.ls into a subdirectory returns its contents', () async {
|
|
final r = await call('files.ls', const {'path': 'lib'});
|
|
expect(r.ok, isTrue);
|
|
final names = [
|
|
for (final e in (r.data['entries'] as List).cast<Map>()) e['name'],
|
|
];
|
|
expect(names, ['main.dart']);
|
|
});
|
|
|
|
test('files.watch acks subscription', () async {
|
|
final r = await call('files.watch', const {});
|
|
expect(r.ok, isTrue);
|
|
expect(r.data['subscribed'], isTrue);
|
|
});
|
|
|
|
test('files.read returns the file content', () async {
|
|
final r = await call('files.read', const {'path': 'README.md'});
|
|
expect(r.ok, isTrue);
|
|
expect(r.data['content'], 'hi');
|
|
expect(r.data['path'], 'README.md');
|
|
});
|
|
|
|
test('files.read without a path returns toolError', () async {
|
|
final r = await call('files.read', const {});
|
|
expect(r.ok, isFalse);
|
|
expect(r.error!.kind, IpcErrorKind.toolError);
|
|
expect(r.error!.message, contains('path'));
|
|
});
|
|
|
|
test('files.read with an empty string path returns toolError', () async {
|
|
final r = await call('files.read', const {'path': ''});
|
|
expect(r.ok, isFalse);
|
|
});
|
|
|
|
test('files.read with a path outside the root is rejected', () async {
|
|
final r = await call('files.read', const {'path': '../escape.txt'});
|
|
expect(r.ok, isFalse);
|
|
expect(r.error!.message, contains('outside workspace'));
|
|
});
|
|
|
|
test('files.read returns toolError for a missing file', () async {
|
|
final r = await call('files.read', const {'path': 'does-not-exist.md'});
|
|
expect(r.ok, isFalse);
|
|
expect(r.error!.message, contains('not found'));
|
|
});
|
|
|
|
test('files.ls with a path outside the root is rejected', () async {
|
|
final r = await call('files.ls', const {'path': '../escape'});
|
|
expect(r.ok, isFalse);
|
|
expect(r.error!.message, contains('outside workspace'));
|
|
});
|
|
|
|
test('files.watch is idempotent: a second call still acks subscription', () async {
|
|
final r1 = await call('files.watch', const {});
|
|
final r2 = await call('files.watch', const {});
|
|
expect(r1.ok, isTrue);
|
|
expect(r2.ok, isTrue);
|
|
});
|
|
|
|
test('FilesService.atCwd resolves a workspace root', () {
|
|
final svc = FilesService.atCwd(events: RecordingEventSink());
|
|
expect(svc.root.existsSync(), isTrue);
|
|
addTearDown(svc.shutdown);
|
|
});
|
|
}
|