From 8eaf7446a134d354a00b60ebb7e03e85f4a10023 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 13 May 2026 07:09:33 +0200 Subject: [PATCH] test sweep: cover daemon dispatcher + git_commands (T-91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two test additions toward finishing src/daemon/: - test/daemon/dispatcher_test.dart (6 tests): the entire DaemonDispatcher surface — ping + version default handlers, unknown-command not-found error, register routing, isEmpty before/after registration, clear preserving ping + version. - test/daemon/git_commands_test.dart extended (14 new tests): the git.* commands the existing suite didn't reach — git.diff with paths, git.stage-hunk + git.unstage-hunk (happy + missing-patch + bad-patch GitException), git.branches, git.checkout (happy + missing + unknown), git.log with count, git.push + git.pull both with and without a local bare remote, git.stage accepting a String single-path arg via _pathList. Coverage: src/daemon/dispatcher.dart 8/22 -> 22/22 (100%); src/daemon/git_commands.dart 73/146 -> 122/146 (84%). The remaining 24 lines in git_commands are mid-call GitException catch branches that need the git client to fail after the dispatcher accepted the request. Total coverage 77.05% -> 77.92%. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/daemon/dispatcher_test.dart | 69 +++++++++++++++++++ test/daemon/git_commands_test.dart | 107 +++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 test/daemon/dispatcher_test.dart diff --git a/test/daemon/dispatcher_test.dart b/test/daemon/dispatcher_test.dart new file mode 100644 index 00000000..14db96d6 --- /dev/null +++ b/test/daemon/dispatcher_test.dart @@ -0,0 +1,69 @@ +/// Unit tests for the daemon's command dispatcher. +library; + +import 'package:clide/clide.dart'; +import 'package:clide/src/daemon/dispatcher.dart'; +import 'package:test/test.dart'; + +IpcRequest _req(String cmd, {String id = '1', Map args = const {}}) { + return IpcRequest(id: id, cmd: cmd, args: args); +} + +void main() { + group('DaemonDispatcher', () { + test('ping is registered by default and returns pong + version', () async { + final d = DaemonDispatcher(); + final r = await d.dispatch(_req('ping')); + expect(r.ok, isTrue); + expect(r.data['pong'], isTrue); + expect(r.data['version'], isNotNull); + expect(r.data['ts'], isA()); + }); + + test('version returns the bundled version string', () async { + final d = DaemonDispatcher(); + final r = await d.dispatch(_req('version')); + expect(r.ok, isTrue); + expect(r.data['version'], clideVersion); + }); + + test('dispatching an unknown command produces a not-found error', () async { + final d = DaemonDispatcher(); + final r = await d.dispatch(_req('nonsense')); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.notFound); + expect(r.error?.message, contains('unknown command')); + }); + + test('register routes a new handler', () async { + final d = DaemonDispatcher(); + d.register('echo', (req) async { + return IpcResponse.ok(id: req.id, data: {'echo': req.args['text']}); + }); + final r = await d.dispatch(_req('echo', args: {'text': 'hi'})); + expect(r.ok, isTrue); + expect(r.data['echo'], 'hi'); + }); + + test('isEmpty is true for a fresh dispatcher (ping + version only)', () { + final d = DaemonDispatcher(); + expect(d.isEmpty, isTrue); + d.register('something', (req) async => IpcResponse.ok(id: req.id, data: const {})); + expect(d.isEmpty, isFalse); + }); + + test('clear removes user handlers but keeps ping + version', () async { + final d = DaemonDispatcher(); + d.register('extra', (req) async => IpcResponse.ok(id: req.id, data: const {})); + expect(d.isEmpty, isFalse); + d.clear(); + expect(d.isEmpty, isTrue); + // ping still works + final r = await d.dispatch(_req('ping')); + expect(r.ok, isTrue); + // 'extra' is gone + final r2 = await d.dispatch(_req('extra')); + expect(r2.ok, isFalse); + }); + }); +} diff --git a/test/daemon/git_commands_test.dart b/test/daemon/git_commands_test.dart index 0534ee90..271cc69d 100644 --- a/test/daemon/git_commands_test.dart +++ b/test/daemon/git_commands_test.dart @@ -188,4 +188,111 @@ void main() { content = await File('${sandbox.path}/file.txt').readAsString(); expect(content, 'stash-me'); }); + + test('git.diff with explicit paths narrows the result', () async { + await File('${sandbox.path}/file.txt').writeAsString('hello\nworld\n'); + await File('${sandbox.path}/other.txt').writeAsString('o'); + final r = await call('git.diff', { + 'paths': ['file.txt'] + }); + expect(r.ok, isTrue); + final diffs = r.data['diffs'] as List; + expect(diffs, hasLength(1)); + }); + + test('git.stage-hunk requires a non-empty patch', () async { + final missing = await call('git.stage-hunk'); + expect(missing.ok, isFalse); + expect(missing.error?.kind, IpcErrorKind.userError); + final empty = await call('git.stage-hunk', {'patch': ''}); + expect(empty.ok, isFalse); + }); + + test('git.unstage-hunk requires a non-empty patch', () async { + final missing = await call('git.unstage-hunk'); + expect(missing.ok, isFalse); + expect(missing.error?.kind, IpcErrorKind.userError); + }); + + test('git.stage-hunk + git.unstage-hunk round-trip a real patch', () async { + await File('${sandbox.path}/file.txt').writeAsString('hello\nworld\n'); + final p = await Process.run('git', ['diff', '-U0'], workingDirectory: sandbox.path); + final patch = p.stdout as String; + final staged = await call('git.stage-hunk', {'patch': patch}); + expect(staged.ok, isTrue); + final unstaged = await call('git.unstage-hunk', {'patch': patch}); + expect(unstaged.ok, isTrue); + }); + + test('git.stage-hunk surfaces GitException as a tool error', () async { + final r = await call('git.stage-hunk', {'patch': 'not a valid patch\n'}); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.toolError); + }); + + test('git.branches lists the local branches', () async { + await Process.run('git', ['branch', 'feature/a'], workingDirectory: sandbox.path); + final r = await call('git.branches'); + expect(r.ok, isTrue); + final branches = r.data['branches'] as List; + expect(branches.map((b) => (b as Map)['name']), containsAll(['feature/a'])); + }); + + test('git.checkout requires a branch name', () async { + final missing = await call('git.checkout'); + expect(missing.ok, isFalse); + expect(missing.error?.kind, IpcErrorKind.userError); + final empty = await call('git.checkout', {'branch': ''}); + expect(empty.ok, isFalse); + }); + + test('git.checkout switches branches', () async { + await Process.run('git', ['branch', 'next'], workingDirectory: sandbox.path); + final r = await call('git.checkout', {'branch': 'next'}); + expect(r.ok, isTrue); + expect(r.data['branch'], 'next'); + }); + + test('git.checkout to an unknown branch surfaces a tool error', () async { + final r = await call('git.checkout', {'branch': 'no-such-branch'}); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.toolError); + }); + + test('git.log accepts an explicit count', () async { + final r = await call('git.log', {'count': 5}); + expect(r.ok, isTrue); + final entries = r.data['entries'] as List; + expect(entries, isNotEmpty); + }); + + test('git.push to no remote surfaces a tool error', () async { + final r = await call('git.push'); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.toolError); + }); + + test('git.pull with no remote surfaces a tool error', () async { + final r = await call('git.pull'); + expect(r.ok, isFalse); + expect(r.error?.kind, IpcErrorKind.toolError); + }); + + test('git.push + git.pull against a local bare remote return output', () async { + final remote = await Directory.systemTemp.createTemp('clide-git-cmd-remote-'); + addTearDown(() => remote.deleteSync(recursive: true)); + await Process.run('git', ['init', '--bare'], workingDirectory: remote.path); + await Process.run('git', ['remote', 'add', 'origin', remote.path], workingDirectory: sandbox.path); + final pushed = await call('git.push', {'remote': 'origin', 'branch': 'HEAD', 'setUpstream': true}); + expect(pushed.ok, isTrue); + final pulled = await call('git.pull'); + expect(pulled.ok, isTrue); + }); + + test('git.stage accepts a string single-path arg', () async { + await File('${sandbox.path}/new.txt').writeAsString('x'); + // _pathList accepts a String, wrapping it as a singleton. + final r = await call('git.stage', {'paths': 'new.txt'}); + expect(r.ok, isTrue); + }); }