diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart
index 25bfa910fd2c897d4b2698c2dda3b7184b084cc3..32cb0cdd447f5e265171bebc4e9c7c83e9f8aa7d 100644
--- a/lib/src/command/deps.dart
+++ b/lib/src/command/deps.dart
@@ -74,7 +74,7 @@ class DepsCommand extends PubCommand {
     _buffer.writeln();
     _buffer.writeln("$section:");
     for (var name in ordered(names)) {
-      var package = entrypoint.packageGraph.packages[name];
+      var package = _getPackage(name);
 
       _buffer.write("- ${_labelPackage(package)}");
       if (package.dependencies.isEmpty) {
@@ -115,7 +115,7 @@ class DepsCommand extends PubCommand {
     _buffer.writeln("$name:");
 
     for (var name in deps) {
-      var package = entrypoint.packageGraph.packages[name];
+      var package = _getPackage(name);
       _buffer.writeln("- ${_labelPackage(package)}");
 
       for (var dep in package.dependencies) {
@@ -141,8 +141,7 @@ class DepsCommand extends PubCommand {
     // Start with the root dependencies.
     var packageTree = {};
     for (var dep in entrypoint.root.immediateDependencies) {
-      toWalk.add(
-          new Pair(entrypoint.packageGraph.packages[dep.name], packageTree));
+      toWalk.add(new Pair(_getPackage(dep.name), packageTree));
     }
 
     // Do a breadth-first walk to the dependency graph.
@@ -163,8 +162,7 @@ class DepsCommand extends PubCommand {
       map[_labelPackage(package)] = childMap;
 
       for (var dep in package.dependencies) {
-        toWalk.add(
-            new Pair(entrypoint.packageGraph.packages[dep.name], childMap));
+        toWalk.add(new Pair(_getPackage(dep.name), childMap));
       }
     }
 
@@ -184,4 +182,17 @@ class DepsCommand extends PubCommand {
     transitive.removeAll(root.dependencyOverrides.map((dep) => dep.name));
     return transitive;
   }
+
+  /// Get the package named [name], or throw a [DataError] if it's not
+  /// available.
+  ///
+  /// It's very unlikely that the lockfile won't be up-to-date with the pubspec,
+  /// but it's possible, since [Entrypoint.assertUpToDate]'s modification time
+  /// check can return a false negative. This fails gracefully if that happens.
+  Package _getPackage(String name) {
+    var package = entrypoint.packageGraph.packages[name];
+    if (package != null) return package;
+    dataError('The pubspec.yaml file has changed since the pubspec.lock file '
+        'was generated, please run "pub get" again.');
+  }
 }
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 3c2fa25809eff4062f859eab320c927b99f9cacf..fd88b8da80cf42d7ec64f00d21f465121658f418 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -7,7 +7,8 @@ library pub.entrypoint;
 import 'dart:async';
 import 'dart:io';
 
-import 'package:path/path.dart' as path;
+import 'package:package_config/packages_file.dart' as packages_file;
+import 'package:path/path.dart' as p;
 import 'package:barback/barback.dart';
 
 import 'barback/asset_environment.dart';
@@ -201,13 +202,13 @@ class Entrypoint {
     // Just precompile the debug version of a package. We're mostly interested
     // in improving speed for development iteration loops, which usually use
     // debug mode.
-    var depsDir = path.join('.pub', 'deps', 'debug');
+    var depsDir = p.join('.pub', 'deps', 'debug');
 
     var dependenciesToPrecompile = packageGraph.packages.values
         .where((package) {
       if (package.pubspec.transformers.isEmpty) return false;
       if (packageGraph.isPackageMutable(package.name)) return false;
-      if (!dirExists(path.join(depsDir, package.name))) return true;
+      if (!dirExists(p.join(depsDir, package.name))) return true;
       if (changed == null) return true;
 
       /// Only recompile [package] if any of its transitive dependencies have
@@ -222,12 +223,12 @@ class Entrypoint {
     if (dirExists(depsDir)) {
       // Delete any cached dependencies that are going to be recached.
       for (var package in dependenciesToPrecompile) {
-        deleteEntry(path.join(depsDir, package));
+        deleteEntry(p.join(depsDir, package));
       }
 
       // Also delete any cached dependencies that should no longer be cached.
       for (var subdir in listDir(depsDir)) {
-        var package = packageGraph.packages[path.basename(subdir)];
+        var package = packageGraph.packages[p.basename(subdir)];
         if (package == null || package.pubspec.transformers.isEmpty ||
             packageGraph.isPackageMutable(package.name)) {
           deleteEntry(subdir);
@@ -257,9 +258,9 @@ class Entrypoint {
         await waitAndPrintErrors(assets.map((asset) async {
           if (!dependenciesToPrecompile.contains(asset.id.package)) return;
 
-          var destPath = path.join(
-              depsDir, asset.id.package, path.fromUri(asset.id.path));
-          ensureDir(path.dirname(destPath));
+          var destPath = p.join(
+              depsDir, asset.id.package, p.fromUri(asset.id.path));
+          ensureDir(p.dirname(destPath));
           await createFileFromStream(asset.read(), destPath);
         }));
 
@@ -271,7 +272,7 @@ class Entrypoint {
       // assets (issue 19491), catch and handle compilation errors on a
       // per-package basis.
       for (var package in dependenciesToPrecompile) {
-        deleteEntry(path.join(depsDir, package));
+        deleteEntry(p.join(depsDir, package));
       }
       rethrow;
     }
@@ -282,8 +283,8 @@ class Entrypoint {
   Future precompileExecutables({Iterable<String> changed}) async {
     if (changed != null) changed = changed.toSet();
 
-    var binDir = path.join('.pub', 'bin');
-    var sdkVersionPath = path.join(binDir, 'sdk-version');
+    var binDir = p.join('.pub', 'bin');
+    var sdkVersionPath = p.join(binDir, 'sdk-version');
 
     // If the existing executable was compiled with a different SDK, we need to
     // recompile regardless of what changed.
@@ -297,7 +298,7 @@ class Entrypoint {
       for (var entry in listDir(binDir)) {
         if (!dirExists(entry)) continue;
 
-        var package = path.basename(entry);
+        var package = p.basename(entry);
         if (!packageGraph.packages.containsKey(package) ||
             packageGraph.isPackageMutable(package)) {
           deleteEntry(entry);
@@ -337,7 +338,7 @@ class Entrypoint {
       });
 
       await waitAndPrintErrors(executables.keys.map((package) async {
-        var dir = path.join(binDir, package);
+        var dir = p.join(binDir, package);
         cleanDir(dir);
         await environment.precompileExecutables(package, dir,
             executableIds: executables[package]);
@@ -373,8 +374,8 @@ class Entrypoint {
     // changed. Since we delete the bin directory before recompiling, we need to
     // recompile all executables.
     var executablesExist = executables.every((executable) =>
-        fileExists(path.join('.pub', 'bin', packageName,
-            "${path.url.basename(executable.path)}.snapshot")));
+        fileExists(p.join('.pub', 'bin', packageName,
+            "${p.url.basename(executable.path)}.snapshot")));
     if (!executablesExist) return executables;
 
     // Otherwise, we don't need to recompile.
@@ -396,7 +397,7 @@ class Entrypoint {
         return source.downloadToSystemCache(id);
       }
 
-      var packageDir = path.join(packagesDir, id.name);
+      var packageDir = p.join(packagesDir, id.name);
       if (entryExists(packageDir)) deleteEntry(packageDir);
       return source.get(id, packageDir);
     }).then((_) => source.resolveId(id));
@@ -415,20 +416,127 @@ class Entrypoint {
       dataError('No .packages file found, please run "pub get" first.');
     }
 
-    var packagesModified = new File(packagesFile).lastModifiedSync();
     var pubspecModified = new File(pubspecPath).lastModifiedSync();
-    if (packagesModified.isBefore(pubspecModified)) {
-      dataError('The pubspec.yaml file has changed since the .packages file '
-          'was generated, please run "pub get" again.');
+    var lockFileModified = new File(lockFilePath).lastModifiedSync();
+
+    var touchedLockFile = false;
+    if (lockFileModified.isBefore(pubspecModified)) {
+      if (_isLockFileUpToDate() && _arePackagesAvailable()) {
+        touchedLockFile = true;
+        touch(lockFilePath);
+      } else {
+        dataError('The pubspec.yaml file has changed since the pubspec.lock '
+            'file was generated, please run "pub get" again.');
+      }
     }
 
-    var lockFileModified = new File(lockFilePath).lastModifiedSync();
+    var packagesModified = new File(packagesFile).lastModifiedSync();
     if (packagesModified.isBefore(lockFileModified)) {
-      dataError('The pubspec.lock file has changed since the .packages file '
-          'was generated, please run "pub get" again.');
+      if (_isPackagesFileUpToDate()) {
+        touch(packagesFile);
+      } else {
+        dataError('The pubspec.lock file has changed since the .packages file '
+            'was generated, please run "pub get" again.');
+      }
+    } else if (touchedLockFile) {
+      touch(packagesFile);
     }
   }
 
+  /// Determines whether or not the lockfile is out of date with respect to the
+  /// pubspec.
+  ///
+  /// This will be `false` if the pubspec contains dependencies that are not in
+  /// the lockfile or that don't match what's in there.
+  bool _isLockFileUpToDate() {
+    return root.immediateDependencies.every((package) {
+      var locked = lockFile.packages[package.name];
+      if (locked == null) return false;
+
+      if (package.source != locked.source) return false;
+
+      if (!package.constraint.allows(locked.version)) return false;
+
+      var source = cache.sources[package.source];
+      if (source == null) return false;
+
+      return source.descriptionsEqual(package.description, locked.description);
+    });
+  }
+
+  /// Determines whether all of the packages in the lockfile are already
+  /// installed and available.
+  ///
+  /// Note: this assumes [_isLockFileUpToDate] has already been called and
+  /// returned `true`.
+  bool _arePackagesAvailable() {
+    return lockFile.packages.values.every((package) {
+      var source = cache.sources[package.source];
+
+      // This should only be called after [_isLockFileUpToDate] has returned
+      // `true`, which ensures all of the sources in the lock file are valid.
+      assert(source != null);
+
+      // We only care about cached sources. Uncached sources aren't "installed".
+      // If one of those is missing, we want to show the user the file not
+      // found error later since installing won't accomplish anything.
+      if (source is! CachedSource) return true;
+
+      // Get the directory.
+      var dir = source.getDirectory(package);
+      // See if the directory is there and looks like a package.
+      return dirExists(dir) && fileExists(p.join(dir, "pubspec.yaml"));
+    });
+  }
+
+  /// Determines whether or not the `.packages` file is out of date with respect
+  /// to the lockfile.
+  ///
+  /// This will be `false` if the packages file contains dependencies that are
+  /// not in the lockfile or that don't match what's in there.
+  bool _isPackagesFileUpToDate() {
+    var packages = packages_file.parse(
+        new File(packagesFile).readAsBytesSync(),
+        p.toUri(packagesFile));
+
+    return lockFile.packages.values.every((lockFileId) {
+      var source = cache.sources[lockFileId.source];
+
+      // It's very unlikely that the lockfile is invalid here, but it's not
+      // impossible—for example, the user may have a very old application
+      // package with a checked-in lockfile that's newer than the pubspec, but
+      // that contains sdk dependencies.
+      if (source == null) return false;
+
+      var packagesFileUri = packages[lockFileId.name];
+      if (packagesFileUri == null) return false;
+
+      // Pub only generates "file:" and relative URIs.
+      if (packagesFileUri.scheme != 'file' &&
+          packagesFileUri.scheme.isNotEmpty) {
+        return false;
+      }
+
+      // Get the dirname of the .packages path, since it's pointing to lib/.
+      var packagesFilePath = p.dirname(
+          p.join(root.dir, p.fromUri(packagesFileUri)));
+      var lockFilePath = p.join(root.dir, source.getDirectory(lockFileId));
+
+      // For cached sources, make sure the directory exists and looks like a
+      // package. This is also done by [_arePackagesAvailable] but that may not
+      // be run if the lockfile is newer than the pubspec.
+      if (source is CachedSource &&
+          !dirExists(packagesFilePath) ||
+          !fileExists(p.join(packagesFilePath, "pubspec.yaml"))) {
+        return false;
+      }
+
+      // Make sure that the packages file agrees with the lock file about the
+      // path to the package.
+      return p.normalize(packagesFilePath) == p.normalize(lockFilePath);
+    });
+  }
+
   /// Saves a list of concrete package versions to the `pubspec.lock` file.
   void _saveLockFile(List<PackageId> packageIds) {
     _lockFile = new LockFile(packageIds, cache.sources);
@@ -439,7 +547,7 @@ class Entrypoint {
   /// Creates a self-referential symlink in the `packages` directory that allows
   /// a package to import its own files using `package:`.
   void _linkSelf() {
-    var linkPath = path.join(packagesDir, root.name);
+    var linkPath = p.join(packagesDir, root.name);
     // Create the symlink if it doesn't exist.
     if (entryExists(linkPath)) return;
     ensureDir(packagesDir);
@@ -482,7 +590,7 @@ class Entrypoint {
   /// files and `package` files.
   List<String> _listDirWithoutPackages(dir) {
     return flatten(listDir(dir).map((file) {
-      if (path.basename(file) == 'packages') return [];
+      if (p.basename(file) == 'packages') return [];
       if (!dirExists(file)) return [];
       var fileAndSubfiles = [file];
       fileAndSubfiles.addAll(_listDirWithoutPackages(file));
@@ -495,7 +603,7 @@ class Entrypoint {
   ///
   /// Otherwise, deletes a "packages" directories in [dir] if one exists.
   void _linkOrDeleteSecondaryPackageDir(String dir) {
-    var symlink = path.join(dir, 'packages');
+    var symlink = p.join(dir, 'packages');
     if (entryExists(symlink)) deleteEntry(symlink);
     if (_packageSymlinks) createSymlink(packagesDir, symlink, relative: true);
   }
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 65b276ee4cdbf5b5c430aa4d527d54fd687ca7a9..bd21b4d8301e0d90739eb64b9598455fb9387ac7 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -82,6 +82,10 @@ Future<int> runExecutable(Entrypoint entrypoint, String package,
       // default mode for them to run. We can't run them in a different mode
       // using the snapshot.
       mode == BarbackMode.RELEASE) {
+    // Since we don't access the package graph, this doesn't happen
+    // automatically.
+    entrypoint.assertUpToDate();
+
     return _runCachedExecutable(entrypoint, localSnapshotPath, args,
         checked: checked);
   }
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 8b7fc17cdf73621c2aa9c22438448f9faa3b030d..952751c451bf9adb716ac7fa76fb90549be54f77 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -831,6 +831,15 @@ _doProcess(Function fn, String executable, List<String> args,
       environment: environment);
 }
 
+/// Updates [path]'s modification time.
+void touch(String path) {
+  var file = new File(path).openSync(mode: FileMode.APPEND);
+  var originalLength = file.lengthSync();
+  file.writeByteSync(0);
+  file.truncateSync(originalLength);
+  file.closeSync();
+}
+
 /// Creates a temporary directory and passes its path to [fn].
 ///
 /// Once the [Future] returned by [fn] completes, the temporary directory and
diff --git a/test/must_pub_get_test.dart b/test/must_pub_get_test.dart
index 5a10d82dcb87fe12a0553e0f943a2ffb1129f06a..981d9ff13a417373f5be50ef0f2445b17785c52a 100644
--- a/test/must_pub_get_test.dart
+++ b/test/must_pub_get_test.dart
@@ -4,39 +4,42 @@
 
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io';
 
 import 'package:path/path.dart' as p;
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:pub/src/io.dart';
+import 'package:scheduled_test/scheduled_stream.dart';
 import 'package:scheduled_test/scheduled_test.dart';
 
 import 'descriptor.dart' as d;
 import 'test_pub.dart';
 
 main() {
-  group("requires the user to run pub get first if", () {
-    setUp(() {
-      d.dir(appPath, [
-        d.appPubspec(),
-        d.dir("web", []),
-        d.dir("bin", [
-          d.file("script.dart", "main() => print('hello!');")
-        ])
-      ]).create();
+  setUp(() {
+    servePackages((builder) {
+      builder.serve("foo", "1.0.0");
+      builder.serve("foo", "2.0.0");
+    });
 
-      pubGet();
+    d.dir(appPath, [
+      d.appPubspec(),
+      d.dir("web", []),
+      d.dir("bin", [
+        d.file("script.dart", "main() => print('hello!');")
+      ])
+    ]).create();
 
-      // Delay a bit to make sure the modification times are noticeably
-      // different. 1s seems to be the finest granularity that dart:io reports.
-      schedule(() => new Future.delayed(new Duration(seconds: 1)));
-    });
+    pubGet();
+  });
 
+  group("requires the user to run pub get first if", () {
     group("there's no lockfile", () {
       setUp(() {
         schedule(() => deleteEntry(p.join(sandboxDir, "myapp/pubspec.lock")));
       });
 
-      _forEveryCommand(
+      _requiresPubGet(
           'No pubspec.lock file found, please run "pub get" first.');
     });
 
@@ -45,55 +48,335 @@ main() {
         schedule(() => deleteEntry(p.join(sandboxDir, "myapp/.packages")));
       });
 
-      _forEveryCommand('No .packages file found, please run "pub get" first.');
+      _requiresPubGet('No .packages file found, please run "pub get" first.');
+    });
+
+    group("the pubspec has a new dependency", () {
+      setUp(() {
+        d.dir("foo", [
+          d.libPubspec("foo", "1.0.0")
+        ]).create();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": {"path": "../foo"}})
+        ]).create();
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
+      });
+
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
+    });
+
+    group("the lockfile has a dependency from the wrong source", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        createLockFile(appPath, sandbox: ["foo"]);
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
+      });
+
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
     });
 
-    group("the pubspec is newer than the package spec", () {
+    group("the lockfile has a dependency from an unknown source", () {
       setUp(() {
-        schedule(() => _touch("pubspec.yaml"));
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        d.dir(appPath, [
+          d.file("pubspec.lock", yaml({
+            "packages": {
+              "foo": {
+                "description": "foo", 
+                "version": "1.0.0",
+                "source": "sdk"
+              }
+            }
+          }))
+        ]).create();
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
       });
 
-      _forEveryCommand('The pubspec.yaml file has changed since the .packages '
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
+    });
+
+    group("the lockfile has a dependency with the wrong description", () {
+      setUp(() {
+        d.dir("bar", [
+          d.libPubspec("foo", "1.0.0")
+        ]).create();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": {"path": "../bar"}})
+        ]).create();
+
+        pubGet();
+
+        createLockFile(appPath, sandbox: ["foo"]);
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
+      });
+
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
+    });
+
+    group("the pubspec has an incompatible version of a dependency", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": "2.0.0"})
+        ]).create();
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
+      });
+
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
+    });
+
+    group("the lockfile is pointing to an unavailable package with a newer "
+        "pubspec", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        schedule(() => deleteEntry(p.join(sandboxDir, cachePath)));
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.yaml");
+      });
+
+      _requiresPubGet('The pubspec.yaml file has changed since the '
+          'pubspec.lock file was generated, please run "pub get" again.');
+    });
+
+    group("the lockfile is pointing to an unavailable package with an older "
+        ".packages", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        schedule(() => deleteEntry(p.join(sandboxDir, cachePath)));
+
+        // Ensure that the lockfile looks newer than the .packages file.
+        _touch("pubspec.lock");
+      });
+
+      _requiresPubGet('The pubspec.lock file has changed since the .packages '
           'file was generated, please run "pub get" again.');
     });
 
-    group("the lockfile is newer than the package spec", () {
+    group("the lockfile has a package that the .packages file doesn't", () {
       setUp(() {
-        schedule(() => _touch("pubspec.lock"));
+        d.dir("foo", [
+          d.libPubspec("foo", "1.0.0")
+        ]).create();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": {"path": "../foo"}})
+        ]).create();
+
+        pubGet();
+
+        createPackagesFile(appPath);
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.lock");
       });
 
-      _forEveryCommand('The pubspec.lock file has changed since the .packages '
+      _requiresPubGet('The pubspec.lock file has changed since the .packages '
           'file was generated, please run "pub get" again.');
     });
+
+    group("the .packages file has a package with a non-file URI", () {
+      setUp(() {
+        d.dir("foo", [
+          d.libPubspec("foo", "1.0.0")
+        ]).create();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": {"path": "../foo"}})
+        ]).create();
+
+        pubGet();
+
+        d.dir(appPath, [
+          d.file(".packages", """
+myapp:lib
+foo:http://example.com/
+""")
+        ]).create();
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.lock");
+      });
+
+      _requiresPubGet('The pubspec.lock file has changed since the .packages '
+          'file was generated, please run "pub get" again.');
+    });
+
+    group("the .packages file points to the wrong place", () {
+      setUp(() {
+        d.dir("bar", [
+          d.libPubspec("foo", "1.0.0")
+        ]).create();
+
+        d.dir(appPath, [
+          d.appPubspec({"foo": {"path": "../bar"}})
+        ]).create();
+
+        pubGet();
+
+        createPackagesFile(appPath, sandbox: ["foo"]);
+
+        // Ensure that the pubspec looks newer than the lockfile.
+        _touch("pubspec.lock");
+      });
+
+      _requiresPubGet('The pubspec.lock file has changed since the .packages '
+          'file was generated, please run "pub get" again.');
+    });
+  });
+
+  group("doesn't require the user to run pub get first if", () {
+    group("the pubspec is older than the lockfile which is older than the "
+        "packages file, even if the contents are wrong", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        _touch("pubspec.lock");
+        _touch(".packages");
+      });
+
+      _runsSuccessfully(runDeps: false);
+    });
+
+    group("the pubspec is newer than the lockfile, but they're up-to-date", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        _touch("pubspec.yaml");
+      });
+
+      _runsSuccessfully();
+    });
+
+    group("the lockfile is newer than .packages, but they're up-to-date", () {
+      setUp(() {
+        d.dir(appPath, [
+          d.appPubspec({"foo": "1.0.0"})
+        ]).create();
+
+        pubGet();
+
+        _touch("pubspec.lock");
+      });
+
+      _runsSuccessfully();
+    });
   });
 }
 
 /// Runs every command that care about the world being up-to-date, and asserts
 /// that it prints [message] as part of its error.
-void _forEveryCommand(String message) {
+void _requiresPubGet(String message) {
   for (var command in ["build", "serve", "run", "deps"]) {
     integration("for pub $command", () {
       var args = [command];
       if (command == "run") args.add("script");
 
-      var output;
-      var error;
-      if (command == "list-package-dirs") {
-        output = contains(JSON.encode(message));
-      } else {
-        error = contains(message);
-      }
-
       schedulePub(
           args: args,
-          output: output,
-          error: error,
+          error: contains(message),
           exitCode: exit_codes.DATA);
     });
   }
 }
 
+/// Ensures that pub doesn't require "pub get" for the current package.
+///
+/// If [runDeps] is false, `pub deps` isn't included in the test. This is
+/// sometimes not desirable, since it uses slightly stronger checks for pubspec
+/// and lockfile consistency.
+void _runsSuccessfully({bool runDeps: true}) {
+  var commands = ["build", "serve", "run"];
+  if (runDeps) commands.add("deps");
+
+  for (var command in commands) {
+    integration("for pub $command", () {
+      var args = [command];
+      if (command == "run") args.add("bin/script.dart");
+      if (command == "serve") ;
+
+      if (command != "serve") {
+        schedulePub(args: args);
+      } else {
+        var pub = startPub(args: ["serve", "--port=0"]);
+        pub.stdout.expect(consumeThrough(startsWith("Serving myapp web")));
+        pub.kill();
+      }
+
+      schedule(() {
+        // If pub determines that everything is up-to-date, it should set the
+        // mtimes to indicate that.
+        var pubspecModified = new File(p.join(sandboxDir, "myapp/pubspec.yaml"))
+            .lastModifiedSync();
+        var lockFileModified =
+            new File(p.join(sandboxDir, "myapp/pubspec.lock"))
+                .lastModifiedSync();
+        var packagesModified = new File(p.join(sandboxDir, "myapp/.packages"))
+            .lastModifiedSync();
+
+        expect(!pubspecModified.isAfter(lockFileModified), isTrue);
+        expect(!lockFileModified.isAfter(packagesModified), isTrue);
+      }, "testing last-modified times");
+    });
+  }
+}
+
+/// Schedules a non-semantic modification to [path].
 void _touch(String path) {
-  path = p.join(sandboxDir, "myapp", path);
-  writeTextFile(path, readTextFile(path) + " ");
+  schedule(() 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 new Future.delayed(new Duration(seconds: 1));
+
+    path = p.join(sandboxDir, "myapp", path);
+    touch(path);
+  }, "touching $path");
 }
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 2c7af92411fa42e0a4b25171fb556c686c662b28..5427625d392a0e93cd87f3733f85bc27b840680b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -714,16 +714,35 @@ void makeGlobalPackage(String package, String version,
 /// hosted packages.
 void createLockFile(String package, {Iterable<String> sandbox,
     Iterable<String> pkg, Map<String, String> hosted}) {
-  var cache = new SystemCache.withSources(
-      rootDir: p.join(sandboxDir, cachePath));
+  schedule(() async {
+    var cache = new SystemCache.withSources(
+        rootDir: p.join(sandboxDir, cachePath));
 
-  var lockFile = _createLockFile(cache.sources,
-      sandbox: sandbox, pkg: pkg, hosted: hosted);
+    var lockFile = _createLockFile(cache.sources,
+        sandbox: sandbox, pkg: pkg, hosted: hosted);
 
-  d.dir(package, [
-    d.file('pubspec.lock', lockFile.serialize(null)),
-    d.file('.packages', lockFile.packagesFile(package))
-  ]).create();
+    await d.dir(package, [
+      d.file('pubspec.lock', lockFile.serialize(null)),
+      d.file('.packages', lockFile.packagesFile(package))
+    ]).create();
+  }, "creating lockfile for $package");
+}
+
+/// Like [createLockFile], but creates only a `.packages` file without a
+/// lockfile.
+void createPackagesFile(String package, {Iterable<String> sandbox,
+    Iterable<String> pkg, Map<String, String> hosted}) {
+  schedule(() async {
+    var cache = new SystemCache.withSources(
+        rootDir: p.join(sandboxDir, cachePath));
+
+    var lockFile = _createLockFile(cache.sources,
+        sandbox: sandbox, pkg: pkg, hosted: hosted);
+
+    await d.dir(package, [
+      d.file('.packages', lockFile.packagesFile(package))
+    ]).create();
+  }, "creating .packages for $package");
 }
 
 /// Creates a lock file for [package] without running `pub get`.