command `pub check-resolution-up-to-date` (#4655)

diff --git a/lib/src/command/check_resolution_up_to_date.dart b/lib/src/command/check_resolution_up_to_date.dart
new file mode 100644
index 0000000..66fff46
--- /dev/null
+++ b/lib/src/command/check_resolution_up_to_date.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../command.dart';
+import '../command_runner.dart';
+import '../entrypoint.dart';
+import '../log.dart' as log;
+import '../utils.dart';
+
+class CheckResolutionUpToDateCommand extends PubCommand {
+  @override
+  String get name => 'check-resolution-up-to-date';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  String get description => '''
+Do a fast timestamp-based check to see resolution is up-to-date and internally
+consistent.
+
+If timestamps are correctly ordered, exit 0, and do not check the external sources for
+newer versions.
+Otherwise exit non-zero.
+''';
+
+  @override
+  String get argumentsDescription => '';
+
+  CheckResolutionUpToDateCommand();
+
+  @override
+  Future<void> runProtected() async {
+    final result = Entrypoint.isResolutionUpToDate(directory, cache);
+    if (result == null) {
+      fail('Resolution needs updating. Run `$topLevelProgram pub get`');
+    } else {
+      log.message('Resolution is up-to-date');
+    }
+  }
+}
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 9ced67e..9a372f1 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -13,6 +13,7 @@
 import 'command/add.dart';
 import 'command/bump.dart';
 import 'command/cache.dart';
+import 'command/check_resolution_up_to_date.dart';
 import 'command/deps.dart';
 import 'command/downgrade.dart';
 import 'command/get.dart';
@@ -151,6 +152,7 @@
     addCommand(OutdatedCommand());
     addCommand(RemoveCommand());
     addCommand(RunCommand());
+    addCommand(CheckResolutionUpToDateCommand());
     addCommand(UpgradeCommand());
     addCommand(UnpackCommand());
     addCommand(UploaderCommand());
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 3f2be96..24d25dc 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -775,23 +775,47 @@
     }
   }
 
-  /// Does a fast-pass check to see if the resolution is up-to-date. If not, run
-  /// a resolution with `pub get` semantics.
+  /// The [PackageConfig] object representing `.dart_tool/package_config.json`
+  /// along with the dir where it resides, if it and `pubspec.lock` exist and
+  /// are up to date with respect to pubspec.yaml and its dependencies. Or
+  /// `null` if it is outdated.
   ///
-  /// If [summaryOnly] is `true` (the default) only a short summary is shown of
-  /// the solve.
+  /// Always returns `null` if `.dart_tool/package_config.json` was generated
+  /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
+  /// different Dart or Flutter SDK version.
   ///
-  /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
-  /// output if no terminal is attached.
+  /// Otherwise first the `modified` timestamps are compared, and if
+  /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is
+  /// newer than all pubspec.yamls of all packages in
+  /// `.dart_tool/package_config.json` we short-circuit and return true.
   ///
-  /// When succesfull returns the found/created `PackageConfig` and the
-  /// directory containing it.
-  static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
-    String dir, {
-    required SystemCache cache,
-    bool summaryOnly = true,
-    bool onlyOutputWhenTerminal = true,
-  }) async {
+  /// If any of the timestamps are out of order, the resolution in
+  /// pubspec.lock is validated against constraints of all pubspec.yamls, and
+  /// the packages of `.dart_tool/package_config.json` is validated against
+  /// pubspec.lock. We do this extra round of checking to accommodate for cases
+  /// where version control or other processes mess up the timestamp order.
+  ///
+  /// If the resolution is still valid, the timestamps are updated and this
+  /// returns the package configuration and the root dir. Otherwise this
+  /// returns `null`.
+  ///
+  /// This check is on the fast-path of `dart run` and should do as little
+  /// work as possible. Specifically we avoid parsing any yaml when the
+  /// timestamps are in the right order.
+  ///
+  /// `.dart_tool/package_config.json` is read and parsed. In the case of `dart
+  /// run` this is acceptable: we speculate that it brings it to the file
+  /// system cache and the dart VM is going to read the file anyways.
+  ///
+  /// Note this procedure will give false positives if the timestamps are
+  /// artificially brought in the "right" order. (eg. by manually running
+  /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is
+  /// hard to avoid, but also unlikely to happen by accident because
+  /// `.dart_tool/package_config.json` is not checked into version control.
+  static (PackageConfig, String)? isResolutionUpToDate(
+    String dir,
+    SystemCache cache,
+  ) {
     late final wasRelative = p.isRelative(dir);
     String relativeIfNeeded(String path) =>
         wasRelative ? p.relative(path) : path;
@@ -1027,271 +1051,254 @@
       return true;
     }
 
-    /// The [PackageConfig] object representing `.dart_tool/package_config.json`
-    /// along with the dir where it resides, if it and `pubspec.lock` exist and
-    /// are up to date with respect to pubspec.yaml and its dependencies. Or
-    /// `null` if it is outdated.
-    ///
-    /// Always returns `null` if `.dart_tool/package_config.json` was generated
-    /// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
-    /// different Dart or Flutter SDK version.
-    ///
-    /// Otherwise first the `modified` timestamps are compared, and if
-    /// `.dart_tool/package_config.json` is newer than `pubspec.lock` that is
-    /// newer than all pubspec.yamls of all packages in
-    /// `.dart_tool/package_config.json` we short-circuit and return true.
-    ///
-    /// If any of the timestamps are out of order, the resolution in
-    /// pubspec.lock is validated against constraints of all pubspec.yamls, and
-    /// the packages of `.dart_tool/package_config.json` is validated against
-    /// pubspec.lock. We do this extra round of checking to accomodate for cases
-    /// where version control or other processes mess up the timestamp order.
-    ///
-    /// If the resolution is still valid, the timestamps are updated and this
-    /// returns the package configuration and the root dir. Otherwise this
-    /// returns `null`.
-    ///
-    /// This check is on the fast-path of `dart run` and should do as little
-    /// work as possible. Specifically we avoid parsing any yaml when the
-    /// timestamps are in the right order.
-    ///
-    /// `.dart_tool/package_config.json` is read parsed. In the case of `dart
-    /// run` this is acceptable: we speculate that it brings it to the file
-    /// system cache and the dart VM is going to read the file anyways.
-    ///
-    /// Note this procedure will give false positives if the timestamps are
-    /// artificially brought in the "right" order. (eg. by manually running
-    /// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is
-    /// hard to avoid, but also unlikely to happen by accident because
-    /// `.dart_tool/package_config.json` is not checked into version control.
-    (PackageConfig, String)? isResolutionUpToDate() {
-      FileStat? packageConfigStat;
-      late final String packageConfigPath;
-      late final String rootDir;
-      for (final parent in parentDirs(dir)) {
-        final potentialPackageConfigPath = p.normalize(
-          p.join(parent, '.dart_tool', 'package_config.json'),
+    FileStat? packageConfigStat;
+    late final String packageConfigPath;
+    late final String rootDir;
+    for (final parent in parentDirs(dir)) {
+      final potentialPackageConfigPath = p.normalize(
+        p.join(parent, '.dart_tool', 'package_config.json'),
+      );
+      packageConfigStat = tryStatFile(potentialPackageConfigPath);
+
+      if (packageConfigStat != null) {
+        packageConfigPath = potentialPackageConfigPath;
+        rootDir = parent;
+        break;
+      }
+      final potentialPubspecPath = p.join(parent, 'pubspec.yaml');
+      if (tryStatFile(potentialPubspecPath) == null) {
+        // No package at [parent] continue to next dir.
+        continue;
+      }
+
+      final potentialWorkspaceRefPath = p.normalize(
+        p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
+      );
+
+      final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
+      if (workspaceRefText == null) {
+        log.fine(
+          '`$potentialPubspecPath` exists without corresponding '
+          '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.',
         );
-        packageConfigStat = tryStatFile(potentialPackageConfigPath);
-
-        if (packageConfigStat != null) {
-          packageConfigPath = potentialPackageConfigPath;
-          rootDir = parent;
-          break;
-        }
-        final potentialPubspecPath = p.join(parent, 'pubspec.yaml');
-        if (tryStatFile(potentialPubspecPath) == null) {
-          // No package at [parent] continue to next dir.
-          continue;
-        }
-
-        final potentialWorkspaceRefPath = p.normalize(
-          p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
-        );
-
-        final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
-        if (workspaceRefText == null) {
-          log.fine(
-            '`$potentialPubspecPath` exists without corresponding '
-            '`$potentialPubspecPath` or `$potentialWorkspaceRefPath`.',
-          );
-          return null;
-        } else {
-          try {
-            if (jsonDecode(workspaceRefText) case {
-              'workspaceRoot': final String path,
-            }) {
-              final potentialPackageConfigPath2 = relativeIfNeeded(
+        return null;
+      } else {
+        try {
+          if (jsonDecode(workspaceRefText) case {
+            'workspaceRoot': final String path,
+          }) {
+            final potentialPackageConfigPath2 = relativeIfNeeded(
+              p.normalize(
+                p.absolute(
+                  p.join(
+                    p.dirname(potentialWorkspaceRefPath),
+                    path,
+                    '.dart_tool',
+                    'package_config.json',
+                  ),
+                ),
+              ),
+            );
+            packageConfigStat = tryStatFile(potentialPackageConfigPath2);
+            if (packageConfigStat == null) {
+              log.fine(
+                '`$potentialWorkspaceRefPath` points to non-existing '
+                '`$potentialPackageConfigPath2`',
+              );
+              return null;
+            } else {
+              packageConfigPath = potentialPackageConfigPath2;
+              rootDir = relativeIfNeeded(
                 p.normalize(
                   p.absolute(
-                    p.join(
-                      p.dirname(potentialWorkspaceRefPath),
-                      path,
-                      '.dart_tool',
-                      'package_config.json',
-                    ),
+                    p.join(p.dirname(potentialWorkspaceRefPath), path),
                   ),
                 ),
               );
-              packageConfigStat = tryStatFile(potentialPackageConfigPath2);
-              if (packageConfigStat == null) {
-                log.fine(
-                  '`$potentialWorkspaceRefPath` points to non-existing '
-                  '`$potentialPackageConfigPath2`',
-                );
-                return null;
-              } else {
-                packageConfigPath = potentialPackageConfigPath2;
-                rootDir = relativeIfNeeded(
-                  p.normalize(
-                    p.absolute(
-                      p.join(p.dirname(potentialWorkspaceRefPath), path),
-                    ),
-                  ),
-                );
 
-                break;
-              }
-            } else {
-              log.fine(
-                '`$potentialWorkspaceRefPath` '
-                'is missing "workspaceRoot" property',
-              );
-              return null;
+              break;
             }
-          } on FormatException catch (e) {
-            log.fine('`$potentialWorkspaceRefPath` not valid json: $e.');
+          } else {
+            log.fine(
+              '`$potentialWorkspaceRefPath` '
+              'is missing "workspaceRoot" property',
+            );
             return null;
           }
-        }
-      }
-      if (packageConfigStat == null) {
-        log.fine(
-          'Found no .dart_tool/package_config.json - no existing resolution.',
-        );
-        return null;
-      }
-      final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
-      final packageConfig = _loadPackageConfig(packageConfigPath);
-      if (p.isWithin(cache.rootDir, packageConfigPath)) {
-        // We always consider a global package (inside the cache) up-to-date.
-        return (packageConfig, rootDir);
-      }
-
-      /// Whether or not the `.dart_tool/package_config.json` file is was
-      /// generated by a different sdk down to changes in minor versions.
-      bool isPackageConfigGeneratedBySameDartSdk() {
-        final generatorVersion = packageConfig.generatorVersion;
-        if (generatorVersion == null ||
-            generatorVersion.major != sdk.version.major ||
-            generatorVersion.minor != sdk.version.minor) {
-          log.fine('The Dart SDK was updated since last package resolution.');
-          return false;
-        }
-        return true;
-      }
-
-      final flutter = FlutterSdk();
-      // If Flutter has moved since last invocation, we want to have new
-      // sdk-packages, and therefore do a new resolution.
-      //
-      // This also counts if Flutter was introduced or removed.
-      final flutterRoot =
-          flutter.rootDirectory == null
-              ? null
-              : p.toUri(p.absolute(flutter.rootDirectory!)).toString();
-      if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
-        log.fine('Flutter has moved since last invocation.');
-        return null;
-      }
-      if (packageConfig.additionalProperties['flutterVersion'] !=
-          (flutter.isAvailable ? flutter.version.toString() : null)) {
-        log.fine('Flutter has updated since last invocation.');
-        return null;
-      }
-      // If the pub cache was moved we should have a new resolution.
-      final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
-      if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
-        final previousPubCachePath =
-            packageConfig.additionalProperties['pubCache'];
-        log.fine(
-          'The pub cache has moved from $previousPubCachePath to $rootCacheUrl '
-          'since last invocation.',
-        );
-        return null;
-      }
-      // If the Dart sdk was updated we want a new resolution.
-      if (!isPackageConfigGeneratedBySameDartSdk()) {
-        return null;
-      }
-      final lockFileStat = tryStatFile(lockFilePath);
-      if (lockFileStat == null) {
-        log.fine('No $lockFilePath file found.');
-        return null;
-      }
-
-      final lockFileModified = lockFileStat.modified;
-      var lockfileNewerThanPubspecs = true;
-
-      // Check that all packages in packageConfig exist and their pubspecs have
-      // not been updated since the lockfile was written.
-      for (var package in packageConfig.packages) {
-        final pubspecPath = p.normalize(
-          p.join(
-            rootDir,
-            '.dart_tool',
-            package.rootUri
-                // Important to use `toFilePath()` here rather than `path`, as
-                // it handles Url-decoding.
-                .toFilePath(),
-            'pubspec.yaml',
-          ),
-        );
-        if (p.isWithin(cache.rootDir, pubspecPath)) {
-          continue;
-        }
-        final pubspecStat = tryStatFile(pubspecPath);
-        if (pubspecStat == null) {
-          log.fine('Could not find `$pubspecPath`');
-          // A dependency is missing - do a full new resolution.
+        } on FormatException catch (e) {
+          log.fine('`$potentialWorkspaceRefPath` not valid json: $e.');
           return null;
         }
-
-        if (pubspecStat.modified.isAfter(lockFileModified)) {
-          log.fine('`$pubspecPath` is newer than `$lockFilePath`');
-          lockfileNewerThanPubspecs = false;
-          break;
-        }
-        final pubspecOverridesPath = p.join(
-          package.rootUri.path,
-          'pubspec_overrides.yaml',
-        );
-        final pubspecOverridesStat = tryStatFile(pubspecOverridesPath);
-        if (pubspecOverridesStat != null) {
-          // This will wrongly require you to reresolve if a
-          // `pubspec_overrides.yaml` in a path-dependency is updated. That
-          // seems acceptable.
-          if (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
-            log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
-            lockfileNewerThanPubspecs = false;
-          }
-        }
       }
-      var touchedLockFile = false;
-      late final lockFile = _loadLockFile(lockFilePath, cache);
-      late final root = Package.load(
-        dir,
-        loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+    }
+    if (packageConfigStat == null) {
+      log.fine(
+        'Found no .dart_tool/package_config.json - no existing resolution.',
       );
-
-      if (!lockfileNewerThanPubspecs) {
-        if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
-          touch(lockFilePath);
-          touchedLockFile = true;
-        } else {
-          return null;
-        }
-      }
-
-      if (touchedLockFile ||
-          lockFileModified.isAfter(packageConfigStat.modified)) {
-        log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
-        if (isPackageConfigUpToDate(
-          packageConfig,
-          lockFile,
-          root,
-          packageConfigPath: packageConfigPath,
-          lockFilePath: lockFilePath,
-        )) {
-          touch(packageConfigPath);
-        } else {
-          return null;
-        }
-      }
+      return null;
+    }
+    final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
+    final packageConfig = _loadPackageConfig(packageConfigPath);
+    if (p.isWithin(cache.rootDir, packageConfigPath)) {
+      // We always consider a global package (inside the cache) up-to-date.
       return (packageConfig, rootDir);
     }
 
-    if (isResolutionUpToDate() case (
+    /// Whether or not the `.dart_tool/package_config.json` file was
+    /// generated by a different sdk down to changes in minor versions.
+    bool isPackageConfigGeneratedBySameDartSdk() {
+      final generatorVersion = packageConfig.generatorVersion;
+      if (generatorVersion == null ||
+          generatorVersion.major != sdk.version.major ||
+          generatorVersion.minor != sdk.version.minor) {
+        log.fine('The Dart SDK was updated since last package resolution.');
+        return false;
+      }
+      return true;
+    }
+
+    final flutter = FlutterSdk();
+    // If Flutter has moved since last invocation, we want to have new
+    // sdk-packages, and therefore do a new resolution.
+    //
+    // This also counts if Flutter was introduced or removed.
+    final flutterRoot =
+        flutter.rootDirectory == null
+            ? null
+            : p.toUri(p.absolute(flutter.rootDirectory!)).toString();
+    if (packageConfig.additionalProperties['flutterRoot'] != flutterRoot) {
+      log.fine('Flutter has moved since last invocation.');
+      return null;
+    }
+    if (packageConfig.additionalProperties['flutterVersion'] !=
+        (flutter.isAvailable ? flutter.version.toString() : null)) {
+      log.fine('Flutter has updated since last invocation.');
+      return null;
+    }
+    // If the pub cache was moved we should have a new resolution.
+    final rootCacheUrl = p.toUri(p.absolute(cache.rootDir)).toString();
+    if (packageConfig.additionalProperties['pubCache'] != rootCacheUrl) {
+      final previousPubCachePath =
+          packageConfig.additionalProperties['pubCache'];
+      log.fine(
+        'The pub cache has moved from $previousPubCachePath to $rootCacheUrl '
+        'since last invocation.',
+      );
+      return null;
+    }
+    // If the Dart sdk was updated we want a new resolution.
+    if (!isPackageConfigGeneratedBySameDartSdk()) {
+      return null;
+    }
+    final lockFileStat = tryStatFile(lockFilePath);
+    if (lockFileStat == null) {
+      log.fine('No $lockFilePath file found.');
+      return null;
+    }
+
+    final lockFileModified = lockFileStat.modified;
+    var lockfileNewerThanPubspecs = true;
+
+    // Check that all packages in packageConfig exist and their pubspecs have
+    // not been updated since the lockfile was written.
+    for (var package in packageConfig.packages) {
+      final pubspecPath = p.normalize(
+        p.join(
+          rootDir,
+          '.dart_tool',
+          package.rootUri
+              // Important to use `toFilePath()` here rather than `path`, as
+              // it handles Url-decoding.
+              .toFilePath(),
+          'pubspec.yaml',
+        ),
+      );
+      if (p.isWithin(cache.rootDir, pubspecPath)) {
+        continue;
+      }
+      final pubspecStat = tryStatFile(pubspecPath);
+      if (pubspecStat == null) {
+        log.fine('Could not find `$pubspecPath`');
+        // A dependency is missing - do a full new resolution.
+        return null;
+      }
+
+      if (pubspecStat.modified.isAfter(lockFileModified)) {
+        log.fine('`$pubspecPath` is newer than `$lockFilePath`');
+        lockfileNewerThanPubspecs = false;
+        break;
+      }
+      final pubspecOverridesPath = p.join(
+        package.rootUri.path,
+        'pubspec_overrides.yaml',
+      );
+      final pubspecOverridesStat = tryStatFile(pubspecOverridesPath);
+      if (pubspecOverridesStat != null) {
+        // This will wrongly require you to reresolve if a
+        // `pubspec_overrides.yaml` in a path-dependency is updated. That
+        // seems acceptable.
+        if (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
+          log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
+          lockfileNewerThanPubspecs = false;
+        }
+      }
+    }
+    var touchedLockFile = false;
+    late final lockFile = _loadLockFile(lockFilePath, cache);
+    late final root = Package.load(
+      dir,
+      loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+    );
+
+    if (!lockfileNewerThanPubspecs) {
+      if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
+        touch(lockFilePath);
+        touchedLockFile = true;
+      } else {
+        return null;
+      }
+    }
+
+    if (touchedLockFile ||
+        lockFileModified.isAfter(packageConfigStat.modified)) {
+      log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
+      if (isPackageConfigUpToDate(
+        packageConfig,
+        lockFile,
+        root,
+        packageConfigPath: packageConfigPath,
+        lockFilePath: lockFilePath,
+      )) {
+        touch(packageConfigPath);
+      } else {
+        return null;
+      }
+    }
+    return (packageConfig, rootDir);
+  }
+
+  /// Does a fast-pass check to see if the resolution is up-to-date. If not, run
+  /// a resolution with `pub get` semantics.
+  ///
+  /// If [summaryOnly] is `true` (the default) only a short summary is shown of
+  /// the solve.
+  ///
+  /// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
+  /// output if no terminal is attached.
+  ///
+  /// When succesfull returns the found/created `PackageConfig` and the
+  /// directory containing it.
+  static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
+    String dir, {
+    required SystemCache cache,
+    bool summaryOnly = true,
+    bool onlyOutputWhenTerminal = true,
+  }) async {
+    late final wasRelative = p.isRelative(dir);
+    String relativeIfNeeded(String path) =>
+        wasRelative ? p.relative(path) : path;
+
+    if (isResolutionUpToDate(dir, cache) case (
       final PackageConfig packageConfig,
       final String rootDir,
     )) {
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 408d00a..0ed7723 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -7,6 +7,7 @@
 import 'command/add.dart';
 import 'command/bump.dart';
 import 'command/cache.dart';
+import 'command/check_resolution_up_to_date.dart';
 import 'command/deps.dart';
 import 'command/downgrade.dart';
 import 'command/get.dart';
@@ -83,6 +84,7 @@
     addSubcommand(LishCommand());
     addSubcommand(OutdatedCommand());
     addSubcommand(RemoveCommand());
+    addSubcommand(CheckResolutionUpToDateCommand());
     addSubcommand(RunCommand(deprecated: true, alwaysUseSubprocess: true));
     addSubcommand(UnpackCommand());
     addSubcommand(UpgradeCommand());
diff --git a/test/check_resolution_up_to_date_test.dart b/test/check_resolution_up_to_date_test.dart
new file mode 100644
index 0000000..45e0edc
--- /dev/null
+++ b/test/check_resolution_up_to_date_test.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('Will exit non-zero if there are changes', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.appDir(dependencies: {'foo': '1.0.0'}).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+
+      exitCode: 1,
+    );
+
+    await d.dir(appPath, [
+      d.nothing('pubspec.lock'),
+      d.nothing('.dart_tool/package_config.json'),
+    ]).validate();
+
+    await pubGet();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      output: contains('Resolution is up-to-date'),
+      exitCode: 0,
+    );
+    await Future<Null>.delayed(const Duration(seconds: 1));
+
+    await d.appDir(dependencies: {'foo': '2.0.0'}).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+  });
+
+  test('Works in a workspace', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myapp',
+        '1.0.0',
+        sdk: '^3.5.0',
+        deps: {'foo': '1.0.0'},
+        extras: {
+          'workspace': ['pkg'],
+        },
+      ),
+      d.dir('pkg', [d.libPubspec('pkg', '1.0.0', resolutionWorkspace: true)]),
+    ]).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      output: contains('+ foo 1.0.0'),
+    );
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      output: contains('Resolution is up-to-date'),
+      exitCode: 0,
+    );
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myapp',
+        '1.0.0',
+        sdk: '^3.5.0',
+        deps: {'foo': '1.0.0'},
+        extras: {
+          'workspace': ['pkg'],
+        },
+      ),
+    ]).create();
+
+    await runPub(
+      args: ['check-resolution-up-to-date'],
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+      workingDirectory: p.join(d.sandbox, appPath, 'pkg'),
+      error: contains('Resolution needs updating. Run `dart pub get`'),
+      exitCode: 1,
+    );
+  });
+}