chromium/components/manta/sparky/sparky_util_unittest.cc

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

#include "components/manta/sparky/sparky_util.h"

#include <memory>
#include <optional>

#include "base/memory/ptr_util.h"
#include "base/test/task_environment.h"
#include "components/manta/proto/sparky.pb.h"
#include "components/manta/sparky/sparky_delegate.h"
#include "components/manta/sparky/system_info_delegate.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace manta {

class SparkyUtilTest : public testing::Test {
 public:
  SparkyUtilTest() = default;

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

  ~SparkyUtilTest() override = default;

 protected:
  base::test::TaskEnvironment task_environment_;

  bool IsSameSetting(const proto::Setting& proto_setting,
                     const SettingsData* settings_data) {
    if (!settings_data->val_set) {
      return false;
    }
    if (proto_setting.settings_id() == settings_data->pref_name &&
        proto_setting.has_value()) {
      if (proto_setting.type() == proto::SETTING_TYPE_BOOL) {
        return (proto_setting.value().has_bool_val() &&
                proto_setting.value().bool_val() == settings_data->bool_val);
      } else if (proto_setting.type() == proto::SETTING_TYPE_STRING) {
        return (
            proto_setting.value().has_text_val() &&
            (proto_setting.value().text_val() == settings_data->string_val));
      } else if (proto_setting.type() == proto::SETTING_TYPE_INTEGER) {
        return (proto_setting.value().has_int_val() &&
                (proto_setting.value().int_val() == settings_data->int_val));
      } else if (proto_setting.type() == proto::SETTING_TYPE_DOUBLE) {
        EXPECT_DOUBLE_EQ(proto_setting.value().double_val(),
                         settings_data->double_val);
        return true;
      }
    }
    return false;  // Settings do not match.
  }

  bool ContainsSetting(
      const google::protobuf::RepeatedPtrField<proto::Setting>& repeatedField,
      SettingsData* settings_data) {
    for (const proto::Setting& proto_setting : repeatedField) {
      if (IsSameSetting(proto_setting, settings_data)) {
        return true;
      }
    }
    return false;  // Did not find the setting.
  }

  bool ContainsApp(
      const google::protobuf::RepeatedPtrField<proto::App>& repeated_field,
      std::string_view name,
      std::string_view id) {
    for (const proto::App& proto_app : repeated_field) {
      if (proto_app.id() == id && proto_app.name() == name) {
        return true;
      }
    }
    return false;
  }

  bool ContainsAction(const proto::Action& action_proto,
                      std::vector<Action>* actions) {
    for (const Action& action : *actions) {
      if (action.updated_setting.has_value() &&
          action_proto.has_update_setting()) {
        if (IsSameSetting(action_proto.update_setting(),
                          action.updated_setting.has_value()
                              ? &action.updated_setting.value()
                              : nullptr)) {
          return true;
        }
      } else if (action.type == ActionType::kLaunchApp &&
                 action_proto.has_launch_app_id()) {
        if (action.launched_app == action_proto.launch_app_id()) {
          return true;
        }
      } else if (action_proto.has_all_done() &&
                 action.type == ActionType::kAllDone) {
        return action_proto.all_done() == action.all_done;
      } else if (action_proto.has_click() && action_proto.click().has_x_pos() &&
                 action_proto.click().has_y_pos() &&
                 action.type == ActionType::kClick) {
        return action_proto.click().x_pos() == action.click->x_pos &&
               action_proto.click().y_pos() == action.click->y_pos;
      } else if (action_proto.has_text_entry() &&
                 action_proto.text_entry().has_text() &&
                 action.type == ActionType::kTextEntry) {
        return action_proto.text_entry().text() == action.text_entry;
      } else if (action_proto.has_file_action() &&
                 action_proto.file_action().has_launch_file_path() &&
                 action.type == ActionType::kLaunchFile) {
        return action_proto.file_action().launch_file_path() ==
               action.file_action->launch_file_path;
      }
    }
    return false;
  }

  bool ContainsDialog(const ::google::protobuf::RepeatedPtrField<
                          ::manta::proto::Turn>& dialog_repeated,
                      const std::string& dialog,
                      Role role,
                      std::vector<Action>* actions) {
    for (const proto::Turn& proto_dialog : dialog_repeated) {
      if (proto_dialog.message() == dialog &&
          proto_dialog.role() == GetRole(role)) {
        if (actions) {
          if ((int)actions->size() != proto_dialog.action_size()) {
            return false;
          }
          auto actions_proto = proto_dialog.action();
          for (const proto::Action& action_proto : actions_proto) {
            if (!ContainsAction(action_proto, actions)) {
              return false;
            }
          }
        }
        return true;
      }
    }
    return false;
  }

  std::optional<proto::File> ObtainFileProto(
      const google::protobuf::RepeatedPtrField<proto::File>& repeated_field,
      std::string file_path) {
    for (const proto::File& proto_file : repeated_field) {
      if (proto_file.path() == file_path) {
        return std::make_optional(proto_file);
      }
    }
    return std::nullopt;
  }
};

TEST_F(SparkyUtilTest, AddSettingsProto) {
  auto current_prefs = SparkyDelegate::SettingsDataList();
  current_prefs["ash.dark_mode.enabled"] = std::make_unique<SettingsData>(
      "ash.dark_mode.enabled", PrefType::kBoolean,
      std::make_optional<base::Value>(true));
  current_prefs["string_pref"] = std::make_unique<SettingsData>(
      "string_pref", PrefType::kString,
      std::make_optional<base::Value>("my string"));
  current_prefs["int_pref"] = std::make_unique<SettingsData>(
      "int_pref", PrefType::kInt, std::make_optional<base::Value>(1));
  current_prefs["ash.night_light.enabled"] = std::make_unique<SettingsData>(
      "ash.night_light.enabled", PrefType::kBoolean,
      std::make_optional<base::Value>(false));
  current_prefs["ash.night_light.color_temperature"] =
      std::make_unique<SettingsData>("ash.night_light.color_temperature",
                                     PrefType::kDouble,
                                     std::make_optional<base::Value>(0.1));
  proto::SparkyContextData sparky_context_data;
  manta::proto::SettingsData* settings_data =
      sparky_context_data.mutable_settings_data();
  AddSettingsProto(current_prefs, settings_data);
  auto settings = settings_data->setting();
  ASSERT_EQ(settings_data->setting_size(), 5);
  ASSERT_TRUE(
      ContainsSetting(settings, current_prefs["ash.dark_mode.enabled"].get()));
  ASSERT_TRUE(ContainsSetting(settings, current_prefs["string_pref"].get()));
  ASSERT_TRUE(ContainsSetting(settings,
                              current_prefs["ash.night_light.enabled"].get()));
  ASSERT_TRUE(ContainsSetting(
      settings, current_prefs["ash.night_light.color_temperature"].get()));
}

TEST_F(SparkyUtilTest, AddDiagnosticsProto) {
  auto cpu_data = std::make_optional<CpuData>(40, 60, 5.0);
  auto memory_data = std::make_optional<MemoryData>(4.0, 8.0);
  auto battery_data =
      std::make_optional<BatteryData>(158, 76, "36 minutes until full", 80);
  auto storage_data = std::make_optional<manta::StorageData>("78 GB", "128 GB");
  std::optional<DiagnosticsData> diagnostics_data =
      std::make_optional<DiagnosticsData>(
          std::move(battery_data), std::move(cpu_data), std::move(memory_data),
          std::move(storage_data));
  proto::SparkyContextData sparky_context_data;
  auto* diagnostics_proto = sparky_context_data.mutable_diagnostics_data();
  AddDiagnosticsProto(std::move(diagnostics_data), diagnostics_proto);
  ASSERT_TRUE(diagnostics_proto->has_battery());
  ASSERT_TRUE(diagnostics_proto->has_cpu());
  ASSERT_TRUE(diagnostics_proto->has_memory());
  ASSERT_TRUE(diagnostics_proto->has_storage());

  ASSERT_DOUBLE_EQ(diagnostics_proto->cpu().clock_speed_ghz(), 5.0);
  ASSERT_EQ(diagnostics_proto->cpu().cpu_usage_snapshot(), 40);
  ASSERT_EQ(diagnostics_proto->cpu().temperature(), 60);
  ASSERT_DOUBLE_EQ(diagnostics_proto->memory().free_ram_gb(), 4.0);
  ASSERT_DOUBLE_EQ(diagnostics_proto->memory().total_ram_gb(), 8.0);
  ASSERT_EQ(diagnostics_proto->battery().battery_health(), 76);
  ASSERT_EQ(diagnostics_proto->battery().battery_charge_percentage(), 80);
  ASSERT_EQ(diagnostics_proto->battery().cycle_count(), 158);
  ASSERT_EQ(diagnostics_proto->battery().battery_time(),
            "36 minutes until full");
  ASSERT_EQ(diagnostics_proto->storage().free_storage(), "78 GB");
  ASSERT_EQ(diagnostics_proto->storage().total_storage(), "128 GB");
}

TEST_F(SparkyUtilTest, AddAppsData) {
  std::vector<manta::AppsData> apps_data;
  manta::AppsData app1 = AppsData("name1", "id1");
  apps_data.emplace_back(std::move(app1));
  manta::AppsData app2 = AppsData("name2", "id2");
  app2.AddSearchableText("search_term1");
  app2.AddSearchableText("search_term2");
  apps_data.emplace_back(std::move(app2));

  proto::SparkyContextData sparky_context_data;
  manta::proto::AppsData* apps_proto = sparky_context_data.mutable_apps_data();
  AddAppsData(std::move(apps_data), apps_proto);
  auto apps = apps_proto->app();
  ASSERT_EQ(apps_proto->app_size(), 2);
  ASSERT_TRUE(ContainsApp(apps, "name2", "id2"));
  ASSERT_TRUE(ContainsApp(apps, "name1", "id1"));
}

TEST_F(SparkyUtilTest, ObtainSettingFromProto) {
  proto::Setting bool_setting_proto;
  bool_setting_proto.set_type(proto::SETTING_TYPE_BOOL);
  bool_setting_proto.set_settings_id("power.adaptive_charging_enabled");
  auto* bool_settings_value = bool_setting_proto.mutable_value();
  bool_settings_value->set_bool_val(true);
  std::unique_ptr<SettingsData> bool_settings_data =
      ObtainSettingFromProto(bool_setting_proto);
  ASSERT_TRUE(IsSameSetting(bool_setting_proto, bool_settings_data.get()));

  proto::Setting int_setting_proto;
  int_setting_proto.set_type(proto::SETTING_TYPE_INTEGER);
  int_setting_proto.set_settings_id("ash.int.setting");
  auto* int_settings_value = int_setting_proto.mutable_value();
  int_settings_value->set_int_val(2);
  std::unique_ptr<SettingsData> int_settings_data =
      ObtainSettingFromProto(int_setting_proto);
  ASSERT_TRUE(IsSameSetting(int_setting_proto, int_settings_data.get()));

  proto::Setting double_setting_proto;
  double_setting_proto.set_type(proto::SETTING_TYPE_DOUBLE);
  double_setting_proto.set_settings_id("ash.night_light.color_temperature");
  auto* double_settings_value = double_setting_proto.mutable_value();
  double_settings_value->set_double_val(0.5);
  std::unique_ptr<SettingsData> double_settings_data =
      ObtainSettingFromProto(double_setting_proto);
  ASSERT_TRUE(IsSameSetting(double_setting_proto, double_settings_data.get()));

  proto::Setting string_setting_proto;
  string_setting_proto.set_type(proto::SETTING_TYPE_STRING);
  string_setting_proto.set_settings_id("ash.string.setting");
  auto* string_settings_value = string_setting_proto.mutable_value();
  string_settings_value->set_text_val("my string");
  std::unique_ptr<SettingsData> string_settings_data =
      ObtainSettingFromProto(string_setting_proto);
  ASSERT_TRUE(IsSameSetting(string_setting_proto, string_settings_data.get()));
}

TEST_F(SparkyUtilTest, ConvertDialogToStruct) {
  proto::Turn simple_turn;
  simple_turn.set_message("text question");
  simple_turn.set_role(proto::ROLE_USER);
  DialogTurn simple_dialog_reply = ConvertDialogToStruct(&simple_turn);
  ASSERT_EQ(simple_dialog_reply.message, simple_turn.message());
  ASSERT_EQ(GetRole(simple_dialog_reply.role), simple_turn.role());

  proto::Turn multi_step_turn;
  multi_step_turn.set_message("text answer");
  multi_step_turn.set_role(proto::ROLE_ASSISTANT);
  auto* open_app_action_proto = multi_step_turn.add_action();
  open_app_action_proto->set_launch_app_id("my app");
  auto* click_action_proto = multi_step_turn.add_action();
  auto* click_proto = click_action_proto->mutable_click();
  click_proto->set_x_pos(10);
  click_proto->set_y_pos(20);
  auto* text_entry_action_proto = multi_step_turn.add_action();
  auto* text_entry_proto = text_entry_action_proto->mutable_text_entry();
  text_entry_proto->set_text("text for my app");
  auto* file_action_proto = multi_step_turn.add_action();
  auto* open_file_proto = file_action_proto->mutable_file_action();
  open_file_proto->set_launch_file_path("/my/file/location");
  auto* all_done_proto = multi_step_turn.add_action();
  all_done_proto->set_all_done(false);
  DialogTurn open_app_dialog_turn = ConvertDialogToStruct(&multi_step_turn);
  ASSERT_EQ(open_app_dialog_turn.message, multi_step_turn.message());
  ASSERT_EQ(GetRole(open_app_dialog_turn.role), multi_step_turn.role());
  ASSERT_EQ((int)open_app_dialog_turn.actions.size(), 5);
  ASSERT_EQ(open_app_dialog_turn.actions.at(0).type, ActionType::kLaunchApp);
  ASSERT_EQ(open_app_dialog_turn.actions.at(0).launched_app, "my app");
  ASSERT_EQ(open_app_dialog_turn.actions.at(1).type, ActionType::kClick);
  ASSERT_EQ(open_app_dialog_turn.actions.at(1).click->x_pos, 10);
  ASSERT_EQ(open_app_dialog_turn.actions.at(1).click->y_pos, 20);
  ASSERT_EQ(open_app_dialog_turn.actions.at(2).type, ActionType::kTextEntry);
  ASSERT_EQ(open_app_dialog_turn.actions.at(2).text_entry, "text for my app");
  ASSERT_EQ(open_app_dialog_turn.actions.at(3).type, ActionType::kLaunchFile);
  ASSERT_TRUE(open_app_dialog_turn.actions.at(3).file_action.has_value());
  ASSERT_EQ(
      open_app_dialog_turn.actions.at(3).file_action.value().launch_file_path,
      "/my/file/location");
  ASSERT_EQ(open_app_dialog_turn.actions.at(4).type, ActionType::kAllDone);
  ASSERT_EQ(open_app_dialog_turn.actions.at(4).all_done, false);

  proto::Turn turn_with_settings;
  turn_with_settings.set_message("Adaptive charging has been enabled");
  turn_with_settings.set_role(proto::ROLE_ASSISTANT);
  auto* settings_action_proto = turn_with_settings.add_action();
  auto* setting_data = settings_action_proto->mutable_update_setting();
  setting_data->set_type(proto::SETTING_TYPE_BOOL);
  setting_data->set_settings_id("power.adaptive_charging_enabled");
  auto* settings_value = setting_data->mutable_value();
  settings_value->set_bool_val(true);
  auto* all_done_proto2 = turn_with_settings.add_action();
  all_done_proto2->set_all_done(true);
  DialogTurn dialog_reply_with_actions =
      ConvertDialogToStruct(&turn_with_settings);
  std::vector<Action> settings_actions;
  SettingsData setting_val =
      SettingsData("power.adaptive_charging_enabled", PrefType::kBoolean,
                   std::make_optional<base::Value>(true));
  settings_actions.emplace_back(setting_val);

  ASSERT_EQ(dialog_reply_with_actions.message, turn_with_settings.message());
  ASSERT_EQ(GetRole(dialog_reply_with_actions.role), turn_with_settings.role());
  ASSERT_EQ(dialog_reply_with_actions.actions.at(0).type, ActionType::kSetting);
  ASSERT_TRUE(
      dialog_reply_with_actions.actions.at(0).updated_setting.has_value());
  ASSERT_EQ(dialog_reply_with_actions.actions.at(0).updated_setting->bool_val,
            true);
  ASSERT_EQ(dialog_reply_with_actions.actions.at(1).type, ActionType::kAllDone);
  ASSERT_EQ(dialog_reply_with_actions.actions.at(1).all_done, true);
}

TEST_F(SparkyUtilTest, AddDialog) {
  std::vector<DialogTurn> dialog;
  dialog.emplace_back("Where is it?", Role::kUser);
  dialog.emplace_back("In Tokyo", Role::kAssistant);
  dialog.emplace_back("Turn on dark mode", Role::kUser);
  std::vector<Action> settings_actions;
  SettingsData setting_val =
      SettingsData("ash.dark_mode.enabled", PrefType::kBoolean,
                   std::make_optional<base::Value>(true));
  settings_actions.emplace_back(setting_val);
  dialog.emplace_back("Okay I have turned on dark mode", Role::kAssistant,
                      settings_actions);
  dialog.emplace_back("Create a new file with a poem about Platypuses",
                      Role::kUser);
  std::vector<Action> multi_step_actions;
  Action open_app_action = Action(ActionType::kLaunchApp);
  open_app_action.launched_app = "text app";
  multi_step_actions.emplace_back(open_app_action);
  Action click_action = Action(ClickAction(10, 20));
  multi_step_actions.emplace_back(click_action);
  Action type_action = Action(ActionType::kTextEntry);
  type_action.text_entry = "text for my app";
  multi_step_actions.emplace_back(type_action);
  multi_step_actions.emplace_back(false);
  dialog.emplace_back("Okay I have opened the text app", Role::kAssistant,
                      multi_step_actions);

  proto::SparkyContextData sparky_context_data;
  AddDialogToSparkyContext(dialog, &sparky_context_data);
  const ::google::protobuf::RepeatedPtrField<::manta::proto::Turn>&
      dialog_proto = sparky_context_data.conversation();

  ASSERT_EQ(dialog_proto.size(), 6);
  ASSERT_TRUE(ContainsDialog(dialog_proto, "Where is it?", Role::kUser, {}));
  ASSERT_TRUE(ContainsDialog(dialog_proto, "In Tokyo", Role::kAssistant, {}));
  ASSERT_TRUE(
      ContainsDialog(dialog_proto, "Turn on dark mode", Role::kUser, {}));
  ASSERT_TRUE(ContainsDialog(dialog_proto, "Okay I have turned on dark mode",
                             Role::kAssistant, &settings_actions));
  ASSERT_TRUE(ContainsDialog(dialog_proto,
                             "Create a new file with a poem about Platypuses",
                             Role::kUser, {}));
  ASSERT_TRUE(ContainsDialog(dialog_proto, "Okay I have opened the text app",
                             Role::kAssistant, &multi_step_actions));
  ASSERT_EQ(dialog_proto.at(5).action_size(), 4);
}

TEST_F(SparkyUtilTest, AddFilesData) {
  std::vector<manta::FileData> files_data;
  auto file_1 = FileData("path1", "name1", "2024");
  file_1.summary = "file 1 summary";
  file_1.bytes =
      std::make_optional(std::vector<uint8_t>({2, 4, 6, 7, 4, 7, 2, 8}));
  file_1.size_in_bytes = 8L;

  files_data.emplace_back(file_1);
  files_data.emplace_back("path2", "name2", "2023");

  proto::SparkyContextData sparky_context_data;
  manta::proto::FilesData* files_proto =
      sparky_context_data.mutable_files_data();
  AddFilesData(std::move(files_data), files_proto);
  auto files = files_proto->files();
  ASSERT_EQ(files_proto->files_size(), 2);
  std::optional<proto::File> proto_file_1 = ObtainFileProto(files, "path1");
  ASSERT_TRUE(proto_file_1.has_value());
  ASSERT_EQ(proto_file_1->name(), "name1");
  ASSERT_EQ(proto_file_1->date_modified(), "2024");
  ASSERT_EQ(proto_file_1->serialized_bytes(), "\x2\x4\x6\a\x4\a\x2\b");
  ASSERT_EQ(proto_file_1->summary(), "file 1 summary");
  std::optional<proto::File> proto_file_2 = ObtainFileProto(files, "path2");
  ASSERT_TRUE(proto_file_2.has_value());
  ASSERT_EQ(proto_file_2->name(), "name2");
  ASSERT_EQ(proto_file_2->date_modified(), "2023");
}

TEST_F(SparkyUtilTest, GetSelectedFilePaths) {
  proto::FileRequest file_request;

  std::set<std::string> empty_set = GetSelectedFilePaths(file_request);
  ASSERT_TRUE(empty_set.empty());

  file_request.add_paths("my/file/path");
  file_request.add_paths("my/second/file/path/");
  std::set<std::string> file_set = GetSelectedFilePaths(file_request);
  ASSERT_EQ((int)file_set.size(), 2);
  ASSERT_TRUE(file_set.contains("my/file/path"));
  ASSERT_TRUE(file_set.contains("my/second/file/path/"));
}

TEST_F(SparkyUtilTest, GetFileDataFromProto) {
  manta::proto::FilesData files_proto;
  std::vector<FileData> empty_files_data = GetFileDataFromProto(files_proto);
  EXPECT_TRUE(empty_files_data.empty());

  manta::proto::File* file1 = files_proto.add_files();
  file1->set_name("name1");
  file1->set_path("my/file/path");
  file1->set_summary("this file is a picture of a cat");
  file1->set_date_modified("2024");
  file1->set_size_in_bytes(823);

  manta::proto::File* file2 = files_proto.add_files();
  file2->set_name("name2");
  file2->set_path("my/second/file/path/");
  file2->set_summary("this file is a poem about a cat");
  file2->set_date_modified("2023");
  file2->set_size_in_bytes(94);

  std::vector<FileData> files_data = GetFileDataFromProto(files_proto);
  ASSERT_TRUE(files_data.size() == 2);
  auto file1_data = files_data.at(0);
  ASSERT_EQ(file1_data.name, "name1");
  ASSERT_EQ(file1_data.path, "my/file/path");
  ASSERT_EQ(file1_data.summary, "this file is a picture of a cat");
  ASSERT_EQ(file1_data.date_modified, "2024");
  ASSERT_EQ(file1_data.size_in_bytes, 823);
}

}  // namespace manta