Consider equal timestamps to be outdated (#4833)
diff --git a/lib/src/command/check_resolution_up_to_date.dart b/lib/src/command/check_resolution_up_to_date.dart
index 66fff46..99d7fb8 100644
--- a/lib/src/command/check_resolution_up_to_date.dart
+++ b/lib/src/command/check_resolution_up_to_date.dart
@@ -34,7 +34,11 @@
 
   @override
   Future<void> runProtected() async {
-    final result = Entrypoint.isResolutionUpToDate(directory, cache);
+    final result = Entrypoint.isResolutionUpToDate(
+      directory,
+      cache,
+      updateOutOfDateTimestamps: false,
+    );
     if (result == null) {
       fail('Resolution needs updating. Run `$topLevelProgram pub get`');
     } else {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 0583b29..75adf4e 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -833,9 +833,9 @@
   /// 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`.
+  /// If the resolution is still valid, the timestamps are updated (unless
+  /// [updateOutOfDateTimestamps] is false) 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
@@ -852,12 +852,24 @@
   /// `.dart_tool/package_config.json` is not checked into version control.
   static (PackageConfig, String)? isResolutionUpToDate(
     String dir,
-    SystemCache cache,
-  ) {
+    SystemCache cache, {
+    bool updateOutOfDateTimestamps = true,
+  }) {
     late final wasRelative = p.isRelative(dir);
     String relativeIfNeeded(String path) =>
         wasRelative ? p.relative(path) : path;
 
+    late final rootPackageDir =
+        parentDirs(dir).firstWhereOrNull(
+          (parent) => tryStatFile(p.join(parent, 'pubspec.yaml')) != null,
+        ) ??
+        dir;
+    late final root = Package.load(
+      rootPackageDir,
+      loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+    );
+    late final Package workspaceRoot;
+
     /// Whether the lockfile is out of date with respect to the dependencies'
     /// pubspecs.
     ///
@@ -871,6 +883,12 @@
       /// Returns whether the locked version of [dep] matches the dependency.
       bool isDependencyUpToDate(PackageRange dep) {
         if (dep.name == root.name) return true;
+        // Workspace packages are local source packages and are never listed in
+        // the `packages` section of `pubspec.lock`. They are always considered
+        // up-to-date here.
+        if (workspaceRoot.transitiveWorkspace.any((p) => p.name == dep.name)) {
+          return true;
+        }
 
         final locked = lockFile.packages[dep.name];
         return locked != null && dep.allows(locked);
@@ -968,7 +986,9 @@
         // they are not supposed to work.
         final hasExtraMappings =
             !packagePathsMapping.keys.every((packageName) {
-              return packageName == root.name ||
+              return workspaceRoot.transitiveWorkspace.any(
+                    (p) => p.name == packageName,
+                  ) ||
                   lockFile.packages.containsKey(packageName);
             });
         if (hasExtraMappings) {
@@ -990,8 +1010,8 @@
           }
 
           final source = lockFileId.source;
-          final lockFilePackagePath = root.path(
-            cache.getDirectory(lockFileId, relativeFrom: root.dir),
+          final lockFilePackagePath = workspaceRoot.path(
+            cache.getDirectory(lockFileId, relativeFrom: workspaceRoot.dir),
           );
 
           // Make sure that the packagePath agrees with the lock file about the
@@ -1024,7 +1044,7 @@
           );
           return false;
         }
-        packagePathsMapping[pkg.name] = root.path(
+        packagePathsMapping[pkg.name] = workspaceRoot.path(
           '.dart_tool',
           p.fromUri(pkg.rootUri),
         );
@@ -1046,6 +1066,20 @@
       // correct. This is important for path dependencies as these can mutate.
       for (final pkg in packageConfig.packages) {
         if (pkg.name == root.name) continue;
+        final workspacePkg = workspaceRoot.transitiveWorkspace.firstWhereOrNull(
+          (p) => p.name == pkg.name,
+        );
+        if (workspacePkg != null) {
+          if (pkg.languageVersion != workspacePkg.pubspec.languageVersion) {
+            log.fine(
+              '${workspacePkg.pubspecPath} has '
+              'changed since the $lockFilePath file was generated.',
+            );
+            return false;
+          }
+          continue;
+        }
+
         final id = lockFile.packages[pkg.name];
         if (id == null) {
           assert(
@@ -1175,12 +1209,19 @@
       );
       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);
     }
+    workspaceRoot =
+        rootDir == rootPackageDir
+            ? root
+            : Package.load(
+              rootDir,
+              loadPubspec: Pubspec.loadRootWithSources(cache.sources),
+            );
+    final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
 
     /// Whether or not the `.dart_tool/package_config.json` file was
     /// generated by a different sdk down to changes in minor versions.
@@ -1236,6 +1277,11 @@
 
     final lockFileModified = lockFileStat.modified;
     var lockfileNewerThanPubspecs = true;
+    // Whether any pubspec is strictly newer than the lockfile.
+    // We only touch the lockfile to make it newer if this is true, avoiding
+    // touching it on equal timestamps to prevent cascading invalidation of
+    // .dart_tool/package_config.json and downstream developer tools.
+    var pubspecStrictlyNewer = false;
 
     // Check that all packages in packageConfig exist and their pubspecs have
     // not been updated since the lockfile was written.
@@ -1261,10 +1307,13 @@
         return null;
       }
 
-      if (pubspecStat.modified.isAfter(lockFileModified)) {
+      if (!lockFileModified.isAfter(pubspecStat.modified)) {
         log.fine('`$pubspecPath` is newer than `$lockFilePath`');
         lockfileNewerThanPubspecs = false;
-        break;
+        if (pubspecStat.modified.isAfter(lockFileModified)) {
+          pubspecStrictlyNewer = true;
+          break;
+        }
       }
       final pubspecOverridesPath = p.join(
         package.rootUri.path,
@@ -1275,30 +1324,47 @@
         // 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)) {
+        if (!lockFileModified.isAfter(pubspecOverridesStat.modified)) {
           log.fine('`$pubspecOverridesPath` is newer than `$lockFilePath`');
           lockfileNewerThanPubspecs = false;
+          if (pubspecOverridesStat.modified.isAfter(lockFileModified)) {
+            pubspecStrictlyNewer = true;
+            break;
+          }
         }
       }
     }
+    if (!updateOutOfDateTimestamps && !lockfileNewerThanPubspecs) {
+      log.fine(
+        'Timestamps are out of order (updateOutOfDateTimestamps: false)',
+      );
+      return null;
+    }
     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;
+        if (pubspecStrictlyNewer) {
+          touch(lockFilePath);
+          touchedLockFile = true;
+        }
       } else {
         return null;
       }
     }
 
+    if (!updateOutOfDateTimestamps &&
+        packageConfigStat.modified.isBefore(lockFileModified)) {
+      log.fine(
+        'Timestamps are out of order (updateOutOfDateTimestamps: false)',
+      );
+      return null;
+    }
+
     if (touchedLockFile ||
-        lockFileModified.isAfter(packageConfigStat.modified)) {
+        !lockfileNewerThanPubspecs ||
+        packageConfigStat.modified.isBefore(lockFileModified)) {
       log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
       if (isPackageConfigUpToDate(
         packageConfig,
@@ -1307,7 +1373,10 @@
         packageConfigPath: packageConfigPath,
         lockFilePath: lockFilePath,
       )) {
-        touch(packageConfigPath);
+        if (touchedLockFile ||
+            lockFileModified.isAfter(packageConfigStat.modified)) {
+          touch(packageConfigPath);
+        }
       } else {
         return null;
       }
diff --git a/test/check_resolution_up_to_date_test.dart b/test/check_resolution_up_to_date_test.dart
index 316a11c..b24c315 100644
--- a/test/check_resolution_up_to_date_test.dart
+++ b/test/check_resolution_up_to_date_test.dart
@@ -38,9 +38,6 @@
       exitCode: 0,
     );
 
-    // Timestamp resolution is rather poor especially on windows.
-    await Future<Null>.delayed(const Duration(seconds: 1));
-
     await d.appDir(dependencies: {'foo': '2.0.0'}).create();
 
     await runPub(
@@ -89,9 +86,6 @@
       exitCode: 0,
     );
 
-    // Timestamp resolution is rather poor especially on windows.
-    await Future<Null>.delayed(const Duration(seconds: 1));
-
     await d.dir(appPath, [
       d.libPubspec(
         'myapp',
diff --git a/test/embedding/ensure_pubspec_resolved.dart b/test/embedding/ensure_pubspec_resolved.dart
index 4808368..d7b5050 100644
--- a/test/embedding/ensure_pubspec_resolved.dart
+++ b/test/embedding/ensure_pubspec_resolved.dart
@@ -379,11 +379,10 @@
         await d.dir(appPath, [
           d.appPubspec(dependencies: {'foo': '1.0.0'}),
         ]).create();
-        // Ensure we get a new mtime (mtime is only reported with 1s precision)
-        await _touch('pubspec.yaml');
-
-        await _touch('pubspec.lock');
-        await _touch('.dart_tool/package_config.json');
+        // Ensure we get a new mtime across files on all platforms
+        await _touchWithDelay('pubspec.yaml');
+        await _touchWithDelay('pubspec.lock');
+        await _touchWithDelay('.dart_tool/package_config.json');
 
         await _noImplicitPubGet();
       });
@@ -535,10 +534,13 @@
 
 /// Schedules a non-semantic modification to [path].
 Future _touch(String path) async {
-  // Delay a bit to make sure the modification times are noticeably different.
-  // 1s seems to be the finest granularity that dart:io reports.
-  await Future<void>.delayed(const Duration(seconds: 1));
+  path = p.join(d.sandbox, 'myapp', path);
+  touch(path);
+}
 
+/// Schedules a non-semantic modification to [path] with an artificial delay.
+Future _touchWithDelay(String path) async {
+  await Future<void>.delayed(const Duration(milliseconds: 10));
   path = p.join(d.sandbox, 'myapp', path);
   touch(path);
 }