From 465581f9073262d749d7f609d6c0bba980785ce0 Mon Sep 17 00:00:00 2001 From: Dominik Simonik Date: Wed, 21 Oct 2026 14:01:43 +0200 Subject: [PATCH 1/3] fix: quieten the update check and say how to recover a newer manifest The update check asked pub.dev about a package that is not published, so every command ended with "Failed to check for updates." Very Good CLI swallows the same failure; it is a best-effort check that should not be an error. ReportingManifestStore now tells the user to run `vgv_ai update` when the manifest was written by a newer version, which only doctor did. --only lists what each harness is, --force is coloured like every other flag, and the resolver's git output reaches --verbose. Also extracts commandHome, which three commands had inlined, and a shared TestRunner so the command tests stop declaring the same three mocks. --- .../vgv_ai_cli/lib/src/command_runner.dart | 4 +- .../src/commands/doctor/doctor_command.dart | 13 ++-- .../lib/src/commands/harness_selection.dart | 4 + .../lib/src/commands/install_command.dart | 16 ++-- .../commands/reporting_manifest_store.dart | 19 ++++- .../lib/src/commands/uninstall_command.dart | 8 +- .../lib/src/commands/user_home.dart | 7 ++ .../lib/src/resolve/plugin_resolver.dart | 12 ++- .../test/helpers/command_helper.dart | 77 +++++++++++++++++++ .../test/src/command_runner_test.dart | 36 +++------ .../commands/doctor/doctor_command_test.dart | 54 +++++++------ .../src/commands/install_command_test.dart | 32 +++----- .../src/commands/uninstall_command_test.dart | 70 ++++++++--------- .../src/commands/update_command_test.dart | 33 ++------ 14 files changed, 227 insertions(+), 158 deletions(-) create mode 100644 packages/vgv_ai_cli/test/helpers/command_helper.dart diff --git a/packages/vgv_ai_cli/lib/src/command_runner.dart b/packages/vgv_ai_cli/lib/src/command_runner.dart index 5bc616b2..69c80118 100644 --- a/packages/vgv_ai_cli/lib/src/command_runner.dart +++ b/packages/vgv_ai_cli/lib/src/command_runner.dart @@ -152,7 +152,9 @@ ${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u219 Run ${lightCyan.wrap('$executableName update')} to update'''); } } on Exception catch (_) { - _logger.err('Failed to check for updates.'); + // Best effort: the check runs after every command, so a user who is + // offline should not be told about it. `vgv_ai_cli` is not published on + // pub.dev either, which is why every run takes this path today. } } } diff --git a/packages/vgv_ai_cli/lib/src/commands/doctor/doctor_command.dart b/packages/vgv_ai_cli/lib/src/commands/doctor/doctor_command.dart index be60dcf3..3f07dfcc 100644 --- a/packages/vgv_ai_cli/lib/src/commands/doctor/doctor_command.dart +++ b/packages/vgv_ai_cli/lib/src/commands/doctor/doctor_command.dart @@ -1,5 +1,3 @@ -import 'dart:io' show Platform; - import 'package:args/command_runner.dart'; import 'package:collection/collection.dart'; import 'package:file/file.dart'; @@ -44,10 +42,13 @@ class DoctorCommand({ final Translator _translator = const Translator(), final Reconciler _reconciler = const Reconciler(), }) extends Command with HarnessSelection { + /// {@macro doctor_command} this { addHarnessOptions('Check the Very Good AI tooling for'); } + /// The name the CLI registers `doctor` under, so other code can name it + /// without constructing one. static const String commandName = 'doctor'; /// The harnesses that load the shared skills, so whose `install` fixes @@ -66,7 +67,7 @@ class DoctorCommand({ @override Future run() async { - final home = _home ?? userHome(_environment ?? Platform.environment); + final home = commandHome(_home, _environment); if (home == null) { _logger.err( 'Could not find your home directory. Set HOME and run doctor again.', @@ -105,8 +106,8 @@ class DoctorCommand({ ..info('Run ${lightCyan.wrap('$fix')} to fix everything install can.'); if (fix.force) { _logger.info( - '--force applies to every harness listed, replacing your values ' - "wherever they conflict with VGV's.", + '${lightCyan.wrap('--force')} applies to every harness listed, ' + "replacing your values wherever they conflict with VGV's.", ); } } @@ -180,7 +181,7 @@ class DoctorCommand({ manifest, selected, ).entries) { - switch (await _resolver.resolve(home, only: only)) { + switch (await _resolver.resolve(home, only: only, log: _logger.detail)) { case ResolvedAssets(:final assets): final translation = await _translator.translate( assets: assets, diff --git a/packages/vgv_ai_cli/lib/src/commands/harness_selection.dart b/packages/vgv_ai_cli/lib/src/commands/harness_selection.dart index 4f7f410f..317f5626 100644 --- a/packages/vgv_ai_cli/lib/src/commands/harness_selection.dart +++ b/packages/vgv_ai_cli/lib/src/commands/harness_selection.dart @@ -20,6 +20,10 @@ mixin HarnessSelection on Command { 'only', help: '$action $only', allowed: Harness.values.map((harness) => harness.name), + allowedHelp: { + for (final harness in Harness.values) + harness.name: '${harness.displayName}.', + }, ); // Opting out matters more than opting in: the default is every harness diff --git a/packages/vgv_ai_cli/lib/src/commands/install_command.dart b/packages/vgv_ai_cli/lib/src/commands/install_command.dart index 2b6dc53a..4e98c4dc 100644 --- a/packages/vgv_ai_cli/lib/src/commands/install_command.dart +++ b/packages/vgv_ai_cli/lib/src/commands/install_command.dart @@ -1,5 +1,3 @@ -import 'dart:io' show Platform; - import 'package:args/command_runner.dart'; import 'package:collection/collection.dart'; import 'package:file/file.dart'; @@ -35,6 +33,7 @@ class InstallCommand({ final Translator _translator = const Translator(), final Reconciler _reconciler = const Reconciler(), }) extends Command with HarnessSelection { + /// {@macro install_command} this { argParser ..addFlag( @@ -57,6 +56,8 @@ class InstallCommand({ addHarnessOptions('Configure the Very Good AI tooling for'); } + /// The name the CLI registers `install` under, so other code can name it + /// without constructing one. static const String commandName = 'install'; /// The outcomes that leave a file with VGV's contributions in it. @@ -80,9 +81,7 @@ class InstallCommand({ String get name => commandName; /// The directory install configures, or null when it cannot be found. - Directory? get home { - return _home ?? userHome(_environment ?? Platform.environment); - } + Directory? get home => commandHome(_home, _environment); @override Future run() async { @@ -116,6 +115,7 @@ class InstallCommand({ _logger, nothingDone: 'Installed nothing', done: 'written', + retry: 'install', ); final manifest = store.load(); if (manifest == null) return ExitCode.ioError.code; @@ -232,7 +232,11 @@ class InstallCommand({ Future _resolveAssets(Directory home) async { final progress = _logger.progress('Resolving plugins'); - switch (await _resolver.resolve(home, only: _selectedPlugins)) { + switch (await _resolver.resolve( + home, + only: _selectedPlugins, + log: _logger.detail, + )) { case final ResolvedAssets resolved: progress.complete( 'Resolved ${resolved.assets.skills.length} skills and ' diff --git a/packages/vgv_ai_cli/lib/src/commands/reporting_manifest_store.dart b/packages/vgv_ai_cli/lib/src/commands/reporting_manifest_store.dart index ebd92a96..c1f747d0 100644 --- a/packages/vgv_ai_cli/lib/src/commands/reporting_manifest_store.dart +++ b/packages/vgv_ai_cli/lib/src/commands/reporting_manifest_store.dart @@ -18,17 +18,28 @@ class const ReportingManifestStore( /// What the command did to the files it records, such as `written`. required final String _done, + + /// The command to run again once the manifest can be read, such as + /// `install`. + required final String _retry, }) { + /// {@macro reporting_manifest_store} + this; + /// The manifest in the store, or null, having said why, when it cannot be /// read or was written by a newer version. InstallManifest? load() { try { final manifest = _store.load(); if (manifest == null) { - _logger.err( - '$_nothingDone, because ${ManifestStore.unreadable}: ' - '${_store.file.path}', - ); + _logger + ..err( + '$_nothingDone, because ${ManifestStore.unreadable}: ' + '${_store.file.path}', + ) + ..err( + 'Run ${lightCyan.wrap('vgv_ai update')}, then run $_retry again.', + ); } return manifest; } on FileSystemException catch (error) { diff --git a/packages/vgv_ai_cli/lib/src/commands/uninstall_command.dart b/packages/vgv_ai_cli/lib/src/commands/uninstall_command.dart index c619d428..4f81b695 100644 --- a/packages/vgv_ai_cli/lib/src/commands/uninstall_command.dart +++ b/packages/vgv_ai_cli/lib/src/commands/uninstall_command.dart @@ -1,5 +1,3 @@ -import 'dart:io' show Platform; - import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:mason_logger/mason_logger.dart'; @@ -32,6 +30,7 @@ class UninstallCommand({ final Map? _environment, final ManifestAuditor _auditor = const ManifestAuditor(), }) extends Command with HarnessSelection { + /// {@macro uninstall_command} this { argParser ..addFlag( @@ -50,6 +49,8 @@ class UninstallCommand({ ); } + /// The name the CLI registers `uninstall` under, so other code can name it + /// without constructing one. static const String commandName = 'uninstall'; /// The outcomes where something the user changed was kept. @@ -78,7 +79,7 @@ class UninstallCommand({ final only = onlyHarnesses; final selected = only ?? Harness.values.where(isEnabled).toSet(); - final home = _home ?? userHome(_environment ?? Platform.environment); + final home = commandHome(_home, _environment); if (home == null) { _logger.err( 'Could not find your home directory. Set HOME and run uninstall ' @@ -92,6 +93,7 @@ class UninstallCommand({ _logger, nothingDone: 'Removed nothing', done: 'removed', + retry: 'uninstall', ); final manifest = store.load(); if (manifest == null) return ExitCode.ioError.code; diff --git a/packages/vgv_ai_cli/lib/src/commands/user_home.dart b/packages/vgv_ai_cli/lib/src/commands/user_home.dart index 3de12ac3..fcbf097f 100644 --- a/packages/vgv_ai_cli/lib/src/commands/user_home.dart +++ b/packages/vgv_ai_cli/lib/src/commands/user_home.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:file/file.dart'; import 'package:file/local.dart'; @@ -6,3 +8,8 @@ Directory? userHome(Map environment) { final home = environment['HOME'] ?? environment['USERPROFILE']; return home == null ? null : const LocalFileSystem().directory(home); } + +/// The directory a command works on: [override] when one was injected, and +/// otherwise the home directory [environment], or the real one, names. +Directory? commandHome(Directory? override, Map? environment) => + override ?? userHome(environment ?? Platform.environment); diff --git a/packages/vgv_ai_cli/lib/src/resolve/plugin_resolver.dart b/packages/vgv_ai_cli/lib/src/resolve/plugin_resolver.dart index 33babc25..9e1e2f85 100644 --- a/packages/vgv_ai_cli/lib/src/resolve/plugin_resolver.dart +++ b/packages/vgv_ai_cli/lib/src/resolve/plugin_resolver.dart @@ -18,15 +18,25 @@ class const PluginResolver({ final PluginParser _parser = const ClaudePluginParser(), final List _plugins = vgvPlugins, }) { + /// {@macro plugin_resolver} + this; + /// Resolves the plugins [only] names for [home], or every pinned one when /// it names none, stopping at the first that cannot be resolved or parsed: /// a half-configured harness is worse than an unconfigured one. - Future resolve(Directory home, {Set? only}) async { + /// + /// [log] receives the git command line and its stderr, for `--verbose`. + Future resolve( + Directory home, { + Set? only, + LogDetail? log, + }) async { final sources = [ LocalCachePluginSource(home: home), GitPluginSource( cacheRoot: home.childDirectory('.vgv_ai').childDirectory('plugins'), runGit: (args) => _runProcess('git', args), + log: log, ), ]; diff --git a/packages/vgv_ai_cli/test/helpers/command_helper.dart b/packages/vgv_ai_cli/test/helpers/command_helper.dart new file mode 100644 index 00000000..3599a405 --- /dev/null +++ b/packages/vgv_ai_cli/test/helpers/command_helper.dart @@ -0,0 +1,77 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pub_updater/pub_updater.dart'; +import 'package:vgv_ai_cli/src/command_runner.dart'; +import 'package:vgv_ai_cli/src/commands/commands.dart'; +import 'package:vgv_ai_cli/src/version.dart'; + +class _MockLogger extends Mock implements Logger; + +class _MockProgress extends Mock implements Progress; + +class _MockPubUpdater extends Mock implements PubUpdater; + +/// {@template test_runner} +/// The mocked collaborators a command test runs against, stubbed the way +/// every one of them needs: the CLI is already up to date, and the progress +/// [logger] hands out records its messages in [progressLogs]. +/// +/// Tests that need another answer re-stub [logger], [progress] or +/// [pubUpdater] themselves. +/// {@endtemplate} +class TestRunner() { + /// {@macro test_runner} + this { + when(() => logger.progress(any())).thenReturn(progress); + when(() => progress.complete(any())).thenAnswer((invocation) { + final message = invocation.positionalArguments.first as String?; + if (message != null) progressLogs.add(message); + }); + when(() => pubUpdater.getLatestVersion(any())) + .thenAnswer((_) async => packageVersion); + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenAnswer((_) async => true); + } + + /// The logger the runner and its commands write to. + final Logger logger = _MockLogger(); + + /// The progress [logger] returns. + final Progress progress = _MockProgress(); + + /// The updater the runner checks the latest version with. + final PubUpdater pubUpdater = _MockPubUpdater(); + + /// What [progress] was completed with, in order. + final List progressLogs = []; + + /// A runner carrying [logger] and [pubUpdater], with the given commands + /// replacing the ones that would otherwise reach the real home directory. + VgvAiCliCommandRunner build({ + InstallCommand? installCommand, + DoctorCommand? doctorCommand, + UninstallCommand? uninstallCommand, + }) => VgvAiCliCommandRunner( + logger: logger, + pubUpdater: pubUpdater, + installCommand: installCommand, + doctorCommand: doctorCommand, + uninstallCommand: uninstallCommand, + ); + + /// Runs [args] through a runner from [build]. + Future run( + List args, { + InstallCommand? installCommand, + DoctorCommand? doctorCommand, + UninstallCommand? uninstallCommand, + }) => build( + installCommand: installCommand, + doctorCommand: doctorCommand, + uninstallCommand: uninstallCommand, + ).run(args); +} diff --git a/packages/vgv_ai_cli/test/src/command_runner_test.dart b/packages/vgv_ai_cli/test/src/command_runner_test.dart index 3c66b9a6..e5ce63d8 100644 --- a/packages/vgv_ai_cli/test/src/command_runner_test.dart +++ b/packages/vgv_ai_cli/test/src/command_runner_test.dart @@ -9,11 +9,7 @@ import 'package:test/test.dart'; import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/version.dart'; -class _MockLogger extends Mock implements Logger; - -class _MockProgress extends Mock implements Progress; - -class _MockPubUpdater extends Mock implements PubUpdater; +import '../helpers/command_helper.dart'; class _FlagCommand() extends Command { this { @@ -46,17 +42,10 @@ Run ${lightCyan.wrap('$executableName update')} to update'''; late VgvAiCliCommandRunner commandRunner; setUp(() { - pubUpdater = _MockPubUpdater(); - - when(() => pubUpdater.getLatestVersion(any())) - .thenAnswer((_) async => packageVersion); - - logger = _MockLogger(); - - commandRunner = VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - ); + final harness = TestRunner(); + pubUpdater = harness.pubUpdater; + logger = harness.logger; + commandRunner = harness.build(); }); test('shows update message when newer version exists', () async { @@ -68,12 +57,13 @@ Run ${lightCyan.wrap('$executableName update')} to update'''; verify(() => logger.info(updatePrompt)).called(1); }); - test('shows error message when failed to check for updates', () async { + test('stays quiet when the update check fails', () async { when(() => pubUpdater.getLatestVersion(any())) .thenThrow(Exception('Failed to check for updates')); - await commandRunner.run(['--version']); - verify(() => logger.err('Failed to check for updates.')).called(1); + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verifyNever(() => logger.err(any())); }); test('Does not show update message when the shell calls the ' @@ -104,14 +94,6 @@ Run ${lightCyan.wrap('$executableName update')} to update'''; ), ).thenAnswer((_) async => true); - final progress = _MockProgress(); - final progressLogs = []; - when(() => progress.complete(any())).thenAnswer((answer) { - final message = answer.positionalArguments.elementAt(0) as String?; - if (message != null) progressLogs.add(message); - }); - when(() => logger.progress(any())).thenReturn(progress); - final result = await commandRunner.run(['update']); expect(result, ExitCode.success.code); verifyNever(() => logger.info(updatePrompt)); diff --git a/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart b/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart index d7995d3f..a0350c9a 100644 --- a/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart @@ -5,9 +5,7 @@ import 'package:file/memory.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:plugin_source/plugin_source.dart'; -import 'package:pub_updater/pub_updater.dart'; import 'package:test/test.dart'; -import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/commands/commands.dart'; import 'package:vgv_ai_cli/src/detect/detect.dart'; import 'package:vgv_ai_cli/src/manifest/manifest.dart'; @@ -15,16 +13,11 @@ import 'package:vgv_ai_cli/src/model/model.dart'; import 'package:vgv_ai_cli/src/reconcile/reconcile.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; import 'package:vgv_ai_cli/src/translate/translate.dart'; -import 'package:vgv_ai_cli/src/version.dart'; -class _MockLogger extends Mock implements Logger; - -class _MockPubUpdater extends Mock implements PubUpdater; +import '../../../helpers/command_helper.dart'; class _MockDetector extends Mock implements HarnessDetector; -class _MockProgress extends Mock implements Progress; - /// A [PluginResolver] that answers with [resolution], and counts its calls. class _FakeResolver extends Fake implements PluginResolver { AssetResolution resolution = const ResolvedAssets( @@ -41,10 +34,18 @@ class _FakeResolver extends Fake implements PluginResolver { /// The plugins each call asked for, in order. final resolved = ?>[]; + /// The sink the command passed for `--verbose` output. + LogDetail? log; + @override - Future resolve(Directory home, {Set? only}) async { + Future resolve( + Directory home, { + Set? only, + LogDetail? log, + }) async { calls++; resolved.add(only); + this.log = log; return resolution; } } @@ -80,8 +81,8 @@ void main() { group(DoctorCommand, () { setUpAll(() => registerFallbackValue(Harness.claude)); + late TestRunner harness; late Logger logger; - late PubUpdater pubUpdater; late FileSystem fs; late Directory home; late ManifestStore store; @@ -104,28 +105,20 @@ void main() { } setUp(() { - logger = _MockLogger(); - pubUpdater = _MockPubUpdater(); + harness = TestRunner(); + logger = harness.logger; + progress = harness.progress; fs = MemoryFileSystem.test(); home = fs.directory('/home/dev')..createSync(recursive: true); store = ManifestStore(home); detector = _MockDetector(); detect(); - progress = _MockProgress(); - when(() => logger.progress(any())).thenReturn(progress); resolver = _FakeResolver(); translator = _FakeTranslator(); - - when(() => pubUpdater.getLatestVersion(any())) - .thenAnswer((_) async => packageVersion); }); Future runThroughRunner(DoctorCommand command, List args) => - VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - doctorCommand: command, - ).run(['doctor', ...args]); + harness.run(['doctor', ...args], doctorCommand: command); Future runDoctor({ List args = const [], @@ -560,6 +553,17 @@ void main() { }, ); + test('hands the resolver a sink for its verbose output', () async { + install([owned('.codex/agents/a.toml', harness: Harness.codex)]); + + await runDoctor(); + + expect(resolver.calls, 1); + expect(resolver.log, isNotNull); + resolver.log!('cloning something'); + verify(() => logger.detail('cloning something')).called(1); + }); + test('is not checked when nothing in scope is configured', () async { install([owned('.codex/agents/a.toml', harness: Harness.codex)]); detect(const [Harness.pi]); @@ -859,9 +863,9 @@ void main() { String combined(String command) => 'Run $command to fix everything install can.'; - const forceNote = - '--force applies to every harness listed, replacing your values ' - "wherever they conflict with VGV's."; + final forceNote = + '${lightCyan.wrap('--force')} applies to every harness listed, ' + "replacing your values wherever they conflict with VGV's."; void verifyNoCombined() => verifyNever( () => diff --git a/packages/vgv_ai_cli/test/src/commands/install_command_test.dart b/packages/vgv_ai_cli/test/src/commands/install_command_test.dart index 81c39dea..89538b77 100644 --- a/packages/vgv_ai_cli/test/src/commands/install_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/install_command_test.dart @@ -6,25 +6,18 @@ import 'package:file/memory.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:plugin_source/plugin_source.dart'; -import 'package:pub_updater/pub_updater.dart'; import 'package:test/test.dart'; -import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/commands/commands.dart'; import 'package:vgv_ai_cli/src/detect/detect.dart'; import 'package:vgv_ai_cli/src/manifest/manifest.dart'; import 'package:vgv_ai_cli/src/model/model.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; import 'package:vgv_ai_cli/src/translate/translate.dart'; -import 'package:vgv_ai_cli/src/version.dart'; -class _MockLogger extends Mock implements Logger; - -class _MockProgress extends Mock implements Progress; +import '../../helpers/command_helper.dart'; class _MockDetector extends Mock implements HarnessDetector; -class _MockPubUpdater extends Mock implements PubUpdater; - class const _FakeParser(final PluginAssets assets) implements PluginParser { @override PluginAssets parse(Directory root) => assets; @@ -86,26 +79,22 @@ void main() { setUpAll(() => registerFallbackValue(Harness.claude)); + late TestRunner harness; late Logger logger; late Progress progress; late HarnessDetector detector; late FileSystem fs; late Directory home; late _FakeTranslator translator; - late PubUpdater pubUpdater; setUp(() { - logger = _MockLogger(); - progress = _MockProgress(); + harness = TestRunner(); + logger = harness.logger; + progress = harness.progress; detector = _MockDetector(); fs = MemoryFileSystem.test(); home = fs.directory('/home/dev')..createSync(recursive: true); translator = _FakeTranslator(); - pubUpdater = _MockPubUpdater(); - - when(() => logger.progress(any())).thenReturn(progress); - when(() => pubUpdater.getLatestVersion(any())) - .thenAnswer((_) async => packageVersion); }); /// Clones every plugin by creating its target, since there is no @@ -120,11 +109,7 @@ void main() { } Future runThroughRunner(InstallCommand command, List args) => - VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - installCommand: command, - ).run(['install', ...args]); + harness.run(['install', ...args], installCommand: command); Future runInstall( List args, { @@ -652,6 +637,11 @@ void main() { '${manifestFile().path}', ), ).called(1); + verify( + () => logger.err( + 'Run ${lightCyan.wrap('vgv_ai update')}, then run install again.', + ), + ).called(1); }); test('installs nothing when it cannot be read', () async { diff --git a/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart b/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart index 3587e35a..bb4e0373 100644 --- a/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart @@ -6,21 +6,14 @@ import 'package:file/memory.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:plugin_source/plugin_source.dart'; -import 'package:pub_updater/pub_updater.dart'; import 'package:test/test.dart'; -import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/commands/commands.dart'; import 'package:vgv_ai_cli/src/manifest/manifest.dart'; import 'package:vgv_ai_cli/src/model/model.dart'; import 'package:vgv_ai_cli/src/reconcile/reconcile.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; -import 'package:vgv_ai_cli/src/version.dart'; -class _MockLogger extends Mock implements Logger; - -class _MockPubUpdater extends Mock implements PubUpdater; - -class _MockProgress extends Mock implements Progress; +import '../../helpers/command_helper.dart'; class const _FakeParser(final PluginAssets assets) implements PluginParser { @override @@ -47,8 +40,8 @@ class const _FailingAuditor() extends ManifestAuditor { void main() { group(UninstallCommand, () { + late TestRunner harness; late Logger logger; - late PubUpdater pubUpdater; late FileSystem fs; late Directory home; late ManifestStore store; @@ -57,8 +50,8 @@ void main() { String? undeletable; setUp(() { - logger = _MockLogger(); - pubUpdater = _MockPubUpdater(); + harness = TestRunner(); + logger = harness.logger; undeletable = null; fs = MemoryFileSystem.test( opHandle: (path, operation) { @@ -69,17 +62,10 @@ void main() { ); home = fs.directory('/home/dev')..createSync(recursive: true); store = ManifestStore(home); - - when(() => pubUpdater.getLatestVersion(any())) - .thenAnswer((_) async => packageVersion); }); Future runThroughRunner(UninstallCommand command, List args) => - VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - uninstallCommand: command, - ).run(['uninstall', ...args]); + harness.run(['uninstall', ...args], uninstallCommand: command); Future runUninstall([ List args = const [], @@ -297,6 +283,15 @@ void main() { 'Remove the Very Good AI tooling from just these harnesses.', ); }); + + test('--only lists what each harness name means', () { + final command = UninstallCommand(logger: logger); + + expect(command.argParser.options['only']!.allowedHelp, { + for (final harness in Harness.values) + harness.name: '${harness.displayName}.', + }); + }); }); group('shared skills and empty directories', () { @@ -581,6 +576,12 @@ void main() { '${store.file.path}', ), ).called(1); + verify( + () => logger.err( + 'Run ${lightCyan.wrap('vgv_ai update')}, then run uninstall ' + 'again.', + ), + ).called(1); }); test('removes nothing when it cannot be read', () async { @@ -639,7 +640,6 @@ void main() { /// Installs one skill and one agent for every harness with the real /// translator and reconciler. Future installEverything() async { - when(() => logger.progress(any())).thenReturn(_MockProgress()); final skill = fs.directory('/plugin/skills/green-gate'); fs.file(fs.path.join(skill.path, 'references', 'guide.md')) ..createSync(recursive: true) @@ -676,22 +676,20 @@ void main() { return ProcessResult(0, 0, '', ''); } - final exitCode = - await VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - installCommand: InstallCommand( - logger: logger, - home: home, - resolver: PluginResolver( - runProcess: clone, - parser: _FakeParser(assets), - ), - ), - ).run([ - 'install', - for (final harness in Harness.values) ...['--only', harness.name], - ]); + final exitCode = await harness.run( + [ + 'install', + for (final harness in Harness.values) ...['--only', harness.name], + ], + installCommand: InstallCommand( + logger: logger, + home: home, + resolver: PluginResolver( + runProcess: clone, + parser: _FakeParser(assets), + ), + ), + ); expect(exitCode, ExitCode.success.code); } diff --git a/packages/vgv_ai_cli/test/src/commands/update_command_test.dart b/packages/vgv_ai_cli/test/src/commands/update_command_test.dart index b8d2032e..320c8d79 100644 --- a/packages/vgv_ai_cli/test/src/commands/update_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/update_command_test.dart @@ -8,11 +8,7 @@ import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/commands/commands.dart'; import 'package:vgv_ai_cli/src/version.dart'; -class _MockLogger extends Mock implements Logger; - -class _MockProgress extends Mock implements Progress; - -class _MockPubUpdater extends Mock implements PubUpdater; +import '../../helpers/command_helper.dart'; void main() { group(UpdateCommand, () { @@ -23,17 +19,11 @@ void main() { late VgvAiCliCommandRunner commandRunner; setUp(() { - final progress = _MockProgress(); - final progressLogs = []; - pubUpdater = _MockPubUpdater(); - logger = _MockLogger(); - commandRunner = VgvAiCliCommandRunner( - logger: logger, - pubUpdater: pubUpdater, - ); + final harness = TestRunner(); + pubUpdater = harness.pubUpdater; + logger = harness.logger; + commandRunner = harness.build(); - when(() => pubUpdater.getLatestVersion(any())) - .thenAnswer((_) async => packageVersion); when( () => pubUpdater.update( packageName: packageName, @@ -42,17 +32,6 @@ void main() { ).thenAnswer( (_) async => ProcessResult(0, ExitCode.success.code, null, null), ); - when( - () => pubUpdater.isUpToDate( - packageName: any(named: 'packageName'), - currentVersion: any(named: 'currentVersion'), - ), - ).thenAnswer((_) async => true); - when(() => progress.complete(any())).thenAnswer((answer) { - final message = answer.positionalArguments.elementAt(0) as String?; - if (message != null) progressLogs.add(message); - }); - when(() => logger.progress(any())).thenReturn(progress); }); test('can be instantiated without a pub updater', () { @@ -133,7 +112,6 @@ void main() { ).thenAnswer( (_) async => ProcessResult(0, ExitCode.success.code, null, null), ); - when(() => logger.progress(any())).thenReturn(_MockProgress()); final result = await commandRunner.run(['update']); expect(result, ExitCode.success.code); verify(() => logger.progress('Checking for updates')).called(1); @@ -149,7 +127,6 @@ void main() { test('does not update when already on latest version', () async { when(() => pubUpdater.getLatestVersion(any())) .thenAnswer((_) async => packageVersion); - when(() => logger.progress(any())).thenReturn(_MockProgress()); final result = await commandRunner.run(['update']); expect(result, ExitCode.success.code); verify(() => logger.info('CLI is already at the latest version.')) From 226f4a946c0872b5141607782889830107fccb34 Mon Sep 17 00:00:00 2001 From: Dominik Simonik Date: Mon, 26 Oct 2026 14:06:25 +0100 Subject: [PATCH 2/3] docs: drop the unpublished claim from the update check comment --- packages/vgv_ai_cli/lib/src/command_runner.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vgv_ai_cli/lib/src/command_runner.dart b/packages/vgv_ai_cli/lib/src/command_runner.dart index 69c80118..3c7fb8fc 100644 --- a/packages/vgv_ai_cli/lib/src/command_runner.dart +++ b/packages/vgv_ai_cli/lib/src/command_runner.dart @@ -153,8 +153,8 @@ Run ${lightCyan.wrap('$executableName update')} to update'''); } } on Exception catch (_) { // Best effort: the check runs after every command, so a user who is - // offline should not be told about it. `vgv_ai_cli` is not published on - // pub.dev either, which is why every run takes this path today. + // offline, or behind a proxy that blocks pub.dev, should not be told + // about it. } } } From da1b83784e48e6c614f732026b2fc19b203980ee Mon Sep 17 00:00:00 2001 From: "unicoderbot[bot]" <269805761+unicoderbot[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:03:22 +0000 Subject: [PATCH 3/3] fix: address review feedback on PR #99 Co-authored-by: marcossevilla --- .../{command_helper.dart => test_runner.dart} | 0 .../vgv_ai_cli/test/src/command_runner_test.dart | 10 +++++----- .../src/commands/doctor/doctor_command_test.dart | 12 ++++++------ .../test/src/commands/install_command_test.dart | 12 ++++++------ .../test/src/commands/uninstall_command_test.dart | 12 ++++++------ .../test/src/commands/update_command_test.dart | 10 +++++----- 6 files changed, 28 insertions(+), 28 deletions(-) rename packages/vgv_ai_cli/test/helpers/{command_helper.dart => test_runner.dart} (100%) diff --git a/packages/vgv_ai_cli/test/helpers/command_helper.dart b/packages/vgv_ai_cli/test/helpers/test_runner.dart similarity index 100% rename from packages/vgv_ai_cli/test/helpers/command_helper.dart rename to packages/vgv_ai_cli/test/helpers/test_runner.dart diff --git a/packages/vgv_ai_cli/test/src/command_runner_test.dart b/packages/vgv_ai_cli/test/src/command_runner_test.dart index e5ce63d8..a225b79d 100644 --- a/packages/vgv_ai_cli/test/src/command_runner_test.dart +++ b/packages/vgv_ai_cli/test/src/command_runner_test.dart @@ -9,7 +9,7 @@ import 'package:test/test.dart'; import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/version.dart'; -import '../helpers/command_helper.dart'; +import '../helpers/test_runner.dart'; class _FlagCommand() extends Command { this { @@ -42,10 +42,10 @@ Run ${lightCyan.wrap('$executableName update')} to update'''; late VgvAiCliCommandRunner commandRunner; setUp(() { - final harness = TestRunner(); - pubUpdater = harness.pubUpdater; - logger = harness.logger; - commandRunner = harness.build(); + final runner = TestRunner(); + pubUpdater = runner.pubUpdater; + logger = runner.logger; + commandRunner = runner.build(); }); test('shows update message when newer version exists', () async { diff --git a/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart b/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart index a0350c9a..09ae0d3c 100644 --- a/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/doctor/doctor_command_test.dart @@ -14,7 +14,7 @@ import 'package:vgv_ai_cli/src/reconcile/reconcile.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; import 'package:vgv_ai_cli/src/translate/translate.dart'; -import '../../../helpers/command_helper.dart'; +import '../../../helpers/test_runner.dart'; class _MockDetector extends Mock implements HarnessDetector; @@ -81,7 +81,7 @@ void main() { group(DoctorCommand, () { setUpAll(() => registerFallbackValue(Harness.claude)); - late TestRunner harness; + late TestRunner runner; late Logger logger; late FileSystem fs; late Directory home; @@ -105,9 +105,9 @@ void main() { } setUp(() { - harness = TestRunner(); - logger = harness.logger; - progress = harness.progress; + runner = TestRunner(); + logger = runner.logger; + progress = runner.progress; fs = MemoryFileSystem.test(); home = fs.directory('/home/dev')..createSync(recursive: true); store = ManifestStore(home); @@ -118,7 +118,7 @@ void main() { }); Future runThroughRunner(DoctorCommand command, List args) => - harness.run(['doctor', ...args], doctorCommand: command); + runner.run(['doctor', ...args], doctorCommand: command); Future runDoctor({ List args = const [], diff --git a/packages/vgv_ai_cli/test/src/commands/install_command_test.dart b/packages/vgv_ai_cli/test/src/commands/install_command_test.dart index 89538b77..caacf9f0 100644 --- a/packages/vgv_ai_cli/test/src/commands/install_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/install_command_test.dart @@ -14,7 +14,7 @@ import 'package:vgv_ai_cli/src/model/model.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; import 'package:vgv_ai_cli/src/translate/translate.dart'; -import '../../helpers/command_helper.dart'; +import '../../helpers/test_runner.dart'; class _MockDetector extends Mock implements HarnessDetector; @@ -79,7 +79,7 @@ void main() { setUpAll(() => registerFallbackValue(Harness.claude)); - late TestRunner harness; + late TestRunner runner; late Logger logger; late Progress progress; late HarnessDetector detector; @@ -88,9 +88,9 @@ void main() { late _FakeTranslator translator; setUp(() { - harness = TestRunner(); - logger = harness.logger; - progress = harness.progress; + runner = TestRunner(); + logger = runner.logger; + progress = runner.progress; detector = _MockDetector(); fs = MemoryFileSystem.test(); home = fs.directory('/home/dev')..createSync(recursive: true); @@ -109,7 +109,7 @@ void main() { } Future runThroughRunner(InstallCommand command, List args) => - harness.run(['install', ...args], installCommand: command); + runner.run(['install', ...args], installCommand: command); Future runInstall( List args, { diff --git a/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart b/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart index bb4e0373..d319ac96 100644 --- a/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/uninstall_command_test.dart @@ -13,7 +13,7 @@ import 'package:vgv_ai_cli/src/model/model.dart'; import 'package:vgv_ai_cli/src/reconcile/reconcile.dart'; import 'package:vgv_ai_cli/src/resolve/resolve.dart'; -import '../../helpers/command_helper.dart'; +import '../../helpers/test_runner.dart'; class const _FakeParser(final PluginAssets assets) implements PluginParser { @override @@ -40,7 +40,7 @@ class const _FailingAuditor() extends ManifestAuditor { void main() { group(UninstallCommand, () { - late TestRunner harness; + late TestRunner runner; late Logger logger; late FileSystem fs; late Directory home; @@ -50,8 +50,8 @@ void main() { String? undeletable; setUp(() { - harness = TestRunner(); - logger = harness.logger; + runner = TestRunner(); + logger = runner.logger; undeletable = null; fs = MemoryFileSystem.test( opHandle: (path, operation) { @@ -65,7 +65,7 @@ void main() { }); Future runThroughRunner(UninstallCommand command, List args) => - harness.run(['uninstall', ...args], uninstallCommand: command); + runner.run(['uninstall', ...args], uninstallCommand: command); Future runUninstall([ List args = const [], @@ -676,7 +676,7 @@ void main() { return ProcessResult(0, 0, '', ''); } - final exitCode = await harness.run( + final exitCode = await runner.run( [ 'install', for (final harness in Harness.values) ...['--only', harness.name], diff --git a/packages/vgv_ai_cli/test/src/commands/update_command_test.dart b/packages/vgv_ai_cli/test/src/commands/update_command_test.dart index 320c8d79..d7def9b3 100644 --- a/packages/vgv_ai_cli/test/src/commands/update_command_test.dart +++ b/packages/vgv_ai_cli/test/src/commands/update_command_test.dart @@ -8,7 +8,7 @@ import 'package:vgv_ai_cli/src/command_runner.dart'; import 'package:vgv_ai_cli/src/commands/commands.dart'; import 'package:vgv_ai_cli/src/version.dart'; -import '../../helpers/command_helper.dart'; +import '../../helpers/test_runner.dart'; void main() { group(UpdateCommand, () { @@ -19,10 +19,10 @@ void main() { late VgvAiCliCommandRunner commandRunner; setUp(() { - final harness = TestRunner(); - pubUpdater = harness.pubUpdater; - logger = harness.logger; - commandRunner = harness.build(); + final runner = TestRunner(); + pubUpdater = runner.pubUpdater; + logger = runner.logger; + commandRunner = runner.build(); when( () => pubUpdater.update(