chromium/chrome/browser/ash/file_suggest/file_suggest_keyed_service_browsertest.cc

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

#include "ash/constants/ash_features.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/drive/drive_integration_service_browser_test_base.h"
#include "chrome/browser/ash/file_suggest/drive_file_suggestion_provider.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service_factory.h"
#include "chrome/browser/ash/file_suggest/file_suggest_test_util.h"
#include "chrome/browser/ash/file_suggest/file_suggest_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chromeos/ash/components/drivefs/fake_drivefs.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "content/public/test/browser_test.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"

using ::testing::_;
using ::testing::Field;
using ::testing::Not;
using ::testing::Pointee;

namespace ash::test {
namespace {

class FakeFailedSearchQuery : public drivefs::mojom::SearchQuery {
 public:
  FakeFailedSearchQuery() = default;
  FakeFailedSearchQuery(const FakeFailedSearchQuery&) = delete;
  FakeFailedSearchQuery& operator=(const FakeFailedSearchQuery&) = delete;
  ~FakeFailedSearchQuery() override = default;

  void GetNextPage(GetNextPageCallback callback) override {
    std::move(callback).Run(drive::FILE_ERROR_FAILED, {});
  }
};

class FakeSearchQuery : public drivefs::mojom::SearchQuery {
 public:
  FakeSearchQuery() = default;
  explicit FakeSearchQuery(std::vector<drivefs::mojom::QueryItemPtr> results)
      : results_(std::move(results)) {}

  FakeSearchQuery(const FakeSearchQuery&) = delete;
  FakeSearchQuery& operator=(const FakeSearchQuery&) = delete;
  ~FakeSearchQuery() override = default;

  void GetNextPage(GetNextPageCallback callback) override {
    if (next_page_called_) {
      std::move(callback).Run(drive::FILE_ERROR_OK, {});
      return;
    }
    next_page_called_ = true;
    std::move(callback).Run(drive::FILE_ERROR_OK, std::move(results_));
  }

 private:
  std::vector<drivefs::mojom::QueryItemPtr> results_;
  bool next_page_called_ = false;
};

class MockObserver : public FileSuggestKeyedService::Observer {
 public:
  explicit MockObserver(FileSuggestKeyedService* file_suggest_service)
      : file_suggest_service_(file_suggest_service) {
    file_suggest_service_observation_.Observe(file_suggest_service_.get());
  }
  MockObserver(const MockObserver&) = delete;
  MockObserver& operator=(const MockObserver&) = delete;
  ~MockObserver() override = default;

  void WaitUntilFetchingSuggestData() { run_loop_.Run(); }

  // Returns the most recently fetched suggest data.
  const std::optional<std::vector<FileSuggestData>>& last_fetched_data() const {
    return last_fetched_data_;
  }

 private:
  void OnSuggestFileDataFetched(
      const std::optional<std::vector<FileSuggestData>>& suggest_data_array) {
    last_fetched_data_ = suggest_data_array;
    run_loop_.Quit();
  }

  // FileSuggestKeyedService::Observer:
  void OnFileSuggestionUpdated(FileSuggestionType type) override {
    EXPECT_EQ(FileSuggestionType::kDriveFile, type);
    file_suggest_service_->GetSuggestFileData(
        type, base::BindOnce(&MockObserver::OnSuggestFileDataFetched,
                             base::Unretained(this)));
  }

  const raw_ptr<FileSuggestKeyedService> file_suggest_service_;
  base::RunLoop run_loop_;
  std::optional<std::vector<FileSuggestData>> last_fetched_data_;
  base::ScopedObservation<FileSuggestKeyedService,
                          FileSuggestKeyedService::Observer>
      file_suggest_service_observation_{this};
};

}  // namespace

class FileSuggestKeyedServiceBrowserTest
    : public drive::DriveIntegrationServiceBrowserTestBase,
      public ::testing::WithParamInterface<bool> {
 public:
  FileSuggestKeyedServiceBrowserTest() {
    scoped_feature_list_.InitWithFeatureState(
        ash::features::kLauncherContinueSectionWithRecentsRollout,
        UseDriveRecents());
  }
  // drive::DriveIntegrationServiceBrowserTestBase:
  void SetUpOnMainThread() override {
    drive::DriveIntegrationServiceBrowserTestBase::SetUpOnMainThread();
    Profile* profile = browser()->profile();

    WaitUntilFileSuggestServiceReady(
        FileSuggestKeyedServiceFactory::GetInstance()->GetService(profile));

    InitTestFileMountRoot(profile);

    if (UseDriveRecents()) {
      ON_CALL(*GetFakeDriveFsForProfile(profile), StartSearchQuery(_, _))
          .WillByDefault([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                                 pending_receiver,
                             drivefs::mojom::QueryParametersPtr query_params) {
            auto search_query = std::make_unique<FakeSearchQuery>();
            mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                        std::move(pending_receiver));
          });
      // Flush any drive FS search requests that may have been initialized in
      // response to Drive FS, and FileSuggestKeyedService initialization, so
      // they don't interfere with the test flow.
      FlushDriveFsSearch();
    }

    // Add two drive files.
    const std::string file_id1("abc123");
    available_files_.push_back(file_id1);
    base::FilePath absolute_file_path;
    AddDriveFileWithRelativePath(profile, file_id1, base::FilePath(""),
                                 /*new_file_relative_path=*/nullptr,
                                 &absolute_file_path);
    file_paths_[file_id1] = absolute_file_path;

    const std::string file_id2("qwertyqwerty");
    available_files_.push_back(file_id2);
    AddDriveFileWithRelativePath(profile, file_id2, base::FilePath(""),
                                 /*new_file_relative_path=*/nullptr,
                                 &absolute_file_path);
    file_paths_[file_id2] = absolute_file_path;
  }

  drivefs::mojom::QueryItemPtr CreateQueryItemForTestFile(
      const std::string& file_id,
      base::Time timestamp) {
    const base::FilePath absolute_path = GetTestFilePath(file_id);
    base::FilePath drive_path;
    if (!drive::DriveIntegrationServiceFactory::FindForProfile(
             browser()->profile())
             ->GetRelativeDrivePath(absolute_path, &drive_path)) {
      return drivefs::mojom::QueryItemPtr();
    }

    auto result = drivefs::mojom::QueryItem::New();
    result->path = drive_path;
    result->metadata = drivefs::mojom::FileMetadata::New();
    result->metadata->modification_time = timestamp;
    result->metadata->modified_by_me_time = timestamp;
    result->metadata->last_viewed_by_me_time = timestamp;
    result->metadata->capabilities = drivefs::mojom::Capabilities::New();
    return result;
  }

  bool UseDriveRecents() const { return GetParam(); }

  const std::vector<std::string>& available_files() const {
    return available_files_;
  }

  base::FilePath GetTestFilePath(const std::string& file_id) const {
    const auto it = file_paths_.find(file_id);
    if (it == file_paths_.end()) {
      return base::FilePath();
    }
    return it->second;
  }

  void NotifyFilesCreated(const std::vector<std::string>& file_ids) {
    std::vector<drivefs::mojom::FileChangePtr> changes;
    Profile* const profile = browser()->profile();
    for (const auto& file_id : file_ids) {
      base::FilePath drive_path("/");
      base::FilePath absolute_path = GetTestFilePath(file_id);
      EXPECT_FALSE(absolute_path.empty());
      EXPECT_TRUE(drive::DriveIntegrationServiceFactory::FindForProfile(profile)
                      ->GetMountPointPath()
                      .AppendRelativePath(absolute_path, &drive_path));

      auto change = drivefs::mojom::FileChange::New();
      change->path = drive_path;
      change->type = drivefs::mojom::FileChange::Type::kCreate;
      changes.push_back(std::move(change));
    }

    // Simulate the `changes` being sent from the server.
    drivefs_delegate()->OnFilesChanged(std::move(changes));
    drivefs_delegate().FlushForTesting();
  }

  void FlushDriveFsSearch() {
    base::RunLoop suggest_file_data_waiter;
    FileSuggestKeyedService* const service =
        FileSuggestKeyedServiceFactory::GetInstance()->GetService(
            browser()->profile());
    service->GetSuggestFileData(
        FileSuggestionType::kDriveFile,
        base::BindLambdaForTesting(
            [&](const std::optional<std::vector<FileSuggestData>>&
                    suggest_data) { suggest_file_data_waiter.Quit(); }));
    suggest_file_data_waiter.Run();
  }

  mojo::Remote<drivefs::mojom::DriveFsDelegate>& drivefs_delegate() {
    return GetFakeDriveFsForProfile(browser()->profile())->delegate();
  }

 private:
  // IDs of files added to fake file system.
  std::vector<std::string> available_files_;

  // Maps a test file added during test setup ID to the associated absolute file
  // path.
  std::map<std::string, base::FilePath> file_paths_;

  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(UseDriveRecents,
                         FileSuggestKeyedServiceBrowserTest,
                         ::testing::Bool());

// Verifies that the file suggest keyed service works as expected when the item
// suggest cache is empty.
IN_PROC_BROWSER_TEST_P(FileSuggestKeyedServiceBrowserTest,
                       QueryWithEmptyCache) {
  // TODO(http://b/349164737): Re-enable this test with forest feature enabled.
  if (ash::features::IsForestFeatureEnabled() && !UseDriveRecents()) {
    GTEST_SKIP() << "Skipping test body for Forest Feature enabled and Drive "
                    "Recents disabled.";
  }

  base::HistogramTester histogram_tester;

  auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
  if (UseDriveRecents()) {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _))
        .Times(3)
        .WillRepeatedly([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                                pending_receiver,
                            drivefs::mojom::QueryParametersPtr query_params) {
          auto search_query = std::make_unique<FakeSearchQuery>();
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        });
    // Invalidate cached suggestions by notifying that new files are present.
    NotifyFilesCreated({});
  } else {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _)).Times(0);
  }

  base::RunLoop suggest_file_data_waiter;

  FileSuggestKeyedService* service =
      FileSuggestKeyedServiceFactory::GetInstance()->GetService(
          browser()->profile());
  service->GetSuggestFileData(
      FileSuggestionType::kDriveFile,
      base::BindLambdaForTesting(
          [&](const std::optional<std::vector<FileSuggestData>>& suggest_data) {
            EXPECT_EQ(UseDriveRecents(), suggest_data.has_value());
            if (UseDriveRecents()) {
              EXPECT_EQ(0u, suggest_data->size());
            }
            suggest_file_data_waiter.Quit();
          }));

  suggest_file_data_waiter.Run();

  if (UseDriveRecents()) {
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.ItemCount.Total", 0, 1);
  } else {
    histogram_tester.ExpectBucketCount(
        "Ash.Search.DriveFileSuggestDataValidation.Status",
        /*sample=*/DriveSuggestValidationStatus::kNoResults,
        /*expected_count=*/1);
  }
}

// Verifies that the file suggest keyed service responds to the update in
// the item suggest cache correctly.
IN_PROC_BROWSER_TEST_P(FileSuggestKeyedServiceBrowserTest,
                       RespondToItemSuggestCacheUpdate) {
  // TODO(http://b/349164737): Re-enable this test with forest feature enabled.
  if (ash::features::IsForestFeatureEnabled() && !UseDriveRecents()) {
    GTEST_SKIP() << "Skipping test body for Forest Feature enabled and Drive "
                    "Recents disabled.";
  }
  base::HistogramTester histogram_tester;

  Profile* profile = browser()->profile();
  FileSuggestKeyedService* service =
      FileSuggestKeyedServiceFactory::GetInstance()->GetService(profile);

  MockObserver observer(service);

  ASSERT_GE(available_files().size(), 2u);

  auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
  if (UseDriveRecents()) {
    EXPECT_CALL(
        *fake_drivefs,
        StartSearchQuery(
            _,
            Not(Pointee(Field(
                &drivefs::mojom::QueryParameters::sort_field,
                drivefs::mojom::QueryParameters::SortField::kSharedWithMe)))))
        .Times(2)
        .WillRepeatedly([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                                pending_receiver,
                            drivefs::mojom::QueryParametersPtr query_params) {
          std::vector<drivefs::mojom::QueryItemPtr> results;
          results.push_back(CreateQueryItemForTestFile(available_files()[0],
                                                       base::Time::Now()));
          results.push_back(CreateQueryItemForTestFile(
              available_files()[1], base::Time::Now() - base::Seconds(1)));
          auto search_query =
              std::make_unique<FakeSearchQuery>(std::move(results));
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        })
        .RetiresOnSaturation();
    EXPECT_CALL(
        *fake_drivefs,
        StartSearchQuery(
            _, Pointee(Field(
                   &drivefs::mojom::QueryParameters::sort_field,
                   drivefs::mojom::QueryParameters::SortField::kSharedWithMe))))
        .WillOnce([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                          pending_receiver,
                      drivefs::mojom::QueryParametersPtr query_params) {
          auto search_query = std::make_unique<FakeSearchQuery>();
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        });
    // Invalidate cached suggestions by notifying that new files are present.
    NotifyFilesCreated({available_files()[0], available_files()[1]});
  } else {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _)).Times(0);

    // Update the item suggest cache with two file ids: one is valid and the
    // other is not.
    std::string json_string = CreateItemSuggestUpdateJsonString(
        {{available_files()[0], "display text 1", "You edited · just now"},
         {available_files()[1], "display text 2", "You edited · just now"}},
        "suggestion id 1");
    DriveFileSuggestionProvider* file_suggestion_provider =
        static_cast<DriveFileSuggestionProvider*>(
            service->drive_file_suggestion_provider_for_test());
    file_suggestion_provider->item_suggest_cache_for_test()
        ->UpdateCacheWithJsonForTest(json_string);
    observer.WaitUntilFetchingSuggestData();
  }

  base::RunLoop suggest_file_data_waiter;
  service->GetSuggestFileData(
      FileSuggestionType::kDriveFile,
      base::BindLambdaForTesting(
          [&](const std::optional<std::vector<FileSuggestData>>& suggest_data) {
            ASSERT_TRUE(suggest_data.has_value());
            EXPECT_EQ(2u, suggest_data->size());
            if (suggest_data->size() < 2u) {
              suggest_file_data_waiter.Quit();
              return;
            }

            const auto& item1 = (*suggest_data)[0];
            EXPECT_EQ(GetTestFilePath(available_files()[0]), item1.file_path);
            EXPECT_EQ(u"You edited · just now", item1.prediction_reason);

            const auto& item2 = (*suggest_data)[1];
            EXPECT_EQ(GetTestFilePath(available_files()[1]), item2.file_path);
            EXPECT_EQ(u"You edited · just now", item2.prediction_reason);

            suggest_file_data_waiter.Quit();
          }));
  suggest_file_data_waiter.Run();

  const auto& fetched_data = observer.last_fetched_data();
  EXPECT_TRUE(fetched_data.has_value());
  ASSERT_EQ(2u, fetched_data->size());

  const auto& item1 = (*fetched_data)[0];
  EXPECT_EQ(GetTestFilePath(available_files()[0]), item1.file_path);
  EXPECT_EQ(u"You edited · just now", item1.prediction_reason);

  const auto& item2 = (*fetched_data)[1];
  EXPECT_EQ(GetTestFilePath(available_files()[1]), item2.file_path);
  EXPECT_EQ(u"You edited · just now", item2.prediction_reason);

  if (UseDriveRecents()) {
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Viewed", 0, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Modified", 0, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Shared", 0, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.ItemCount.Total", 2, 1);
  } else {
    // Note that the `observer` calls `GetSuggestFileData()` when the item
    // cache is updated, so the file suggest data gets processed twice.
    histogram_tester.ExpectBucketCount(
        "Ash.Search.DriveFileSuggestDataValidation.Status",
        /*sample=*/DriveSuggestValidationStatus::kOk,
        /*expected_count=*/2);
  }
}

// Verifies that the file suggest keyed service responds to the update in
// the item suggest cache correctly when item fetch fails.
IN_PROC_BROWSER_TEST_P(FileSuggestKeyedServiceBrowserTest,
                       RespondToItemSuggestCacheInvalidUpdate) {
  // TODO(http://b/349164737): Re-enable this test with forest feature enabled.
  if (ash::features::IsForestFeatureEnabled() && !UseDriveRecents()) {
    GTEST_SKIP() << "Skipping test body for Forest Feature enabled and Drive "
                    "Recents disabled.";
  }

  base::HistogramTester histogram_tester;

  Profile* profile = browser()->profile();
  FileSuggestKeyedService* service =
      FileSuggestKeyedServiceFactory::GetInstance()->GetService(profile);

  // Ensure that `observer` exists before updating the suggest cache. Because
  // notifying the observer of the suggest cache update is synchronous.
  MockObserver observer(service);

  auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
  if (UseDriveRecents()) {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _))
        .Times(3)
        .WillRepeatedly([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                                pending_receiver,
                            drivefs::mojom::QueryParametersPtr query_params) {
          auto search_query = std::make_unique<FakeFailedSearchQuery>();
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        });
    NotifyFilesCreated({});
  } else {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _)).Times(0);

    // Update the item suggest cache with a non-existed file id.
    DriveFileSuggestionProvider* file_suggestion_provider =
        static_cast<DriveFileSuggestionProvider*>(
            service->drive_file_suggestion_provider_for_test());
    file_suggestion_provider->item_suggest_cache_for_test()
        ->UpdateCacheWithJsonForTest(CreateItemSuggestUpdateJsonString(
            {{"unknown", "display text 1", "prediction reason 1"}},
            "suggestion id 0"));
    observer.WaitUntilFetchingSuggestData();
    const auto& fetched_data = observer.last_fetched_data();
    EXPECT_FALSE(fetched_data.has_value());
  }

  base::RunLoop suggest_file_data_waiter;
  service->GetSuggestFileData(
      FileSuggestionType::kDriveFile,
      base::BindLambdaForTesting(
          [&](const std::optional<std::vector<FileSuggestData>>& suggest_data) {
            EXPECT_EQ(UseDriveRecents(), suggest_data.has_value());
            if (UseDriveRecents()) {
              EXPECT_EQ(0u, suggest_data->size());
            }
            suggest_file_data_waiter.Quit();
          }));

  suggest_file_data_waiter.Run();

  if (UseDriveRecents()) {
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Viewed", 1, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Modified", 1, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Shared", 1, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.ItemCount.Total", 0, 1);
  } else {
    // Note that the `observer` calls `GetSuggestFileData()` when the item cache
    // is updated, so the file suggest data gets processed twice.
    histogram_tester.ExpectBucketCount(
        "Ash.Search.DriveFileSuggestDataValidation.Status",
        /*sample=*/DriveSuggestValidationStatus::kAllFilesErrored,
        /*expected_count=*/2);
  }
}

// Verifies that the file suggest keyed service responds to the update in
// the item suggest cache correctly if some item fetches fail.
IN_PROC_BROWSER_TEST_P(FileSuggestKeyedServiceBrowserTest,
                       RespondToItemSuggestCachePartiallyInvalidUpdate) {
  // TODO(http://b/349164737): Re-enable this test with forest feature enabled.
  if (ash::features::IsForestFeatureEnabled() && !UseDriveRecents()) {
    GTEST_SKIP() << "Skipping test body for Forest Feature enabled and Drive "
                    "Recents disabled.";
  }

  base::HistogramTester histogram_tester;

  Profile* profile = browser()->profile();
  FileSuggestKeyedService* service =
      FileSuggestKeyedServiceFactory::GetInstance()->GetService(profile);

  MockObserver observer(service);

  ASSERT_GE(available_files().size(), 1u);
  const std::string file_id = available_files()[0];

  auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
  if (UseDriveRecents()) {
    EXPECT_CALL(
        *fake_drivefs,
        StartSearchQuery(
            _, Pointee(Field(
                   &drivefs::mojom::QueryParameters::sort_field,
                   drivefs::mojom::QueryParameters::SortField::kLastModified))))
        .WillOnce([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                          pending_receiver,
                      drivefs::mojom::QueryParametersPtr query_params) {
          std::vector<drivefs::mojom::QueryItemPtr> results;
          results.push_back(
              CreateQueryItemForTestFile(file_id, base::Time::Now()));
          auto search_query =
              std::make_unique<FakeSearchQuery>(std::move(results));
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        })
        .RetiresOnSaturation();
    EXPECT_CALL(
        *fake_drivefs,
        StartSearchQuery(
            _,
            Not(Pointee(Field(
                &drivefs::mojom::QueryParameters::sort_field,
                drivefs::mojom::QueryParameters::SortField::kLastModified)))))
        .Times(2)
        .WillRepeatedly([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                                pending_receiver,
                            drivefs::mojom::QueryParametersPtr query_params) {
          auto search_query = std::make_unique<FakeFailedSearchQuery>();
          mojo::MakeSelfOwnedReceiver(std::move(search_query),
                                      std::move(pending_receiver));
        });

    // Invalidate cached suggestions by notifying that new files are present.
    NotifyFilesCreated({file_id});
  } else {
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _)).Times(0);

    // Update the item suggest cache with two file ids: one is valid and the
    // other is not.
    std::string json_string = CreateItemSuggestUpdateJsonString(
        {{file_id, "display text 1", "You edited · just now"},
         {"unknown", "display text 2", "prediction reason 2"}},
        "suggestion id 1");
    DriveFileSuggestionProvider* file_suggestion_provider =
        static_cast<DriveFileSuggestionProvider*>(
            service->drive_file_suggestion_provider_for_test());
    file_suggestion_provider->item_suggest_cache_for_test()
        ->UpdateCacheWithJsonForTest(json_string);
    observer.WaitUntilFetchingSuggestData();

    const auto& fetched_data = observer.last_fetched_data();
    EXPECT_TRUE(fetched_data.has_value());
    EXPECT_EQ(1u, fetched_data->size());
    EXPECT_EQ(GetTestFilePath(file_id), fetched_data->at(0).file_path);
    EXPECT_EQ(u"You edited · just now", *fetched_data->at(0).prediction_reason);
  }

  base::RunLoop suggest_file_data_waiter;
  service->GetSuggestFileData(
      FileSuggestionType::kDriveFile,
      base::BindLambdaForTesting(
          [&](const std::optional<std::vector<FileSuggestData>>& suggest_data) {
            ASSERT_TRUE(suggest_data.has_value());
            EXPECT_EQ(1u, suggest_data->size());
            if (suggest_data->size() < 1u) {
              suggest_file_data_waiter.Quit();
              return;
            }

            const auto& item = (*suggest_data)[0];
            EXPECT_EQ(GetTestFilePath(file_id), item.file_path);
            EXPECT_EQ(u"You edited · just now", item.prediction_reason);

            suggest_file_data_waiter.Quit();
          }));
  suggest_file_data_waiter.Run();

  if (UseDriveRecents()) {
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Viewed", 1, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Modified", 0, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.QueryResult.Shared", 1, 1);
    histogram_tester.ExpectUniqueSample(
        "Ash.Search.FileSuggestions.DriveRecents.ItemCount.Total", 1, 1);
  } else {
    // Note that the `observer` calls `GetSuggestFileData()` when the item cache
    // is updated, so the file suggest data gets processed twice.
    histogram_tester.ExpectBucketCount(
        "Ash.Search.DriveFileSuggestDataValidation.Status",
        /*sample=*/DriveSuggestValidationStatus::kOk,
        /*expected_count=*/2);
  }
}

}  // namespace ash::test