import 'dart:io'; import 'package:clide/clide.dart'; import 'package:clide/src/daemon/editor_commands.dart'; import 'package:clide/src/editor/registry.dart'; import 'package:test/test.dart'; void main() { late Directory sandbox; late DaemonDispatcher dispatcher; late EditorRegistry reg; setUp(() async { sandbox = await Directory.systemTemp.createTemp('clide-ed-cmd-test-'); await File('${sandbox.path}/doc.md').writeAsString('alpha beta'); final sink = RecordingEventSink(); reg = EditorRegistry(events: sink, workspaceRoot: sandbox); dispatcher = DaemonDispatcher(); registerEditorCommands(dispatcher, reg); }); tearDown(() async { await reg.shutdown(); if (sandbox.existsSync()) sandbox.deleteSync(recursive: true); }); Future call(String cmd, [Map args = const {}]) { return dispatcher.dispatch(IpcRequest(id: '1', cmd: cmd, args: args)); } test('editor.open requires a path', () async { final r = await call('editor.open'); expect(r.ok, isFalse); expect(r.error!.kind, 'user_error'); }); test('editor.open + editor.active round-trip', () async { final open = await call('editor.open', {'path': 'doc.md'}); expect(open.ok, isTrue); final id = open.data['id']! as String; expect(id, startsWith('b_')); final active = await call('editor.active'); expect(active.ok, isTrue); final act = active.data['active']! as Map; expect(act['id'], id); expect(act['path'], 'doc.md'); }); test('CLI positional path binds to editor.open (T-232)', () async { // The C client sends the argv shape {positional:[...]}; the registered // schema's normalize must map positional[0] -> path so the handler opens // it (rather than returning "path is required"). final r = await call('editor.open', { 'positional': ['doc.md'], }); expect(r.ok, isTrue, reason: r.error?.message); expect(r.data['path'], 'doc.md'); }); test('CLI --line flag coerces to a number and binds (T-232)', () async { final r = await call('editor.open', { 'positional': ['doc.md'], 'flags': {'line': '1'}, }); expect(r.ok, isTrue, reason: r.error?.message); expect(r.data['path'], 'doc.md'); }); test('editor.open with a 1-based line jumps the initial selection (T-52)', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n'); final r = await call('editor.open', {'path': 'multi.txt', 'line': 3}); expect(r.ok, isTrue); final sel = r.data['selection'] as Map; // Line 3 starts after 'one\n' + 'two\n' = 8 characters. expect(sel['start'], 8); expect(sel['end'], 8); }); test('editor.open without a line opens at the top', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\n'); final r = await call('editor.open', {'path': 'multi.txt'}); expect((r.data['selection'] as Map)['start'], 0); }); test('editor.open with an out-of-range line clamps to the content end', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\n'); // 8 chars final r = await call('editor.open', {'path': 'multi.txt', 'line': 999}); expect((r.data['selection'] as Map)['start'], 8); }); test('editor.goto-line jumps the active buffer to a 1-based line (T-407)', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n'); await call('editor.open', {'path': 'multi.txt'}); final r = await call('editor.goto-line', {'line': 3}); expect(r.ok, isTrue, reason: r.error?.message); expect(r.data['line'], 3); // Line 3 starts after 'one\n' + 'two\n' = 8 characters. final read = await call('editor.read'); expect((read.data['selection'] as Map)['start'], 8); }); test('editor.goto-line clamps an out-of-range line to the content end', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\n'); // 8 chars await call('editor.open', {'path': 'multi.txt'}); final r = await call('editor.goto-line', {'line': 999}); expect(r.ok, isTrue); final read = await call('editor.read'); expect((read.data['selection'] as Map)['start'], 8); }); test('editor.goto-line rejects a non-positive line', () async { await call('editor.open', {'path': 'doc.md'}); final r = await call('editor.goto-line', {'line': 0}); expect(r.ok, isFalse); expect(r.error!.kind, 'user_error'); }); test('editor.goto-line with no active buffer is not-found', () async { final r = await call('editor.goto-line', {'line': 2}); expect(r.ok, isFalse); expect(r.error!.kind, 'not_found'); }); test('CLI positional line binds to editor.goto-line (T-232)', () async { await File('${sandbox.path}/multi.txt').writeAsString('one\ntwo\nthree\n'); await call('editor.open', {'path': 'multi.txt'}); final r = await call('editor.goto-line', { 'positional': ['2'], }); expect(r.ok, isTrue, reason: r.error?.message); expect(r.data['line'], 2); }); test('editor.insert without id targets the active buffer', () async { await call('editor.open', {'path': 'doc.md'}); final r = await call('editor.insert', {'text': 'X '}); expect(r.ok, isTrue); expect(r.data['inserted'], 2); final read = await call('editor.read'); expect((read.data['content'] as String).startsWith('X '), isTrue); }); test('editor.replace-selection swaps selected range', () async { final open = await call('editor.open', {'path': 'doc.md'}); final id = open.data['id'] as String?; await call('editor.set-selection', { 'id': id, 'selection': {'start': 0, 'end': 5}, // 'alpha' }); final r = await call('editor.replace-selection', {'text': 'gamma'}); expect(r.ok, isTrue); final read = await call('editor.read'); expect(read.data['content'], 'gamma beta'); }); test('editor.save persists to disk', () async { await call('editor.open', {'path': 'doc.md'}); await call('editor.insert', {'text': 'Z '}); final save = await call('editor.save'); expect(save.ok, isTrue); final disk = await File('${sandbox.path}/doc.md').readAsString(); expect(disk.startsWith('Z '), isTrue); }); test('editor.close removes the buffer', () async { final open = await call('editor.open', {'path': 'doc.md'}); final id = open.data['id'] as String?; final r = await call('editor.close', {'id': id}); expect(r.ok, isTrue); final list = await call('editor.list'); expect((list.data['buffers'] as List), isEmpty); }); test('editor.list includes all open buffers', () async { await File('${sandbox.path}/a.md').writeAsString('a'); await File('${sandbox.path}/b.md').writeAsString('b'); await call('editor.open', {'path': 'a.md'}); await call('editor.open', {'path': 'b.md'}); final r = await call('editor.list'); final names = [for (final b in (r.data['buffers'] as List).cast()) b['path']]; expect(names, containsAll(['a.md', 'b.md'])); }); test('insert on unknown id returns not-found', () async { final r = await call('editor.insert', {'id': 'b_404', 'text': 'x'}); expect(r.ok, isFalse); expect(r.error!.code, IpcExitCode.notFound); }); test('editor.open returns toolError for an unreadable path', () async { // Create a file then chmod 000 so reading fails with a FileSystemException. final unreadable = File('${sandbox.path}/locked.md'); await unreadable.writeAsString('x'); await Process.run('chmod', ['000', unreadable.path]); addTearDown(() async { await Process.run('chmod', ['644', unreadable.path]); }); final r = await call('editor.open', {'path': 'locked.md'}); expect(r.ok, isFalse); // Either errno-mapped or toolError — either is acceptable. expect(r.error!.code, isNot(IpcExitCode.notFound)); }); test('editor.active returns null when no buffer is open', () async { final r = await call('editor.active'); expect(r.ok, isTrue); expect(r.data['active'], isNull); }); test('editor.activate requires id and validates it', () async { final missing = await call('editor.activate'); expect(missing.ok, isFalse); expect(missing.error!.kind, 'user_error'); final unknown = await call('editor.activate', {'id': 'b_404'}); expect(unknown.ok, isFalse); expect(unknown.error!.kind, 'not_found'); }); test('editor.activate flips the active buffer to the requested one', () async { await File('${sandbox.path}/a.md').writeAsString('a'); await File('${sandbox.path}/b.md').writeAsString('b'); final a = await call('editor.open', {'path': 'a.md'}); await call('editor.open', {'path': 'b.md'}); final r = await call('editor.activate', {'id': a.data['id']}); expect(r.ok, isTrue); expect(r.data['active'], a.data['id']); }); test('editor.read with no active buffer and no id returns not-found', () async { final r = await call('editor.read'); expect(r.ok, isFalse); expect(r.error!.kind, 'not_found'); }); test('editor.read with an unknown id returns not-found', () async { final r = await call('editor.read', {'id': 'b_404'}); expect(r.ok, isFalse); expect(r.error!.kind, 'not_found'); }); test('editor.set-selection clamps and applies', () async { await call('editor.open', {'path': 'doc.md'}); final r = await call('editor.set-selection', { 'selection': {'start': 0, 'end': 3}, }); expect(r.ok, isTrue); }); test('editor.set-selection without an id or active buffer returns not-found', () async { final r = await call('editor.set-selection', { 'selection': {'start': 0, 'end': 1}, }); expect(r.ok, isFalse); expect(r.error!.kind, 'not_found'); }); test('editor.set-content overwrites the buffer (with and without selection)', () async { await call('editor.open', {'path': 'doc.md'}); final r1 = await call('editor.set-content', {'text': 'replaced'}); expect(r1.ok, isTrue); expect(r1.data['length'], 'replaced'.length); final read1 = await call('editor.read'); expect(read1.data['content'], 'replaced'); final r2 = await call('editor.set-content', { 'text': 'short', 'selection': {'start': 1, 'end': 99}, }); expect(r2.ok, isTrue); }); test('editor.save with no active buffer returns not-found', () async { final r = await call('editor.save'); expect(r.ok, isFalse); expect(r.error!.kind, 'not_found'); }); test('editor.close requires id and validates it', () async { final missing = await call('editor.close'); expect(missing.ok, isFalse); expect(missing.error!.kind, 'user_error'); final unknown = await call('editor.close', {'id': 'b_404'}); expect(unknown.ok, isFalse); expect(unknown.error!.kind, 'not_found'); }); test('insert / replace / set-content / save with no active buffer all return not-found', () async { for (final cmd in ['editor.insert', 'editor.replace-selection', 'editor.set-content', 'editor.save']) { final r = await call(cmd, {'text': 'x'}); expect(r.ok, isFalse, reason: cmd); expect(r.error!.kind, 'not_found', reason: cmd); } }); }