Finish override logic (#4780)

* Make applicationConfigHome conditional, avoid it when dart:io is unavailable

* Added override logic

* Better override logic

* Test that we can run pub inmemory

* Expose override IO logic

* Enable overriding and add tests

* Ignore harmless .tar.gz padding

* Fix golden stack trace

* Nit
diff --git a/lib/pub.dart b/lib/pub.dart
index 12f364c..321b905 100644
--- a/lib/pub.dart
+++ b/lib/pub.dart
@@ -2,11 +2,16 @@
 // 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' show StreamSink;
+
 import 'package:args/command_runner.dart';
+import 'package:file/file.dart' as f;
+import 'package:http/http.dart' as http;
 
 import 'src/entrypoint.dart';
 import 'src/exceptions.dart';
 import 'src/http.dart';
+import 'src/io.dart';
 import 'src/pub_embeddable_command.dart';
 import 'src/source/git.dart';
 import 'src/system_cache.dart';
@@ -26,7 +31,33 @@
 Command<int> pubCommand({
   required bool Function() isVerbose,
   String category = '',
-}) => PubEmbeddableCommand(isVerbose, category);
+  f.FileSystem? fileSystem,
+  Map<String, String>? environment,
+  String? platformVersion,
+  Stream<List<int>>? stdin,
+  StreamSink<List<int>>? stdout,
+  StreamSink<List<int>>? stderr,
+  http.Client? httpClient,
+}) => withOverrides(
+  () => PubEmbeddableCommand(
+    isVerbose,
+    category,
+    fileSystem: fileSystem,
+    environment: environment,
+    platformVersion: platformVersion,
+    stdin: stdin,
+    stdout: stdout,
+    stderr: stderr,
+    httpClient: httpClient,
+  ),
+  fileSystem: fileSystem,
+  environment: environment,
+  platformVersion: platformVersion,
+  stdin: stdin,
+  stdout: stdout,
+  stderr: stderr,
+  httpClient: httpClient,
+);
 
 /// Makes sure that [dir]/pubspec.yaml is resolved such that pubspec.lock and
 /// .dart_tool/package_config.json are up-to-date and all packages are
@@ -46,22 +77,40 @@
   bool isOffline = false,
   bool summaryOnly = true,
   bool onlyOutputWhenTerminal = true,
+  f.FileSystem? fileSystem,
+  Map<String, String>? environment,
+  String? platformVersion,
+  Stream<List<int>>? stdin,
+  StreamSink<List<int>>? stdout,
+  StreamSink<List<int>>? stderr,
+  http.Client? httpClient,
 }) async {
-  try {
-    await Entrypoint.ensureUpToDate(
-      dir,
-      cache: SystemCache(isOffline: isOffline),
-      summaryOnly: summaryOnly,
-      onlyOutputWhenTerminal: onlyOutputWhenTerminal,
-    );
-  } on ApplicationException catch (e) {
-    throw ResolutionFailedException._(e.toString());
-  } finally {
-    // TODO(https://github.com/dart-lang/pub/issues/4200)
-    // This is a bit of a hack.
-    // We should most likely take a client here.
-    globalHttpClient.close();
-  }
+  return await withOverrides(
+    () async {
+      try {
+        await Entrypoint.ensureUpToDate(
+          dir,
+          cache: SystemCache(isOffline: isOffline),
+          summaryOnly: summaryOnly,
+          onlyOutputWhenTerminal: onlyOutputWhenTerminal,
+        );
+      } on ApplicationException catch (e) {
+        throw ResolutionFailedException._(e.toString());
+      } finally {
+        // TODO(https://github.com/dart-lang/pub/issues/4200)
+        // This is a bit of a hack.
+        // We should most likely take a client here.
+        globalHttpClient.close();
+      }
+    },
+    fileSystem: fileSystem,
+    environment: environment,
+    platformVersion: platformVersion,
+    stdin: stdin,
+    stdout: stdout,
+    stderr: stderr,
+    httpClient: httpClient,
+  );
 }
 
 class ResolutionFailedException implements Exception {
diff --git a/lib/src/authentication/client.dart b/lib/src/authentication/client.dart
index 1a8823b..40e7cd9 100644
--- a/lib/src/authentication/client.dart
+++ b/lib/src/authentication/client.dart
@@ -26,7 +26,7 @@
   /// not be injected to requests.
   _AuthenticatedClient(this._inner, this._credential);
 
-  final http.BaseClient _inner;
+  final http.Client _inner;
 
   /// Authentication scheme that could be used for authenticating requests.
   final Credential? _credential;
diff --git a/lib/src/command.dart b/lib/src/command.dart
index d1a9d0b..8932723 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -18,6 +18,7 @@
 import 'git.dart' as git;
 import 'global_packages.dart';
 import 'http.dart';
+import 'io.dart';
 import 'log.dart' as log;
 import 'path.dart';
 import 'platform_info.dart';
@@ -186,7 +187,20 @@
 
   @override
   @nonVirtual
-  FutureOr<int> run() async {
+  Future<int> run() async {
+    return await withOverrides(
+      _run,
+      fileSystem: _pubEmbeddableCommand?.fileSystem,
+      environment: _pubEmbeddableCommand?.environment,
+      platformVersion: _pubEmbeddableCommand?.platformVersion,
+      stdin: _pubEmbeddableCommand?.stdin,
+      stdout: _pubEmbeddableCommand?.stdout,
+      stderr: _pubEmbeddableCommand?.stderr,
+      httpClient: _pubEmbeddableCommand?.httpClient,
+    );
+  }
+
+  Future<int> _run() async {
     _computeCommand(_pubTopLevel.argResults);
     _decideOnColors(_pubTopLevel.argResults);
 
diff --git a/lib/src/gzip/gzip.dart b/lib/src/gzip/gzip.dart
index 74f2b9b..6b1bc91 100644
--- a/lib/src/gzip/gzip.dart
+++ b/lib/src/gzip/gzip.dart
@@ -6,7 +6,7 @@
 
 import 'gzip_stub.dart'
     if (dart.library.io) 'gzip_io.dart'
-    if (dart.library.js_util) 'gzip_js.dart'
+    if (dart.library.js_interop) 'gzip_js.dart'
     as impl;
 
 /// A [Converter] that decompresses gzip-compressed data.
diff --git a/lib/src/gzip/gzip_js.dart b/lib/src/gzip/gzip_js.dart
index 1a3cb46..c00c01a 100644
--- a/lib/src/gzip/gzip_js.dart
+++ b/lib/src/gzip/gzip_js.dart
@@ -33,39 +33,60 @@
     return controller.stream;
   }
 
+  bool _isHarmlessPaddingError(Object e) {
+    // Some .tar.gz may contain harmless padding, which DecrompressionStream in
+    // the browsers are sensitive to. Example:
+    // https://pub.dev/api/archives/lints-1.0.1.tar.gz
+    final errorMessage = e.toString().toLowerCase();
+    // Match the specific EOF padding errors from V8, SpiderMonkey, and WebKit
+    return errorMessage.contains('junk found') ||
+        errorMessage.contains('unexpected input') ||
+        errorMessage.contains('extra bytes');
+  }
+
   Future<void> _pipe(
     Stream<List<int>> stream,
     StreamController<List<int>> controller,
   ) async {
-    try {
-      final decompressionStream = web.DecompressionStream('gzip');
-      final writer = decompressionStream.writable.getWriter();
-      final reader =
-          decompressionStream.readable.getReader()
-              as web.ReadableStreamDefaultReader;
+    final decompressionStream = web.DecompressionStream('gzip');
+    final writer = decompressionStream.writable.getWriter();
+    final reader =
+        decompressionStream.readable.getReader()
+            as web.ReadableStreamDefaultReader;
 
-      final readFuture = () async {
-        try {
-          while (true) {
-            final result = await reader.read().toDart;
-            if (result.done) break;
-            final value = result.value as JSUint8Array;
-            controller.add(value.toDart);
-          }
-        } finally {
-          reader.releaseLock();
+    final readFuture = () async {
+      try {
+        while (true) {
+          final result = await reader.read().toDart;
+          if (result.done) break;
+          final value = result.value as JSUint8Array;
+          controller.add(value.toDart);
         }
-      }();
+      } catch (e, st) {
+        if (_isHarmlessPaddingError(e)) {
+          // Ignore trailing junk
+          return;
+        }
+        controller.addError(e, st);
+      } finally {
+        reader.releaseLock();
+      }
+    }();
 
+    try {
       await for (final chunk in stream) {
         final bytes = chunk is Uint8List ? chunk : Uint8List.fromList(chunk);
         await writer.write(bytes.toJS).toDart;
       }
       await writer.close().toDart;
-      await readFuture;
-      await controller.close();
     } catch (e, st) {
-      controller.addError(e, st);
+      if (_isHarmlessPaddingError(e)) {
+        // Ignore trailing junk
+      } else {
+        controller.addError(e, st);
+      }
+    } finally {
+      await readFuture;
       await controller.close();
     }
   }
diff --git a/lib/src/http.dart b/lib/src/http.dart
index a5b461a..e954836 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -34,14 +34,14 @@
 class _PubHttpClient extends http.BaseClient {
   final _requestStopwatches = <http.BaseRequest, Stopwatch>{};
 
-  http.Client _inner;
+  final http.Client _inner;
 
   /// We manually keep track of whether the client was closed,
   /// indicating that no more networking should be done. (And thus we don't need
   /// to retry failed requests).
   bool _wasClosed = false;
 
-  _PubHttpClient([http.Client? inner]) : _inner = inner ?? http.Client();
+  _PubHttpClient(this._inner);
 
   @override
   Future<http.StreamedResponse> send(http.BaseRequest request) async {
@@ -156,16 +156,22 @@
   }
 }
 
-/// The [_PubHttpClient] wrapped by [globalHttpClient].
-final _pubClient = _PubHttpClient();
+final _defaultGlobalHttpClient = _PubHttpClient(http.Client());
 
 /// The HTTP client to use for all HTTP requests.
-final globalHttpClient = _pubClient;
+http.Client get globalHttpClient =>
+    Zone.current[_globalHttpClientKey] as http.Client? ??
+    _defaultGlobalHttpClient;
 
-/// The underlying HTTP client wrapped by [globalHttpClient].
-/// This enables the ability to use a mock client in tests.
-http.Client get innerHttpClient => _pubClient._inner;
-set innerHttpClient(http.Client client) => _pubClient._inner = client;
+/// The key for the [globalHttpClient] in the current [Zone].
+final _globalHttpClientKey = Object();
+
+/// Runs [callback] in a [Zone] where [globalHttpClient] wraps [client].
+R withHttpClient<R>(R Function() callback, {required http.Client client}) =>
+    runZoned(
+      callback,
+      zoneValues: {_globalHttpClientKey: _PubHttpClient(client)},
+    );
 
 extension AttachHeaders on http.Request {
   /// Adds headers required for pub.dev API requests.
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 13462be..367e350 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -8,6 +8,7 @@
 import 'dart:async';
 import 'dart:collection';
 import 'dart:convert';
+import 'dart:io' as io;
 import 'dart:io';
 import 'dart:typed_data';
 
@@ -15,7 +16,10 @@
 import 'package:cli_util/cli_util.dart'
     show EnvironmentNotFoundException, applicationConfigHome;
 import 'package:collection/collection.dart';
+import 'package:file/file.dart' as f;
+import 'package:file/local.dart' as f;
 import 'package:http/http.dart' show ByteStream;
+import 'package:http/http.dart' as http;
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:meta/meta.dart';
 import 'package:pool/pool.dart';
@@ -26,6 +30,7 @@
 import 'exceptions.dart';
 import 'exit_codes.dart' as exit_codes;
 import 'gzip/gzip.dart';
+import 'http.dart';
 import 'log.dart' as log;
 import 'path.dart';
 import 'platform_info.dart';
@@ -1392,3 +1397,251 @@
     d = parent;
   }
 }
+
+/// Run [fn] in a zone with overrides.
+R withOverrides<R>(
+  R Function() fn, {
+  f.FileSystem? fileSystem,
+  Map<String, String>? environment,
+  String? platformVersion,
+  Stream<List<int>>? stdin,
+  StreamSink<List<int>>? stdout,
+  StreamSink<List<int>>? stderr,
+  http.Client? httpClient,
+}) {
+  // If there are no overrides we're done
+  if (fileSystem == null &&
+      environment == null &&
+      platformVersion == null &&
+      stdin == null &&
+      stdout == null &&
+      stderr == null &&
+      httpClient == null) {
+    return fn();
+  }
+
+  fileSystem ??= const f.LocalFileSystem();
+  environment ??= platform.environment;
+  platformVersion ??= platform.version;
+  stdin ??= io.stdin;
+  stdout ??= io.stdout;
+  stderr ??= io.stderr;
+  final client = httpClient ?? http.Client();
+
+  final pathContext = fileSystem.path;
+
+  return IOOverrides.runWithIOOverrides(
+    () {
+      return withPlatform(
+        () {
+          return withHttpClient(() {
+            return withPathContext(fn, pathContext: pathContext);
+          }, client: client);
+        },
+        platform: PlatformInfo.override(
+          environment: environment,
+          version: platformVersion,
+          pathSeparator: pathContext.separator,
+        ),
+      );
+    },
+    _IOOverrides(
+      fileSystem: fileSystem,
+      stdin: stdin is Stdin ? stdin : StdinStream(stdin),
+      stdout: stdout is Stdout ? stdout : StdoutSink(stdout),
+      stderr: stderr is Stdout ? stderr : StdoutSink(stderr),
+    ),
+  );
+}
+
+/// An [IOOverrides] that uses a [f.FileSystem] for all operations.
+final class _IOOverrides extends IOOverrides {
+  final f.FileSystem fileSystem;
+
+  @override
+  final Stdin stdin;
+
+  @override
+  final Stdout stdout;
+
+  @override
+  final Stdout stderr;
+
+  _IOOverrides({
+    required this.fileSystem,
+    required this.stdin,
+    required this.stdout,
+    required this.stderr,
+  });
+
+  @override
+  File createFile(String path) => fileSystem.file(path);
+
+  @override
+  Directory createDirectory(String path) => fileSystem.directory(path);
+
+  @override
+  Link createLink(String path) => fileSystem.link(path);
+
+  @override
+  Future<FileStat> stat(String path) => fileSystem.stat(path);
+
+  @override
+  FileStat statSync(String path) => fileSystem.statSync(path);
+
+  @override
+  Future<bool> fseIdentical(String path1, String path2) =>
+      fileSystem.identical(path1, path2);
+
+  @override
+  bool fseIdenticalSync(String path1, String path2) =>
+      fileSystem.identicalSync(path1, path2);
+
+  @override
+  Future<FileSystemEntityType> fseGetType(String path, bool followLinks) =>
+      fileSystem.type(path, followLinks: followLinks);
+
+  @override
+  FileSystemEntityType fseGetTypeSync(String path, bool followLinks) =>
+      fileSystem.typeSync(path, followLinks: followLinks);
+
+  @override
+  Directory getCurrentDirectory() => fileSystem.currentDirectory;
+
+  @override
+  void setCurrentDirectory(String path) {
+    fileSystem.currentDirectory = path;
+  }
+
+  @override
+  Directory getSystemTempDirectory() => fileSystem.systemTempDirectory;
+
+  @override
+  bool fsWatchIsSupported() => fileSystem.isWatchSupported;
+
+  @override
+  Stream<FileSystemEvent> fsWatch(String path, int events, bool recursive) =>
+      fileSystem.directory(path).watch(events: events, recursive: recursive);
+}
+
+/// Wrap a [Stream<List<int>>] as [Stdin].
+final class StdinStream extends StreamView<List<int>> implements Stdin {
+  StdinStream(super.stream);
+
+  @override
+  bool get echoMode => false;
+
+  @override
+  set echoMode(bool value) {}
+
+  @override
+  bool get echoNewlineMode => false;
+
+  @override
+  set echoNewlineMode(bool value) {}
+
+  @override
+  bool get lineMode => false;
+
+  @override
+  set lineMode(bool value) {}
+
+  @override
+  bool get hasTerminal => false;
+
+  @override
+  int readByteSync() => throw UnsupportedError('cannot read sync from stdin');
+
+  @override
+  String? readLineSync({
+    Encoding encoding = systemEncoding,
+    bool retainNewlines = false,
+  }) => throw UnsupportedError('cannot read sync from stdin');
+
+  @override
+  bool get supportsAnsiEscapes => false;
+}
+
+/// Wrap a [StreamSink<List<int>>] as [Stdout].
+final class StdoutSink implements Stdout {
+  final StreamSink<List<int>> _sink;
+
+  StdoutSink(this._sink);
+
+  @override
+  Encoding encoding = utf8;
+
+  @override
+  void add(List<int> data) {
+    _sink.add(data);
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    _sink.addError(error, stackTrace);
+  }
+
+  @override
+  Future addStream(Stream<List<int>> stream) => _sink.addStream(stream);
+
+  @override
+  Future close() => _sink.close();
+
+  @override
+  Future get done => _sink.done;
+
+  @override
+  Future flush() async {}
+
+  @override
+  bool get hasTerminal => false;
+
+  @override
+  IOSink get nonBlocking => this;
+
+  @override
+  bool get supportsAnsiEscapes => false;
+
+  @override
+  int get terminalColumns =>
+      throw const StdoutException('no terminal attached');
+
+  @override
+  int get terminalLines => throw const StdoutException('no terminal attached');
+
+  @override
+  void write(Object? object) {
+    add(encoding.encode('$object'));
+  }
+
+  @override
+  void writeAll(Iterable objects, [String separator = '']) {
+    final iterator = objects.iterator;
+    if (!iterator.moveNext()) return;
+    if (separator.isEmpty) {
+      do {
+        write(iterator.current);
+      } while (iterator.moveNext());
+    } else {
+      write(iterator.current);
+      while (iterator.moveNext()) {
+        write(separator);
+        write(iterator.current);
+      }
+    }
+  }
+
+  @override
+  void writeCharCode(int charCode) {
+    write(String.fromCharCode(charCode));
+  }
+
+  @override
+  void writeln([Object? object = '']) {
+    write(object);
+    write(lineTerminator);
+  }
+
+  @override
+  String lineTerminator = '\n';
+}
diff --git a/lib/src/path.dart b/lib/src/path.dart
index f32135f..068297d 100644
--- a/lib/src/path.dart
+++ b/lib/src/path.dart
@@ -14,14 +14,10 @@
 final _pathContextKey = Object();
 
 /// Runs [callback] in a [Zone] where [p] is overridden by [pathContext].
-Future<T> withPathContext<T>(
-  FutureOr<T> Function() callback, {
+R withPathContext<R>(
+  R Function() callback, {
   required path.Context pathContext,
-}) {
-  return runZoned(() async {
-    return await callback();
-  }, zoneValues: {_pathContextKey: pathContext});
-}
+}) => runZoned(callback, zoneValues: {_pathContextKey: pathContext});
 
 extension PathContextExt on path.Context {
   /// A default context for manipulating POSIX paths.
diff --git a/lib/src/platform_info.dart b/lib/src/platform_info.dart
index 0a75feb..d37cfad 100644
--- a/lib/src/platform_info.dart
+++ b/lib/src/platform_info.dart
@@ -8,43 +8,56 @@
 /// A proxy for [Platform] from `dart:io` which can be overridden.
 PlatformInfo get platform =>
     Zone.current[_platformInfoKey] as PlatformInfo? ??
-    PlatformInfo.nativePlatform();
+    PlatformInfo.defaultPlatform();
 
 /// The key for the [platform] in the current [Zone].
 final _platformInfoKey = Object();
 
 /// Runs [callback] in a [Zone] where `platform` is overridden by [platform].
-Future<T> withPlatform<T>(
-  FutureOr<T> Function() callback, {
-  required PlatformInfo platform,
-}) {
-  return runZoned(() async {
-    return await callback();
-  }, zoneValues: {_platformInfoKey: platform});
-}
+R withPlatform<R>(R Function() callback, {required PlatformInfo platform}) =>
+    runZoned(callback, zoneValues: {_platformInfoKey: platform});
 
 abstract final class PlatformInfo {
   const PlatformInfo._();
 
-  factory PlatformInfo.nativePlatform() = _NativePlatformInfo;
+  factory PlatformInfo.defaultPlatform() =>
+      const bool.fromEnvironment('dart.library.io')
+          ? const _NativePlatformInfo()
+          : const _BrowserPlatformInfo();
 
   factory PlatformInfo.override({
-    required Map<String, String> environment,
-    required String executable,
-    required bool isAndroid,
-    required bool isFuchsia,
-    required bool isIOS,
-    required bool isLinux,
-    required bool isMacOS,
-    required bool isWindows,
-    required String lineTerminator,
-    required String operatingSystem,
-    required String pathSeparator,
-    required String resolvedExecutable,
-    required String version,
-    required int numberOfProcessors,
-    required Uri script,
-  }) = _PlatformInfoOverride;
+    Map<String, String>? environment,
+    String? executable,
+    bool? isAndroid,
+    bool? isFuchsia,
+    bool? isIOS,
+    bool? isLinux,
+    bool? isMacOS,
+    bool? isWindows,
+    String? lineTerminator,
+    String? operatingSystem,
+    String? pathSeparator,
+    String? resolvedExecutable,
+    String? version,
+    int? numberOfProcessors,
+    Uri? script,
+  }) => _PlatformInfoOverride(
+    environment: environment ?? platform.environment,
+    executable: executable ?? platform.executable,
+    isAndroid: isAndroid ?? platform.isAndroid,
+    isFuchsia: isFuchsia ?? platform.isFuchsia,
+    isIOS: isIOS ?? platform.isIOS,
+    isLinux: isLinux ?? platform.isLinux,
+    isMacOS: isMacOS ?? platform.isMacOS,
+    isWindows: isWindows ?? platform.isWindows,
+    lineTerminator: lineTerminator ?? platform.lineTerminator,
+    operatingSystem: operatingSystem ?? platform.operatingSystem,
+    pathSeparator: pathSeparator ?? platform.pathSeparator,
+    resolvedExecutable: resolvedExecutable ?? platform.resolvedExecutable,
+    version: version ?? platform.version,
+    numberOfProcessors: numberOfProcessors ?? platform.numberOfProcessors,
+    script: script ?? platform.script,
+  );
 
   /// Returns [Platform.environment].
   Map<String, String> get environment;
@@ -141,6 +154,55 @@
   Uri get script => Platform.script;
 }
 
+final class _BrowserPlatformInfo extends PlatformInfo {
+  const _BrowserPlatformInfo() : super._();
+
+  @override
+  Map<String, String> get environment => const {};
+
+  @override
+  String get executable => '';
+
+  @override
+  bool get isAndroid => false;
+
+  @override
+  bool get isFuchsia => false;
+
+  @override
+  bool get isIOS => false;
+
+  @override
+  bool get isLinux => false;
+
+  @override
+  bool get isMacOS => false;
+
+  @override
+  bool get isWindows => false;
+
+  @override
+  String get lineTerminator => '\n';
+
+  @override
+  String get operatingSystem => '';
+
+  @override
+  String get pathSeparator => '/';
+
+  @override
+  String get resolvedExecutable => '';
+
+  @override
+  String get version => '';
+
+  @override
+  int get numberOfProcessors => 1;
+
+  @override
+  Uri get script => Uri();
+}
+
 final class _PlatformInfoOverride extends PlatformInfo {
   const _PlatformInfoOverride({
     required this.environment,
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 0ed7723..854af65 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -2,6 +2,11 @@
 // 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 'package:file/file.dart' as f;
+import 'package:http/http.dart' as http;
+
 import 'command.dart' show PubCommand, PubTopLevel;
 import 'command.dart';
 import 'command/add.dart';
@@ -49,7 +54,25 @@
   @override
   final String category;
 
-  PubEmbeddableCommand(this.isVerbose, this.category) : super() {
+  final f.FileSystem? fileSystem;
+  final Map<String, String>? environment;
+  final String? platformVersion;
+  final Stream<List<int>>? stdin;
+  final StreamSink<List<int>>? stdout;
+  final StreamSink<List<int>>? stderr;
+  final http.Client? httpClient;
+
+  PubEmbeddableCommand(
+    this.isVerbose,
+    this.category, {
+    this.fileSystem,
+    this.environment,
+    this.platformVersion,
+    this.stdin,
+    this.stdout,
+    this.stderr,
+    this.httpClient,
+  }) : super() {
     // This flag was never honored in the embedding but since it was accepted we
     // leave it as a hidden flag to avoid breaking clients that pass it.
     argParser.addFlag('trace', hide: true);
diff --git a/pubspec.lock b/pubspec.lock
index 4d9ed0f..cdb25e8 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -106,7 +106,7 @@
     source: hosted
     version: "3.5.2"
   file:
-    dependency: "direct dev"
+    dependency: "direct main"
     description:
       name: file
       sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
diff --git a/pubspec.yaml b/pubspec.yaml
index 54973d2..a7e35b3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -11,6 +11,7 @@
   collection: ^1.19.1
   convert: ^3.1.2
   crypto: ^3.0.7
+  file: ^7.0.1
   frontend_server_client: ^4.0.0
   glob: ^2.1.3
   graphs: ^2.3.2
@@ -33,7 +34,6 @@
 dev_dependencies:
   checks: ^0.3.1
   dart_flutter_team_lints: ^3.5.2
-  file: ^7.0.1
   shelf_test_handler: ^2.0.2
   test: ^1.26.3
   test_descriptor: ^2.0.2
diff --git a/test/overrides/bytesink.dart b/test/overrides/bytesink.dart
new file mode 100644
index 0000000..fbcf105
--- /dev/null
+++ b/test/overrides/bytesink.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2026, 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 'dart:typed_data';
+
+final class ByteSink implements StreamSink<List<int>> {
+  final builder = BytesBuilder();
+  final _completer = Completer<void>();
+
+  /// Access the buffered bytes as a Uint8List
+  Uint8List get bytes => builder.toBytes();
+
+  @override
+  void add(List<int> data) {
+    builder.add(data);
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    if (!_completer.isCompleted) {
+      _completer.completeError(error, stackTrace);
+    }
+  }
+
+  @override
+  Future<void> addStream(Stream<List<int>> stream) async {
+    await stream.forEach(add);
+  }
+
+  @override
+  Future<void> close() async {
+    if (!_completer.isCompleted) {
+      _completer.complete();
+    }
+  }
+
+  @override
+  Future<void> get done => _completer.future;
+}
diff --git a/test/overrides/ensure_resolved_test.dart b/test/overrides/ensure_resolved_test.dart
new file mode 100644
index 0000000..1ed0fa1
--- /dev/null
+++ b/test/overrides/ensure_resolved_test.dart
@@ -0,0 +1,57 @@
+// Copyright (c) 2026, 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.
+
+@TestOn('vm || browser')
+library;
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+
+import 'package:file/memory.dart';
+import 'package:pub/pub.dart';
+import 'package:test/test.dart';
+
+import 'bytesink.dart';
+
+void main() {
+  test('ensurePubspecResolved in memory', () async {
+    final fs = MemoryFileSystem();
+
+    fs.directory('/workspace').createSync();
+    fs.currentDirectory = '/workspace';
+    fs.directory('/sdk/bin').createSync(recursive: true);
+
+    final bs = ByteSink();
+
+    final pubspec = fs.file('/workspace/pubspec.yaml');
+    await pubspec.writeAsString('''
+name: my_app
+version: 1.0.0
+environment:
+  sdk: ^3.0.0
+dependencies:
+  retry:
+''');
+
+    await ensurePubspecResolved(
+      '/workspace',
+      summaryOnly: false,
+      onlyOutputWhenTerminal: false,
+
+      fileSystem: fs,
+      stdout: bs,
+      stderr: bs,
+      stdin: const Stream.empty(),
+      platformVersion: '3.11.0',
+      environment: {'PUB_CACHE': '/tmp/pub_cache', 'DART_ROOT': '/sdk'},
+    );
+
+    expect(
+      fs.file('/workspace/.dart_tool/package_config.json').existsSync(),
+      isTrue,
+    );
+
+    expect(utf8.decode(bs.bytes), contains('Changed 1 dependency'));
+  });
+}
diff --git a/test/overrides/pubcommand_test.dart b/test/overrides/pubcommand_test.dart
new file mode 100644
index 0000000..49fa711
--- /dev/null
+++ b/test/overrides/pubcommand_test.dart
@@ -0,0 +1,106 @@
+// Copyright (c) 2026, 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.
+
+@TestOn('vm || browser')
+library;
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+
+import 'package:args/command_runner.dart';
+import 'package:file/file.dart' as f;
+import 'package:file/memory.dart';
+import 'package:http/http.dart' as http;
+import 'package:pub/pub.dart';
+import 'package:test/test.dart';
+
+import 'bytesink.dart';
+
+void main() {
+  test('pubCommand() in memory', () async {
+    final fs = MemoryFileSystem();
+
+    fs.directory('/workspace').createSync();
+    fs.currentDirectory = '/workspace';
+    fs.directory('/sdk/bin').createSync(recursive: true);
+
+    final bs = ByteSink();
+
+    Future<int?> pub(List<String> args) async {
+      bs.add(utf8.encode('\$ dart pub ${args.join(' ')}\n'));
+      return await Runner(
+        fileSystem: fs,
+        stdout: bs,
+        stderr: bs,
+        stdin: const Stream.empty(),
+        platformVersion: '3.11.0',
+        environment: {'PUB_CACHE': '/tmp/pub_cache', 'DART_ROOT': '/sdk'},
+        httpClient: http.Client(),
+      ).run(['pub', ...args]);
+    }
+
+    final pubspec = fs.file('/workspace/pubspec.yaml');
+    await pubspec.writeAsString('''
+name: my_app
+version: 1.0.0
+environment:
+  sdk: ^3.0.0
+''');
+
+    try {
+      final exitCode = await pub(['get']);
+      expect(exitCode, 0);
+
+      expect(
+        fs.file('/workspace/.dart_tool/package_config.json').existsSync(),
+        isTrue,
+      );
+
+      expect(await pub(['add', 'retry']), 0);
+      expect(await pub(['downgrade']), 0);
+      expect(await pub(['outdated']), 0);
+      expect(await pub(['upgrade']), 0);
+      expect(await pub(['remove', 'retry']), 0);
+      expect(await pub(['unpack', 'retry']), 0);
+    } catch (_) {
+      printOnFailure(utf8.decode(bs.bytes));
+      rethrow;
+    }
+
+    expect(utf8.decode(bs.bytes), contains('Changed 1 dependency'));
+  });
+}
+
+class Runner extends CommandRunner<int> {
+  final f.FileSystem fileSystem;
+  final Map<String, String> environment;
+  final String platformVersion;
+  final Stream<List<int>> stdin;
+  final StreamSink<List<int>> stdout;
+  final StreamSink<List<int>> stderr;
+  final http.Client httpClient;
+
+  Runner({
+    required this.fileSystem,
+    required this.environment,
+    required this.platformVersion,
+    required this.stdin,
+    required this.stdout,
+    required this.stderr,
+    required this.httpClient,
+  }) : super('dart', 'dart pub emulator') {
+    addCommand(
+      pubCommand(
+        isVerbose: () => false,
+        fileSystem: fileSystem,
+        environment: environment,
+        platformVersion: platformVersion,
+        stdin: stdin,
+        stdout: stdout,
+        stderr: stderr,
+        httpClient: httpClient,
+      ),
+    );
+  }
+}
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 7698901..a4adec3 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -17,11 +17,9 @@
 import 'dart:typed_data';
 
 import 'package:async/async.dart';
-import 'package:http/testing.dart';
 import 'package:pub/src/entrypoint.dart';
 import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:pub/src/git.dart' as git;
-import 'package:pub/src/http.dart';
 import 'package:pub/src/io.dart';
 import 'package:pub/src/lock_file.dart';
 import 'package:pub/src/log.dart' as log;
@@ -754,18 +752,6 @@
   return LockFile(packages);
 }
 
-/// Uses [client] as the mock HTTP client for this test.
-///
-/// Note that this will only affect HTTP requests made via http.dart in the
-/// parent process.
-void useMockClient(MockClient client) {
-  final oldInnerClient = innerHttpClient;
-  innerHttpClient = client;
-  addTearDown(() {
-    innerHttpClient = oldInnerClient;
-  });
-}
-
 /// Describes a map representing a library package with the given [name],
 /// [version], and [dependencies].
 Map<String, Object> packageMap(
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index 9615a0f..cf58928 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -327,11 +327,13 @@
 $ tool/test-bin/pub_command_runner.dart pub fail
 [E] Bad state: Pub has crashed
 [E]  tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected
-[E] package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn>
+[E] package:pub/src/command.dart $LINE:$COL PubCommand._run.<fn>
 [E] dart:async   new Future.sync
 [E] package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback
 [E] dart:async   runZonedGuarded
 [E] package:pub/src/utils.dart $LINE:$COL captureErrors
+[E] package:pub/src/command.dart $LINE:$COL PubCommand._run
+[E] package:pub/src/io.dart $LINE:$COL withOverrides
 [E] package:pub/src/command.dart $LINE:$COL PubCommand.run
 [E] package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand
 [E]  tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand
@@ -368,11 +370,13 @@
 ERR : Bad state: Pub has crashed
 FINE: Exception type: StateError
 ERR : tool/test-bin/pub_command_runner.dart $LINE:$COL ThrowingCommand.runProtected
-   | package:pub/src/command.dart $LINE:$COL PubCommand.run.<fn>
+   | package:pub/src/command.dart $LINE:$COL PubCommand._run.<fn>
    | dart:async   new Future.sync
    | package:pub/src/utils.dart $LINE:$COL captureErrors.wrappedCallback
    | dart:async   runZonedGuarded
    | package:pub/src/utils.dart $LINE:$COL captureErrors
+   | package:pub/src/command.dart $LINE:$COL PubCommand._run
+   | package:pub/src/io.dart $LINE:$COL withOverrides
    | package:pub/src/command.dart $LINE:$COL PubCommand.run
    | package:args/command_runner.dart $LINE:$COL CommandRunner.runCommand
    | tool/test-bin/pub_command_runner.dart $LINE:$COL Runner.runCommand
diff --git a/test/wasm/iooverrides.dart b/test/wasm/iooverrides.dart
deleted file mode 100644
index 4532703..0000000
--- a/test/wasm/iooverrides.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-import 'dart:io';
-
-import 'package:file/file.dart' as f;
-
-/// Creates an [IOOverrides] that uses [fs] for all operations.
-IOOverrides createFileSystemIOOverrides(f.FileSystem fs) =>
-    _FileSystemIOOverrides(fs);
-
-/// An [IOOverrides] that uses a [f.FileSystem] for all operations.
-final class _FileSystemIOOverrides extends IOOverrides {
-  final f.FileSystem _fs;
-
-  _FileSystemIOOverrides(this._fs);
-
-  @override
-  File createFile(String path) => _fs.file(path);
-
-  @override
-  Directory createDirectory(String path) => _fs.directory(path);
-
-  @override
-  Link createLink(String path) => _fs.link(path);
-
-  @override
-  Future<FileStat> stat(String path) => _fs.stat(path);
-
-  @override
-  FileStat statSync(String path) => _fs.statSync(path);
-
-  @override
-  Future<bool> fseIdentical(String path1, String path2) =>
-      _fs.identical(path1, path2);
-
-  @override
-  bool fseIdenticalSync(String path1, String path2) =>
-      _fs.identicalSync(path1, path2);
-
-  @override
-  Future<FileSystemEntityType> fseGetType(String path, bool followLinks) =>
-      _fs.type(path, followLinks: followLinks);
-
-  @override
-  FileSystemEntityType fseGetTypeSync(String path, bool followLinks) =>
-      _fs.typeSync(path, followLinks: followLinks);
-
-  @override
-  Directory getCurrentDirectory() => _fs.currentDirectory;
-
-  @override
-  void setCurrentDirectory(String path) {
-    _fs.currentDirectory = path;
-  }
-
-  @override
-  Directory getSystemTempDirectory() => _fs.systemTempDirectory;
-}
diff --git a/test/wasm/iooverrides_test.dart b/test/wasm/iooverrides_test.dart
deleted file mode 100644
index 0986462..0000000
--- a/test/wasm/iooverrides_test.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-import 'dart:io';
-
-import 'package:file/memory.dart';
-import 'package:test/test.dart';
-
-import 'iooverrides.dart';
-
-void main() {
-  test('FileSystemIOOverrides redirects IO operations', () async {
-    final fs = MemoryFileSystem();
-    await IOOverrides.runWithIOOverrides(() async {
-      final file = File('/test.txt');
-      await file.writeAsString('hello');
-
-      expect(fs.file('/test.txt').readAsStringSync(), 'hello');
-      expect(await file.readAsString(), 'hello');
-
-      final dir = Directory('/subdir');
-      await dir.create();
-      expect(fs.directory('/subdir').existsSync(), isTrue);
-
-      final list = await Directory('/').list().toList();
-      expect(list.map((e) => e.path), containsAll(['test.txt', 'subdir']));
-    }, createFileSystemIOOverrides(fs));
-  });
-
-  test('getCurrentDirectory and setCurrentDirectory', () async {
-    final fs = MemoryFileSystem();
-    fs.directory('/a/b').createSync(recursive: true);
-    await IOOverrides.runWithIOOverrides(() async {
-      expect(Directory.current.path, '/');
-      Directory.current = '/a/b';
-      expect(Directory.current.path, '/a/b');
-      expect(fs.currentDirectory.path, '/a/b');
-    }, createFileSystemIOOverrides(fs));
-  });
-
-  test('systemTempDirectory', () async {
-    final fs = MemoryFileSystem();
-    await IOOverrides.runWithIOOverrides(() async {
-      expect(Directory.systemTemp.path, fs.systemTempDirectory.path);
-    }, createFileSystemIOOverrides(fs));
-  });
-}