Use dev tools URL when debugging (#2283)

Towards #2185

The Observatory UI is no longer served and the current URL does not
work. Dev Tools does not support deep linking to a particular isolate or
library, so link to the overall devtools app for now.
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index 0cfc58a..b8e9d2d 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -11,6 +11,7 @@
   all tests with OS `'windows'` would previously still run browser tests on
   windows, but will now skip all tests including browser tests.
 * Treat calls to `exit` as test failures in VM tests.
+* Use a DevTools URL instead of a defunct observatory URL.
 
 ## 1.31.1
 
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 608cc4b..ef00a79 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 * Support using the OS platform selector to configure browser tests.
 * Treat calls to `exit` as test failures in VM tests.
+* Use a DevTools URL instead of a defunct observatory URL.
 
 ## 0.6.18
 
diff --git a/pkgs/test_core/lib/src/runner/environment.dart b/pkgs/test_core/lib/src/runner/environment.dart
index 58033d2..fd73d0b 100644
--- a/pkgs/test_core/lib/src/runner/environment.dart
+++ b/pkgs/test_core/lib/src/runner/environment.dart
@@ -12,8 +12,9 @@
   /// Whether this environment supports interactive debugging.
   bool get supportsDebugging;
 
-  /// The URL of the Dart VM Observatory for this environment, or `null` if this
-  /// environment doesn't run the Dart VM or the URL couldn't be detected.
+  /// The URL of the Dart Dev Tools server for this environment, or `null` if
+  /// this environment doesn't run the Dart VM or the URL couldn't be detected.
+  // TODO(https://github.com/dart-lang/test/issues/2185) rename to `devToolsUrl`
   Uri? get observatoryUrl;
 
   /// The URL of the remote debugger for this environment, or `null` if it isn't
diff --git a/pkgs/test_core/lib/src/runner/vm/platform.dart b/pkgs/test_core/lib/src/runner/vm/platform.dart
index 6414251..1986a85 100644
--- a/pkgs/test_core/lib/src/runner/vm/platform.dart
+++ b/pkgs/test_core/lib/src/runner/vm/platform.dart
@@ -125,6 +125,8 @@
 
     Environment? environment;
     IsolateRef? isolateRef;
+    String? isolateID;
+    Uri? serverUri;
     if (_config.debug) {
       if (platform.compiler == Compiler.exe) {
         throw UnsupportedError(
@@ -136,21 +138,17 @@
         enable: true,
         silenceOutput: true,
       );
-      // ignore: deprecated_member_use, Remove when SDK constraint is at 3.2.0
-      var isolateID = Service.getIsolateID(isolate!)!;
+      serverUri = info.serverUri!;
+      isolateID = Service.getIsolateId(isolate!)!;
 
-      var libraryPath = (await absoluteUri(path)).toString();
-      var serverUri = info.serverUri!;
-      client = await vmServiceConnectUri(_wsUriFor(serverUri).toString());
+      var serviceWebsocket = _wsUriFor(serverUri);
+      client = await vmServiceConnectUri(serviceWebsocket.toString());
       var isolateNumber = int.parse(isolateID.split('/').last);
       isolateRef = (await client.getVM()).isolates!.firstWhere(
         (isolate) => isolate.number == isolateNumber.toString(),
       );
       await client.setName(isolateRef.id!, path);
-      var libraryRef = (await client.getIsolate(
-        isolateRef.id!,
-      )).libraries!.firstWhere((library) => library.uri == libraryPath);
-      var url = _observatoryUrlFor(serverUri, isolateRef.id!, libraryRef.id!);
+      var url = _devtoolsUriFor(serviceWebsocket);
       environment = VMEnvironment(url, isolateRef, client);
     }
 
@@ -163,7 +161,14 @@
       environment,
       channel.cast(),
       message,
-      gatherCoverage: () => _gatherCoverage(environment!, _config),
+      gatherCoverage: () {
+        if (serverUri == null || isolateID == null) {
+          throw StateError(
+            'VM Service is not running, cannot gather coverage.',
+          );
+        }
+        return _gatherCoverage(serverUri, isolateID, _config);
+      },
     );
 
     if (isolateRef != null) {
@@ -458,19 +463,17 @@
 }
 
 Future<Map<String, dynamic>> _gatherCoverage(
-  Environment environment,
+  Uri serviceUri,
+  String isolateId,
   Configuration config,
 ) async {
-  final isolateId = Uri.parse(
-    environment.observatoryUrl!.fragment,
-  ).queryParameters['isolateId'];
   return await collect(
-    environment.observatoryUrl!,
+    serviceUri,
     false,
     false,
     false,
     await _filterCoveragePackages(config.coveragePackages, config.coverageLcov),
-    isolateIds: {isolateId!},
+    isolateIds: {isolateId},
     branchCoverage: config.branchCoverage,
   );
 }
@@ -478,12 +481,16 @@
 Uri _wsUriFor(Uri observatoryUrl) =>
     observatoryUrl.replace(scheme: 'ws').resolve('ws');
 
-Uri _observatoryUrlFor(Uri base, String isolateId, String id) => base.replace(
-  fragment: Uri(
-    path: '/inspect',
-    queryParameters: {'isolateId': isolateId, 'objectId': id},
-  ).toString(),
-);
+Uri _devtoolsUriFor(Uri serviceUri) {
+  assert(serviceUri.isScheme('ws'));
+  return serviceUri
+      .resolve('devtools/debugger')
+      .replace(
+        scheme: 'http',
+        queryParameters: {'uri': '$serviceUri'},
+        fragment: '',
+      );
+}
 
 var _hasRegistered = false;
 void _setupPauseAfterTests() {