// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>

#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/extensions/api/permissions/permissions_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_management_test_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/profile_destruction_waiter.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/api/test/test_api.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/service_worker/service_worker_keepalive.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

constexpr char kTestOpenerExtensionId[] = "adpghjkjicpfhcjicmiifjpbalaildpo";
constexpr char kTestOpenerExtensionUrl[] =
    "chrome-extension://adpghjkjicpfhcjicmiifjpbalaildpo/";
constexpr char kTestOpenerExtensionRelativePath[] =
    "service_worker/policy/opener_extension";

constexpr char kTestReceiverExtensionId[] = "eagjmgdicfmccfhiiihnaehbfheheidk";
constexpr char kTestReceiverExtensionUrl[] =
    "chrome-extension://eagjmgdicfmccfhiiihnaehbfheheidk/";
constexpr char kTestReceiverExtensionRelativePath[] =
    "service_worker/policy/receiver_extension";

constexpr char kPersistentPortConnectedMessage[] = "Persistent port connected";
constexpr char kPersistentPortDisconnectedMessage[] =
    "Persistent port disconnected";

// Gets a keepalive matcher that enforces the extra data field.
testing::Matcher<ProcessManager::ServiceWorkerKeepaliveData>
GetKeepaliveMatcher(const WorkerId& worker_id,
                    Activity::Type type,
                    const std::string& activity_extra_data) {
  return testing::AllOf(
      testing::Field("worker_id",
                     &ProcessManager::ServiceWorkerKeepaliveData::worker_id,
                     worker_id),
      testing::Field("activity_type",
                     &ProcessManager::ServiceWorkerKeepaliveData::activity_type,
                     type),
      testing::Field("extra_data",
                     &ProcessManager::ServiceWorkerKeepaliveData::extra_data,
                     activity_extra_data));
}

// Gets a keepalive matcher enforcing only the worker ID and activity type.
testing::Matcher<ProcessManager::ServiceWorkerKeepaliveData>
GetKeepaliveMatcher(const WorkerId& worker_id, Activity::Type type) {
  return testing::AllOf(
      testing::Field("worker_id",
                     &ProcessManager::ServiceWorkerKeepaliveData::worker_id,
                     worker_id),
      testing::Field("activity_type",
                     &ProcessManager::ServiceWorkerKeepaliveData::activity_type,
                     type));
}

// Returns the number of active external requests to the service worker of
// the specified `extension` in the given `context`.
size_t GetExternalRequestCountForWorker(content::BrowserContext& context,
                                        const Extension& extension) {
  const blink::StorageKey extension_key =
      blink::StorageKey::CreateFirstParty(extension.origin());
  return service_worker_test_utils::GetServiceWorkerContext(&context)
      ->CountExternalRequestsForTest(extension_key);
}

}  // namespace

using service_worker_test_utils::TestServiceWorkerContextObserver;

class ServiceWorkerLifetimeKeepaliveBrowsertest : public ExtensionApiTest {
 public:
  ServiceWorkerLifetimeKeepaliveBrowsertest() {
    // TODO(crbug.com/40937027): Convert test to use HTTPS and then re-enable.
    feature_list_.InitAndDisableFeature(features::kHttpsFirstModeIncognito);
  }

  ServiceWorkerLifetimeKeepaliveBrowsertest(
      const ServiceWorkerLifetimeKeepaliveBrowsertest&) = delete;
  ServiceWorkerLifetimeKeepaliveBrowsertest& operator=(
      const ServiceWorkerLifetimeKeepaliveBrowsertest&) = delete;

  ~ServiceWorkerLifetimeKeepaliveBrowsertest() override = default;

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(StartEmbeddedTestServer());
  }

  void TearDownOnMainThread() override {
    ExtensionApiTest::TearDownOnMainThread();
    // Some tests use SetTickClockForTesting() with `tick_clock_opener_` or
    // `tick_clock_receiver_`. Restore the TickClock to the default now.
    // This is required because the TickClock must outlive ServiceWorkerVersion,
    // otherwise ServiceWorkerVersion will hold a dangling pointer.
    content::ResetTickClockToDefaultForAllLiveServiceWorkerVersions(
        GetServiceWorkerContext());
  }

  void TriggerTimeoutAndCheckActive(content::ServiceWorkerContext* context,
                                    int64_t version_id) {
    EXPECT_TRUE(
        content::TriggerTimeoutAndCheckRunningState(context, version_id));
  }

  void TriggerTimeoutAndCheckStopped(content::ServiceWorkerContext* context,
                                     int64_t version_id) {
    EXPECT_FALSE(
        content::TriggerTimeoutAndCheckRunningState(context, version_id));
  }

  base::SimpleTestTickClock tick_clock_opener_;
  base::SimpleTestTickClock tick_clock_receiver_;

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Loads two extensions that open a persistent port connection between each
// other and tests that their service worker will stop after kRequestTimeout (5
// minutes).
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersTimeOutWithoutPolicy) {
  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerStarted();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  TestServiceWorkerContextObserver sw_observer_opener_extension(
      context, kTestOpenerExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
  const int64_t service_worker_opener_id =
      sw_observer_opener_extension.WaitForWorkerStarted();

  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  // Advance clock and check that the receiver service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);
  sw_observer_receiver_extension.WaitForWorkerStopped();

  // Advance clock and check that the opener service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
  sw_observer_opener_extension.WaitForWorkerStopped();
}

// Tests that the service workers will not stop if both extensions are
// allowlisted via policy and the port is not closed.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersDoNotTimeOutWithPolicy) {
  base::ListValue urls;
  // Both extensions receive extended lifetime.
  urls.Append(kTestOpenerExtensionUrl);
  urls.Append(kTestReceiverExtensionUrl);
  profile()->GetPrefs()->SetList(
      pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
      std::move(urls));

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  // Load the extensions and wait for the service workers to be activated. This
  // test advances the worker's clock. If the activation request is in-flight
  // when the clock is advanced, the request will expire and the worker will be
  // terminated (because activation requests have KILL_ON_TIMEOUT behavior).
  // Thus, we ensure that there are no in-flight activation requests before
  // advancing the clock.
  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerActivated();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  TestServiceWorkerContextObserver sw_observer_opener_extension(
      context, kTestOpenerExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
  const int64_t service_worker_opener_id =
      sw_observer_opener_extension.WaitForWorkerActivated();

  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  // Advance clock and check that the receiver service worker did not stop.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckActive(context, service_worker_receiver_id);

  // Advance clock and check that the opener service worker did not stop.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckActive(context, service_worker_opener_id);
}

// Tests that the extended lifetime only lasts as long as there is a persistent
// port connection. If the port is closed (by one of the service workers
// stopping), the other service worker will also stop, even if it received an
// extended lifetime.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersTimeOutWhenOnlyOneHasExtendedLifetime) {
  base::ListValue urls;
  // Opener extension will receive extended lifetime because it connects to a
  // policy allowlisted extension.
  urls.Append(kTestReceiverExtensionUrl);
  profile()->GetPrefs()->SetList(
      pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
      std::move(urls));

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerStarted();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  TestServiceWorkerContextObserver sw_observer_opener_extension(
      context, kTestOpenerExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
  const int64_t service_worker_opener_id =
      sw_observer_opener_extension.WaitForWorkerStarted();

  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  ExtensionTestMessageListener disconnect_listener(
      kPersistentPortDisconnectedMessage);
  disconnect_listener.set_extension_id(kTestOpenerExtensionId);

  // Advance clock and check that the receiver service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);

  // Wait for the receiver SW to be closed in order for the port to be
  // disconnected and the opener SW losing extended lifetime.
  sw_observer_receiver_extension.WaitForWorkerStopped();

  // Wait for port to close in the opener extension.
  ASSERT_TRUE(disconnect_listener.WaitUntilSatisfied());

  // Advance clock and check that the opener service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
  sw_observer_opener_extension.WaitForWorkerStopped();
}

// Tests that the service workers will stop if both extensions are allowlisted
// via policy and the port is disconnected.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersTimeOutWhenPortIsDisconnected) {
  base::ListValue urls;
  // Both extensions receive extended lifetime.
  urls.Append(kTestReceiverExtensionUrl);
  urls.Append(kTestOpenerExtensionUrl);
  profile()->GetPrefs()->SetList(
      pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
      std::move(urls));

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerStarted();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  TestServiceWorkerContextObserver sw_observer_opener_extension(
      context, kTestOpenerExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
  const int64_t service_worker_opener_id =
      sw_observer_opener_extension.WaitForWorkerStarted();

  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  ExtensionTestMessageListener disconnect_listener(
      kPersistentPortDisconnectedMessage);
  disconnect_listener.set_extension_id(kTestOpenerExtensionId);

  // Disconnect the port from the receiver extension.
  constexpr char kDisconnectScript[] = R"(port.disconnect();)";
  BackgroundScriptExecutor script_executor(profile());
  script_executor.ExecuteScriptAsync(
      kTestReceiverExtensionId, kDisconnectScript,
      BackgroundScriptExecutor::ResultCapture::kNone,
      browsertest_util::ScriptUserActivation::kDontActivate);

  // Wait for port to close in the opener extension.
  ASSERT_TRUE(disconnect_listener.WaitUntilSatisfied());

  // Advance clock and check that the receiver service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);

  // Wait for the receiver SW to be closed.
  sw_observer_receiver_extension.WaitForWorkerStopped();

  // Advance clock and check that the opener service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
  sw_observer_opener_extension.WaitForWorkerStopped();
}

// Tests that certain API functions can keep the service worker alive
// indefinitely.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       KeepalivesForCertainExtensionFunctions) {
  static constexpr char kManifest[] =
      R"({
           "name": "test extension",
           "manifest_version": 3,
           "background": {"service_worker": "background.js"},
           "version": "0.1",
           "optional_permissions": ["tabs"]
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// blank");

  // Load up the extension and wait for the worker to start.
  TestServiceWorkerContextObserver registration_observer(profile());
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  // We explicitly wait for the worker to be activated. Otherwise, the
  // activation event might still be running when we advance the timer, causing
  // the worker to be killed for the activation event timing out.
  int64_t version_id = registration_observer.WaitForWorkerActivated();

  // Inject a script that will trigger chrome.permissions.request() and then
  // return. When permissions.request() resolves, it will send a message.
  static constexpr char kTriggerPrompt[] =
      R"(chrome.test.runWithUserGesture(() => {
           chrome.permissions.request({permissions: ['tabs']}).then(() => {
             chrome.test.sendMessage('resolved');
           });
           chrome.test.sendScriptResult('success');
         });)";

  // Programmatically control the permissions request result. This allows us
  // to control when it is resolved.
  auto dialog_action_reset =
      PermissionsRequestFunction::SetDialogActionForTests(
          PermissionsRequestFunction::DialogAction::kProgrammatic);

  base::Value result = BackgroundScriptExecutor::ExecuteScript(
      profile(), extension->id(), kTriggerPrompt,
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("success", result);

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  // Right now, the permissions request should be pending. Since
  // `permissions.request()` is specified as a function that can keep the
  // extension worker alive indefinitely, advancing the clock and triggering the
  // timeout should not result in a worker kill.
  content::AdvanceClockAfterRequestTimeout(context, version_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckActive(context, version_id);

  {
    ExtensionTestMessageListener listener("resolved");
    // Resolve the pending dialog and wait for the resulting message.
    PermissionsRequestFunction::ResolvePendingDialogForTests(false);
    ASSERT_TRUE(listener.WaitUntilSatisfied());
    // We also run a run loop here so that the keepalive from the
    // test.sendMessage() call is resolved.
    base::RunLoop().RunUntilIdle();
  }

  // Advance the timer again. This should result in the worker being stopped,
  // since the permissions.request() function call is now completed.
  content::AdvanceClockAfterRequestTimeout(context, version_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, version_id);
}

// Test the flow of an extension function resolving after an extension service
// worker has timed out and been terminated.
// Regression test for https://crbug.com/40065560.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ExtensionFunctionGetsResolvedAfterWorkerTermination) {
  static constexpr char kManifest[] =
      R"({
           "name": "test extension",
           "manifest_version": 3,
           "background": {"service_worker": "background.js"},
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// blank");

  // Load up the extension and wait for the worker to start.
  TestServiceWorkerContextObserver registration_observer(profile());
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  // We explicitly wait for the worker to be activated. Otherwise, the
  // activation event might still be running when we advance the timer, causing
  // the worker to be killed for the activation event timing out.
  int64_t version_id = registration_observer.WaitForWorkerActivated();

  // Inject a trivial script that will call test.sendMessage(). This is a handy
  // API because, by indicating the test will reply, we control when the
  // function is resolved.
  static constexpr char kScript[] =
      "chrome.test.sendMessage('hello', () => {});";
  ExtensionTestMessageListener message_listener("hello",
                                                ReplyBehavior::kWillReply);
  BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension->id(),
                                               kScript);

  ASSERT_TRUE(message_listener.WaitUntilSatisfied());

  content::ServiceWorkerContext* context = GetServiceWorkerContext();
  TestServiceWorkerContextObserver context_observer(context, extension->id());
  context_observer.SetRunningId(version_id);

  // Advance the request past the timeout. Since test.sendMessage() doesn't
  // keep a worker alive indefinitely, the service worker should be terminated.
  content::AdvanceClockAfterRequestTimeout(context, version_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, version_id);
  // Wait for the worker to fully stop.
  context_observer.WaitForWorkerStopped();

  // Reply to the extension (even though the worker is gone). This triggers
  // the completion of the extension function, which would otherwise try to
  // decrement the keepalive count of the worker. The worker was already
  // terminated; it should gracefully handle this case (as opposed to crash).
  message_listener.Reply("foo");
}

// Tests that an active debugger session will keep an extension service worker
// alive past its typical timeout.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       DebuggerAttachKeepsServiceWorkerAlive) {
  static constexpr char kManifest[] =
      R"({
           "name": "Debugger attach",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["debugger"],
           "background": {
             "service_worker": "background.js"
           }
         })";
  // A simple background script that knows how to attach and detach a debugging
  // session from a target (active) tab.
  static constexpr char kBackgroundJs[] =
      R"(let attachedTab;
         async function attachToActiveTab() {
           let tabs =
               await chrome.tabs.query({active: true, currentWindow: true});
           let tab = tabs[0];
           await chrome.debugger.attach({tabId: tab.id}, '1.3');
           attachedTab = tab;
           chrome.test.sendScriptResult('attached');
         }

         async function detach() {
           await chrome.debugger.detach({tabId: attachedTab.id});
           chrome.test.sendScriptResult('detached');
         })";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);

  // Load up the extension and wait for the worker to start.
  TestServiceWorkerContextObserver registration_observer(profile());
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  // We explicitly wait for the worker to be activated. Otherwise, the
  // activation event might still be running when we advance the timer, causing
  // the worker to be killed for the activation event timing out.
  int64_t version_id = registration_observer.WaitForWorkerActivated();

  // Open a new tab for the extension to attach a debugger to.
  const GURL example_com =
      embedded_test_server()->GetURL("example.com", "/simple.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, example_com));
  EXPECT_EQ(example_com, web_contents->GetLastCommittedURL());

  // Attach the extension debugger.
  EXPECT_EQ("attached",
            BackgroundScriptExecutor::ExecuteScript(
                profile(), extension->id(), "attachToActiveTab();",
                BackgroundScriptExecutor::ResultCapture::kSendScriptResult));
  // Ensure the keepalive associated with sendScriptResult() has resolved.
  base::RunLoop().RunUntilIdle();

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  // Since the extension has an active debugger session, it should not be
  // terminated, even for going past the typical time limit.
  content::AdvanceClockAfterRequestTimeout(context, version_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckActive(context, version_id);

  // Have the extension detach its debugging session.
  EXPECT_EQ("detached",
            BackgroundScriptExecutor::ExecuteScript(
                profile(), extension->id(), "detach();",
                BackgroundScriptExecutor::ResultCapture::kSendScriptResult));
  // Ensure the keepalive associated with sendScriptResult() has resolved.
  base::RunLoop().RunUntilIdle();

  // The extension service worker should now be terminated, since it no longer
  // has an active debug session.
  content::AdvanceClockAfterRequestTimeout(context, version_id,
                                           &tick_clock_opener_);
  TriggerTimeoutAndCheckStopped(context, version_id);
}

// Tests the behavior of the ServiceWorkerKeepalive struct, ensuring it properly
// keeps the service worker alive.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkerKeepaliveUtility) {
  // Load up a simple extension and grab its service worker data.
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "version": "0.1",
           "manifest_version": 3,
           "background": {"service_worker": "background.js"}
         })";
  static constexpr char kBackground[] = R"(chrome.test.sendMessage('ready');)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);

  ExtensionTestMessageListener ready_listener("ready");
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
  // Note: We RunUntilIdle() to ensure the implementation handling of the
  // test.sendMessage() API call has finished; otherwise, that affects our
  // keepalives.
  base::RunLoop().RunUntilIdle();

  ProcessManager* process_manager = ProcessManager::Get(profile());

  std::vector<WorkerId> worker_ids =
      process_manager->GetServiceWorkersForExtension(extension->id());
  ASSERT_EQ(1u, worker_ids.size());
  WorkerId worker_id = worker_ids[0];

  // To begin, there should be no associated keepalives for the extension.
  EXPECT_EQ(0u, process_manager
                    ->GetServiceWorkerKeepaliveDataForRecords(extension->id())
                    .size());

  // Create a single keepalive.
  std::optional<ServiceWorkerKeepalive> function_keepalive(
      ServiceWorkerKeepalive(
          profile(), worker_id,
          content::ServiceWorkerExternalRequestTimeoutType::kDefault,
          Activity::API_FUNCTION, "alarms.create"));

  // There should be one keepalive for the extension.
  EXPECT_THAT(
      process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
      testing::UnorderedElementsAre(GetKeepaliveMatcher(
          worker_id, Activity::API_FUNCTION, "alarms.create")));

  // Create a second keepalive (an event-related one).
  std::optional<ServiceWorkerKeepalive> event_keepalive(ServiceWorkerKeepalive(
      profile(), worker_id,
      content::ServiceWorkerExternalRequestTimeoutType::kDefault,
      Activity::EVENT, "alarms.onAlarm"));

  // Now, there should be two keepalives.
  EXPECT_THAT(
      process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
      testing::UnorderedElementsAre(
          GetKeepaliveMatcher(worker_id, Activity::API_FUNCTION,
                              "alarms.create"),
          GetKeepaliveMatcher(worker_id, Activity::EVENT, "alarms.onAlarm")));

  // Reset the first. There should now be only the second keepalive.
  function_keepalive.reset();
  EXPECT_THAT(
      process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
      testing::UnorderedElementsAre(
          GetKeepaliveMatcher(worker_id, Activity::EVENT, "alarms.onAlarm")));

  // Reset the second, and the keepalive count should go to zero.
  event_keepalive.reset();
  EXPECT_EQ(0u, process_manager
                    ->GetServiceWorkerKeepaliveDataForRecords(extension->id())
                    .size());
}

// Tests that the browser ignores stale idle termination requests from the
// renderer that were sent before the browser started an external request. See
// https://crbug.com/487746357.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       StaleIdleTerminationIgnoredAfterExternalRequest) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "version": "0.1",
           "manifest_version": 3,
           "background": {"service_worker": "background.js"}
         })";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");

  TestServiceWorkerContextObserver registration_observer(profile());
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  const int64_t version_id = registration_observer.WaitForWorkerActivated();

  ProcessManager* process_manager = ProcessManager::Get(profile());
  ASSERT_TRUE(process_manager);
  std::vector<WorkerId> worker_ids =
      process_manager->GetServiceWorkersForExtension(extension->id());
  ASSERT_EQ(1u, worker_ids.size());
  WorkerId worker_id = worker_ids[0];
  EXPECT_EQ(version_id, worker_id.version_id);

  // Ensure no requests are currently pending.
  EXPECT_EQ(0u, GetExternalRequestCountForWorker(*profile(), *extension));

  // Simulate an Extension API dispatching a message. We create a
  // `ServiceWorkerKeepalive` to prevent the worker from sleeping.
  {
    ServiceWorkerKeepalive keepalive(
        profile(), worker_id,
        content::ServiceWorkerExternalRequestTimeoutType::kDefault,
        Activity::MESSAGE, "stale-idle-termination-test");
    // The request count goes up, and under the hood, the browser's keepalive
    // sequence number increments to 1.
    EXPECT_EQ(1u, GetExternalRequestCountForWorker(*profile(), *extension));
  }

  // The keepalive goes out of scope and is destroyed. The request count
  // drops back to 0. The worker is now idle.
  EXPECT_EQ(0u, GetExternalRequestCountForWorker(*profile(), *extension));

  // Forcefully trigger a termination request as if it came from the renderer.
  // Crucially, we pass sequence number 0. Simulates the renderer requesting
  // termination just before it received the keepalive we created earlier.
  content::ServiceWorkerContext* context = GetServiceWorkerContext();
  const bool stale_termination_kept_worker_running =
      content::TriggerTimeoutAndCheckRunningStateWithSequenceNumber(
          context, version_id, 0);

  // The browser must recognize 0 is stale (since the keepalive above bumped the
  // browser's internal sequence number to 1). Therefore, it ignores the
  // termination request and keeps the worker running.
  EXPECT_TRUE(stale_termination_kept_worker_running);
  EXPECT_TRUE(content::CheckServiceWorkerIsRunning(context, version_id));
}

// Tests shutting down the associated browser context while the extension has
// an active keepalive from a message pipe behaves appropriately.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ShutdownWithActiveMessagePipe) {
  // Load an extension with incognito split mode and a content script that
  // runs on example.com.
  // The split mode incognito is important so that we can fully shut down a
  // browser context with separately-tracked keepalives.
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "split",
           "background": {"service_worker": "background.js"},
           "content_scripts": [
             {
               "js": ["content_script.js"],
               "matches": ["*://example.com/*"],
               "run_at": "document_end"
             }
           ]
         })";
  static constexpr char kBackgroundJs[] = R"(// Intentionally blank.)";
  // The content script adds a listener for a new message and then
  // (asynchronously) signals success.
  // See keepalive comments below for why this is async.
  // NOTE: We're careful not to have the port be garbage collected by storing
  // it on `self`; otherwise this could close the message pipe.
  static constexpr char kContentScriptJs[] =
      R"(chrome.runtime.onMessage.addListener((msg, sender, reply) => {
           self.reply = reply;
           setTimeout(() => { chrome.test.sendScriptResult('success'); }, 0);
           // Indicates async response, keeping the message pipe open.
           return true;
         });
         chrome.test.sendMessage('content script ready');)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScriptJs);

  const Extension* extension =
      LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(extension);

  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  TestServiceWorkerContextObserver registration_observer(incognito_profile);
  // Open example.com/simple.html in an incognito window. The content script
  // will inject.
  ExtensionTestMessageListener content_script_listener("content script ready");
  content::WebContents* incognito_contents = PlatformOpenURLOffTheRecord(
      profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
  ASSERT_TRUE(content_script_listener.WaitUntilSatisfied());
  registration_observer.WaitForWorkerActivated();
  int tab_id = ExtensionTabUtil::GetTabId(incognito_contents);

  // Send a message to the incognito tab from the incognito service worker.
  // This will open a message pipe. Since the content script never responds,
  // the message pipe will remain open.
  static constexpr char kOpenMessagePipe[] =
      R"((async () => {
           // Note: Pass a callback to signal a reply is expected.
           chrome.tabs.sendMessage(%d, 'hello', () => {});
         })();)";

  base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
      incognito_profile, extension->id(),
      base::StringPrintf(kOpenMessagePipe, tab_id),
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("success", script_result);

  ProcessManager* incognito_process_manager =
      ProcessManager::Get(incognito_profile);

  // Grab the active worker for the incognito context.
  std::vector<WorkerId> worker_ids =
      incognito_process_manager->GetServiceWorkersForExtension(extension->id());
  ASSERT_EQ(1u, worker_ids.size());
  WorkerId worker_id = worker_ids[0];

  // Verify the service worker currently has a keepalive for the message
  // port.
  // The keepalive flow is as follows:
  // * Service worker opens a message pipe. New Activity::MESSAGE_PORT
  //   keepalive from the worker context.
  // * Message pipe is opened in the tab. New Activity::MESSAGE_PORT
  //   keepalive from the tab context.
  // * Message is sent to the tab. New Activity::MESSAGE keepalive from
  //   the tab context.
  // * The message is ack'd from the tab. Activity::MESSAGE keepalive
  //   from the tab context is removed. Since we signal success in the
  //   tab asynchronously, the keepalive is guaranteed to have resolved.
  //   (Otherwise, it could potentially be racy).
  // Thus, at the end, we have two remaining keepalives.
  // TODO(crbug.com/41487026): Ideally, there would only be one -- we shouldn't
  // add keepalives for the service worker due to a tab's message port.

  EXPECT_THAT(
      incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
          extension->id()),
      testing::UnorderedElementsAre(
          GetKeepaliveMatcher(worker_id, Activity::MESSAGE_PORT),
          GetKeepaliveMatcher(worker_id, Activity::MESSAGE_PORT)));

  // Close the incognito tab while the message channel is still open. Since
  // this is the only browser window for the incognito context, this also
  // results in the browser context being invalidated.
  ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
  CloseTabForWebContents(incognito_contents);
  profile_destruction_waiter.Wait();
  // Note: `ProfileDestructionWaiter` only waits for the profile to signal it
  // *will* be destroyed. Spin once to finish the job.
  base::RunLoop().RunUntilIdle();
  // Verify the profile is destroyed.
  EXPECT_FALSE(
      ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));
  // The test succeeds if there are no crashes. There's nothing left to verify
  // for keepalives, since the profile is gone.
}

// Tests that we can safely shut down a BrowserContext when an extension has
// an active message port to another extension, where each are running in
// split incognito mode.
// Regression test for https://crbug.com/40070712.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
                       ShutdownWithActiveMessagePipe_SplitModeExtension) {
  // A split-mode extension. This will have a separate process for the on- and
  // off-the-record profiles.
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "split",
           "background": {"service_worker": "background.js"}
         })";
  // A background page that knows how to open a message pipe to another
  // extension.
  static constexpr char kOpenerBackgroundJs[] =
      R"(async function openMessagePipe(listenerId) {
           // Note: Pass a callback to signal a reply is expected.
           chrome.runtime.sendMessage(listenerId, 'hello', () => {});
         })";
  // The listener extension will listen for an external message (from the
  // opener mode extension). We save the `sendReply` callback so it's not
  // garbage collected and keeps the message pipe open, and then asynchronously
  // respond that the message was received. The asynchronous response is
  // important in order to ensure the message being received from this
  // extension is properly ack'd.
  static constexpr char kListenerBackgroundJs[] =
      R"(chrome.runtime.onMessageExternal.addListener(
             (msg, sender, sendReply) => {
               self.sendReply = sendReply;
               setTimeout(() => { chrome.test.sendScriptResult('success'); });
               return true;
             });)";

  TestExtensionDir opener_extension_dir;
  opener_extension_dir.WriteManifest(kManifest);
  opener_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                                 kOpenerBackgroundJs);

  TestExtensionDir listener_extension_dir;
  listener_extension_dir.WriteManifest(kManifest);
  listener_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                                   kListenerBackgroundJs);

  const Extension* opener_extension = LoadExtension(
      opener_extension_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(opener_extension);
  const Extension* listener_extension = LoadExtension(
      listener_extension_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(listener_extension);

  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  service_worker_test_utils::TestServiceWorkerTaskQueueObserver
      task_queue_observer;
  // Open a new tab in incognito. This spawns the new process for the split mode
  // extensions.
  content::WebContents* incognito_contents = PlatformOpenURLOffTheRecord(
      profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
  task_queue_observer.WaitForWorkerContextInitialized(listener_extension->id());
  task_queue_observer.WaitForWorkerContextInitialized(opener_extension->id());

  // Send a message from one extension to the other, opening a message pipe.
  // Since the listener extension never responds, the message pipe will
  // remain open. The listener then sends the script result 'success' when it
  // receives the message.
  static constexpr char kOpenMessagePipe[] = R"(openMessagePipe('%s');)";
  base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
      incognito_profile, opener_extension->id(),
      base::StringPrintf(kOpenMessagePipe, listener_extension->id().c_str()),
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("success", script_result);

  ProcessManager* incognito_process_manager =
      ProcessManager::Get(incognito_profile);

  // Grab each extension's active worker.
  std::vector<WorkerId> opener_worker_ids =
      incognito_process_manager->GetServiceWorkersForExtension(
          opener_extension->id());
  ASSERT_EQ(1u, opener_worker_ids.size());
  WorkerId opener_worker_id = opener_worker_ids[0];

  std::vector<WorkerId> listener_worker_ids =
      incognito_process_manager->GetServiceWorkersForExtension(
          listener_extension->id());
  ASSERT_EQ(1u, listener_worker_ids.size());
  WorkerId listener_worker_id = listener_worker_ids[0];

  // Verify the service workers currently have a keepalive for the message
  // port.
  // The keepalive flow is as follows:
  // * Open a new message port. Add keepalives for both extensions with
  //   Activity::MESSAGE_PORT.
  // * Message is sent to the listener extension. New Activity::MESSAGE
  //   keepalive is added for the sender extension.
  // * The message is ack'd from the listener extension's process.
  //   Activity::MESSAGE keepalive is removed for the sender extension.
  //   Since we signal success in the listener asynchronously, the keepalive is
  //   guaranteed to have resolved. (Otherwise, it could potentially be racy).
  // * Send chrome.test.sendScriptResult() from the listener extension.
  //   Add and remove Activity::API_FUNCTION keepalives.
  // Thus, at the end, the remaining keepalives are one MESSAGE_PORT keepalive
  // for each extension.
  EXPECT_THAT(
      incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
          opener_extension->id()),
      testing::UnorderedElementsAre(
          GetKeepaliveMatcher(opener_worker_id, Activity::MESSAGE_PORT)));
  EXPECT_THAT(
      incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
          listener_extension->id()),
      testing::UnorderedElementsAre(
          GetKeepaliveMatcher(listener_worker_id, Activity::MESSAGE_PORT)));

  // Close the incognito tab while the message channel is still open. Since
  // this is the only browser window for the incognito context, this also
  // results in the browser context being invalidated.
  // As part of this, the keepalives are removed for the extensions, which
  // can trigger an attempted removal of an external request from the
  // service worker layer. Since the context is being shut down, this can
  // fail with `content::ServiceWorkerExternalRequestResult::kNullContext`. This
  // is fine, since the whole context is going away.
  // See https://crbug.com/40070712.
  ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
  CloseTabForWebContents(incognito_contents);
  profile_destruction_waiter.Wait();
  // Note: `ProfileDestructionWaiter` only waits for the profile to signal it
  // *will* be destroyed. Spin once to finish the job.
  base::RunLoop().RunUntilIdle();
  // Verify the profile is destroyed.
  EXPECT_FALSE(
      ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));
  // The test succeeds if there are no crashes. There's nothing left to verify
  // for keepalives, since the profile is gone.
}

// Tests that we can safely shut down a BrowserContext when an extension has an
// active keepalive for a service worker that spans between the on- and off-
// the-record profiles between two different extensions.
IN_PROC_BROWSER_TEST_F(
    ServiceWorkerLifetimeKeepaliveBrowsertest,
    ShutdownWithActiveMessagePipe_BetweenExtensionsInDifferentContexts) {
  // A split-mode extension. This will have a separate process for the on- and
  // off-the-record profiles.
  static constexpr char kSplitModeManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "split",
           "background": {"service_worker": "background.js"}
         })";
  static constexpr char kSplitBackgroundJs[] = R"(// Intentionally blank.)";

  // A spanning mode extension. This will share a process between the on- and
  // off-the-record profiles.
  static constexpr char kSpanningManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "spanning",
           "background": {"service_worker": "background.js"}
         })";

  // The spanning mode extension listens for an external message (from the
  // split mode extension). We save the `sendReply` callback so it's not garbage
  // collected and keeps the message pipe open, and then asynchronously respond
  // that the message was received.
  // The asynchronous response is important in order to ensure the message being
  // received from this extension is properly ack'd in the renderer before the
  // script result is received.
  static constexpr char kSpanningModeBackgroundJs[] =
      R"(chrome.runtime.onMessageExternal.addListener(
             (msg, sender, sendReply) => {
               self.sendReply = sendReply;
               setTimeout(
                   () => { chrome.test.sendScriptResult('success'); }, 0);
               return true;
             });)";

  TestExtensionDir split_mode_dir;
  split_mode_dir.WriteManifest(kSplitModeManifest);
  split_mode_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                           kSplitBackgroundJs);

  TestExtensionDir spanning_mode_dir;
  spanning_mode_dir.WriteManifest(kSpanningManifest);
  spanning_mode_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                              kSpanningModeBackgroundJs);

  const Extension* split_mode_extension = LoadExtension(
      split_mode_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(split_mode_extension);
  const Extension* spanning_mode_extension = LoadExtension(
      spanning_mode_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(spanning_mode_extension);

  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  // Wait for the single worker from split_mode_extension.
  service_worker_test_utils::TestServiceWorkerTaskQueueObserver
      task_queue_observer;
  // Open a new tab in incognito. This spawns the new process for the split mode
  // extension.
  content::WebContents* incognito_contents = PlatformOpenURLOffTheRecord(
      profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
  task_queue_observer.WaitForWorkerContextInitialized(
      split_mode_extension->id());

  // Send a message to the spanning mode extension from the incognito context of
  // the split mode extension.
  // This will open a message pipe. Since the spanning mode extension never
  // responds, the message pipe will remain open.
  // The spanning mode extension will then send the script result "success" when
  // it receives the message.
  static constexpr char kOpenMessagePipe[] =
      R"((async () => {
           // Note: Pass a callback to signal a reply is expected.
           chrome.runtime.sendMessage('%s', 'hello', () => {});
         })();)";
  base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
      incognito_profile, split_mode_extension->id(),
      base::StringPrintf(kOpenMessagePipe,
                         spanning_mode_extension->id().c_str()),
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("success", script_result);

  ProcessManager* process_manager = ProcessManager::Get(profile());
  ProcessManager* incognito_process_manager =
      ProcessManager::Get(incognito_profile);

  std::vector<WorkerId> worker_ids =
      process_manager->GetServiceWorkersForExtension(
          spanning_mode_extension->id());
  ASSERT_EQ(1u, worker_ids.size());
  WorkerId spanning_worker_id = worker_ids[0];

  worker_ids = incognito_process_manager->GetServiceWorkersForExtension(
      split_mode_extension->id());
  ASSERT_EQ(1u, worker_ids.size());
  WorkerId split_incognito_worker_id = worker_ids[0];

  // Verify the current keepalives for the extensions.
  // Each extension should have exactly one keepalive (the active message
  // port). However, the context in which the keepalive is present is
  // different:
  // - The spanning mode extension should have a keepalive in the on-the-record
  //   context, and not in the incognito context (where it's not running).
  // - The split mode extension should have a keepalive in the incognito
  //   context, but not the on-the-record context (since the message pipe is
  //   with the incognito version).

  EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
                  spanning_mode_extension->id()),
              testing::UnorderedElementsAre(GetKeepaliveMatcher(
                  spanning_worker_id, Activity::MESSAGE_PORT)));
  EXPECT_THAT(
      incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
          spanning_mode_extension->id()),
      testing::IsEmpty());

  EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
                  split_mode_extension->id()),
              testing::IsEmpty());
  EXPECT_THAT(
      incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
          split_mode_extension->id()),
      testing::UnorderedElementsAre(GetKeepaliveMatcher(
          split_incognito_worker_id, Activity::MESSAGE_PORT)));

  // Verify the active external request count to validate the above.
  EXPECT_EQ(1u, GetExternalRequestCountForWorker(*profile(),
                                                 *spanning_mode_extension));
  EXPECT_EQ(0u, GetExternalRequestCountForWorker(*incognito_profile,
                                                 *spanning_mode_extension));
  EXPECT_EQ(
      0u, GetExternalRequestCountForWorker(*profile(), *split_mode_extension));
  EXPECT_EQ(1u, GetExternalRequestCountForWorker(*incognito_profile,
                                                 *split_mode_extension));

  // Close the incognito tab while the message channel is still open. Since
  // this is the only browser window for the incognito context, this also
  // results in the browser context being invalidated.
  ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
  CloseTabForWebContents(incognito_contents);
  profile_destruction_waiter.Wait();
  // Note: `ProfileDestructionWaiter` only waits for the profile to signal it
  // *will* be destroyed. Spin once to finish the job.
  base::RunLoop().RunUntilIdle();

  // Verify the profile is destroyed.
  EXPECT_FALSE(
      ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));

  // Verify that all keepalives have been removed, since the message port was
  // closed as part of the incognito profile shutdown. (We can't verify
  // the incognito values since the incognito profile is destroyed.)
  EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
                  spanning_mode_extension->id()),
              testing::IsEmpty());
  EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
                  split_mode_extension->id()),
              testing::IsEmpty());
  EXPECT_EQ(0u, GetExternalRequestCountForWorker(*profile(),
                                                 *spanning_mode_extension));
  EXPECT_EQ(
      0u, GetExternalRequestCountForWorker(*profile(), *split_mode_extension));
}

#if !BUILDFLAG(IS_ANDROID)
// Isolated web app tests require code from //chrome/browser/web_applications
// that isn't supported on Android.
class IWAServiceWorkerLifetimeKeepaliveBrowsertest
    : public ServiceWorkerLifetimeKeepaliveBrowsertest {
 public:
  IWAServiceWorkerLifetimeKeepaliveBrowsertest() {
    iwa_scoped_feature_list_.InitWithFeatures(
        {features::kIsolatedWebAppDevMode, features::kIsolatedWebApps}, {});
  }

  web_app::IsolatedWebAppUrlInfo InstallWebApp() {
    const std::unique_ptr<web_app::BundledIsolatedWebApp> bundle =
        web_app::IsolatedWebAppBuilder(web_app::ManifestBuilder())
            .BuildBundle();
    return bundle->InstallChecked(browser()->GetProfile());
  }

  void ConnectToReceiverExtension(content::RenderFrameHost* app_frame) {
    constexpr static std::string_view kScript = R"(
          chrome.runtime.connect($1);
        )";
    CHECK(ExecJs(app_frame,
                 content::JsReplace(kScript, kTestReceiverExtensionId)));
  }

  base::test::ScopedFeatureList iwa_scoped_feature_list_;
  web_app::OsIntegrationTestOverrideBlockingRegistration faked_os_integration_;
};

// Load IWA and an extension that open a persistent port connection between each
// other and tests that extension service worker will stop after kRequestTimeout
// (5 minutes).
IN_PROC_BROWSER_TEST_F(IWAServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersTimeOutWithoutPolicy) {
  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  // 1. Receiver extension.
  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerStarted();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  // 2. Opener IWA.
  web_app::IsolatedWebAppUrlInfo opener_url_info = InstallWebApp();
  content::RenderFrameHost* opener_frame =
      web_app::OpenIsolatedWebApp(profile(), opener_url_info.app_id());
  ConnectToReceiverExtension(opener_frame);

  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  // Advance clock and check that the receiver service worker stopped.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);
  sw_observer_receiver_extension.WaitForWorkerStopped();
}

// Tests that the service workers will not stop between extension and IWA if IWA
// is allowlisted via policy and the port is not closed.
IN_PROC_BROWSER_TEST_F(IWAServiceWorkerLifetimeKeepaliveBrowsertest,
                       ServiceWorkersDoNotTimeOutWithPolicy) {
  web_app::IsolatedWebAppUrlInfo opener_url_info = InstallWebApp();

  base::ListValue urls;
  // Only the receiver must get extended lifetime, because IWA
  // is kept opened.
  urls.Append(opener_url_info.origin().Serialize());
  profile()->GetPrefs()->SetList(
      pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
      std::move(urls));

  content::ServiceWorkerContext* context = GetServiceWorkerContext();

  // Load the extensions and wait for the service workers to be activated. This
  // test advances the worker's clock. If the activation request is in-flight
  // when the clock is advanced, the request will expire and the worker will be
  // terminated (because activation requests have KILL_ON_TIMEOUT behavior).
  // Thus, we ensure that there are no in-flight activation requests before
  // advancing the clock.
  TestServiceWorkerContextObserver sw_observer_receiver_extension(
      context, kTestReceiverExtensionId);
  LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
  const int64_t service_worker_receiver_id =
      sw_observer_receiver_extension.WaitForWorkerActivated();

  ExtensionTestMessageListener connect_listener(
      kPersistentPortConnectedMessage);
  connect_listener.set_extension_id(kTestReceiverExtensionId);

  content::RenderFrameHost* opener_frame =
      web_app::OpenIsolatedWebApp(profile(), opener_url_info.app_id());
  ConnectToReceiverExtension(opener_frame);
  ASSERT_TRUE(connect_listener.WaitUntilSatisfied());

  // Advance clock and check that the receiver service worker did not stop.
  content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
                                           &tick_clock_receiver_);
  TriggerTimeoutAndCheckActive(context, service_worker_receiver_id);
}
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace extensions
