test sweep: cover daemon dispatcher + git_commands (T-91)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String, Object?> 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<String>());
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user