[dds/dap] Ensure DAP always sends resume on PausePostRequest

The change at https://dart-review.googlesource.com/c/sdk/+/410760 to fix a race incorrectly assumed that once we had handled startup for a thread, we would never need to send an automatic resume again. However this was not the case - after a hot reload, we need to resume the thread even though we had technically already handled startup.

This is essentially a partial revert of 84e6ed0784c679c03ec6380a4aed6aa78eab2c24, with a test to verify we trigger readyToResume on PausePostRequest.

Change-Id: I1e171043f04f38dd6f52e1692d9257d34e5248be
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/416580
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Reviewed-by: Derek Xu <derekx@google.com>
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index a029480..6d90f2e 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -645,7 +645,12 @@
     // after a hot restart.
     if (eventKind == vm.EventKind.kPausePostRequest) {
       await _configureIsolate(thread);
-      await handleThreadStartup(thread, sendStoppedOnEntry: false);
+
+      // We always want to resume here regardless of whether startupHandled was
+      // already `true` (because that might be from before the reload).
+      // Therefore set the flag and resume always.
+      thread.startupHandled = true;
+      await readyToResumeThread(thread.threadId);
     } else if (eventKind == vm.EventKind.kPauseStart) {
       handleThreadStartup(thread, sendStoppedOnEntry: true);
     } else {
diff --git a/pkg/dds/test/dap/isolate_manager_test.dart b/pkg/dds/test/dap/isolate_manager_test.dart
index 74871d4..0751f62 100644
--- a/pkg/dds/test/dap/isolate_manager_test.dart
+++ b/pkg/dds/test/dap/isolate_manager_test.dart
@@ -144,5 +144,27 @@
       expect(receivedUri, 'scheme://file/path.dart');
       expect(resolved, Uri.file('/returned/file/path.dart'));
     });
+
+    test('sends resume after kPausePostRequest', () async {
+      // Trigger initial startup.
+      await adapter.isolateManager.handleEvent(Event(
+          isolate: adapter.mockService.isolate1,
+          kind: EventKind.kIsolateRunnable));
+      isolateManager.threads[0].startupHandled = true;
+
+      // Clear any previously called methods.
+      adapter.mockService.calledMethods.clear();
+
+      // Now simulate a hot reload causing the isolate to pause again.
+      await adapter.isolateManager.handleEvent(Event(
+          isolate: adapter.mockService.isolate1,
+          kind: EventKind.kPausePostRequest));
+
+      // Ensure we called readyToResume.
+      expect(adapter.mockService.calledMethods.map((cm) => cm.method),
+          contains('readyToResume'));
+
+      // Expect it was resumed.
+    });
   });
 }
diff --git a/pkg/dds/test/dap/mocks.dart b/pkg/dds/test/dap/mocks.dart
index 3b6b81b..7a4c3a7 100644
--- a/pkg/dds/test/dap/mocks.dart
+++ b/pkg/dds/test/dap/mocks.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 
 import 'package:dap/dap.dart' as dap;
-import 'package:dds/dap.dart';
+import 'package:dds/dap.dart' hide Response;
 import 'package:dds/dds.dart';
 import 'package:dds/dds_launcher.dart';
 import 'package:dds/src/dap/adapters/dart_cli_adapter.dart';
@@ -168,6 +168,10 @@
   /// tests.
   final List<String> requests = [];
 
+  /// A list of methods that were called on the Mock VM Service.
+  List<({String method, String? isolateId, Map<String, dynamic>? args})>
+      calledMethods = [];
+
   @override
   Future<Success> setLibraryDebuggable(
     String isolateId,
@@ -214,6 +218,30 @@
   }
 
   @override
+  Future<Response> callMethod(
+    String method, {
+    String? isolateId,
+    Map<String, dynamic>? args,
+  }) async {
+    calledMethods.add((method: method, isolateId: isolateId, args: args));
+
+    // Some DDS methods are implemented as extensions so we can't override
+    // them in the mock, we need to override callMethod and handle them here
+    // (and provide their responses) instead.
+
+    if (method == 'getDartDevelopmentServiceVersion') {
+      return Version(major: 2, minor: 0);
+    } else if (method == 'readyToResume') {
+      isolateId ??= args != null ? args['isolateId'] : null;
+      return {isolate1.id, isolate2.id}.contains(isolateId)
+          ? Success()
+          : throw SentinelException.parse(method, {});
+    }
+
+    throw 'MockVmService does not handle $method ($isolateId, $args)';
+  }
+
+  @override
   Future<Success> resume(
     String isolateId, {
     String? step,
@@ -224,6 +252,18 @@
         ? Success()
         : throw SentinelException.parse('resume', {});
   }
+
+  @override
+  Future<Success> setIsolatePauseMode(
+    String isolateId, {
+    /*ExceptionPauseMode*/ String? exceptionPauseMode,
+    bool? shouldPauseOnExit,
+  }) async {
+    // Do nothing, just pretend.
+    return {isolate1.id, isolate2.id}.contains(isolateId)
+        ? Success()
+        : throw SentinelException.parse('setIsolatePauseMode', {});
+  }
 }
 
 class MockDartDevelopmentServiceLauncher