#gpowerd Support loading empty saved actions

This change updates `findLargestFileId` to return `std::optional<uint64_t>`, allowing it to clearly indicate when no valid "savedactions_" files are found. `ReadSavedActions` now returns an empty `SavedActions` proto in this case, rather than an error.

Additional tests are added to cover scenarios with no files, invalid filenames, and corrupted file contents.

PiperOrigin-RevId: 834808486
Change-Id: I948e0ea485385df8d0e5056be0fbd2b450a6d89f
diff --git a/persistent_storage_impl.cc b/persistent_storage_impl.cc
index d39dfa3..bdff4a3 100644
--- a/persistent_storage_impl.cc
+++ b/persistent_storage_impl.cc
@@ -10,6 +10,7 @@
 #include <ios>
 #include <iterator>
 #include <ostream>
+#include <optional>
 #include <string>
 #include <system_error>  // NOLINT(build/c++11)
 #include <vector>
@@ -103,12 +104,10 @@
   if (!files.ok()) {
     return files.status();
   }
-  auto id = findLargestFileId(files.value());
-  if (!id.ok()) {
-    return id.status();
-  }
+  std::optional<uint64_t> id = findLargestFileId(files.value());
+  uint64_t current_id_val = id.has_value() ? id.value() : 0;
 
-  std::string file_path = MakeFilePath(dir_path_, id.value());
+  std::string file_path = MakeFilePath(dir_path_, current_id_val);
 
   // write the new record to the file, and close it
   // this ensures that the data is always written before the file is collated
@@ -122,6 +121,11 @@
         absl::StrCat("Failed to get file size", ec.message()));
   }
   if (file_size > max_file_size_) {
+    if (!id.has_value()) {
+      LOG(WARNING) << "A single record written to a new file " << file_path
+                   << " exceeds max_file_size " << max_file_size_
+                   << ". File size is " << file_size;
+    }
     LOG(INFO) << "file size too large merging" << std::endl;
     absl::StatusOr<SavedActions> saved_actions_write_ordered =
         proto_reader::ReadProto<SavedActions>(file_path);
@@ -152,7 +156,7 @@
       *(ptr_record->mutable_actions()) = i.second;
     }
 
-    std::string next_file = MakeFilePath(dir_path_, id.value() + 1);
+    std::string next_file = MakeFilePath(dir_path_, current_id_val + 1);
     RETURN_IF_ERROR(WriteSavedActions(next_file, new_log));
     return deleteAllFilesExceptLargest(dir_path_);
   }
@@ -199,10 +203,12 @@
   if (!files.ok()) {
     return files.status();
   }
-  auto id = findLargestFileId(files.value());
-  if (!id.ok()) {
-    return id.status();
+
+  std::optional<uint64_t> id = findLargestFileId(files.value());
+  if (!id.has_value()) {
+    return SavedActions();
   }
+
   std::string file_path = MakeFilePath(dir_path_, id.value());
   auto saved_actions = proto_reader::ReadProto<SavedActions>(file_path);
   absl::StatusOr<SavedActions> compressed_saved_actions;
@@ -333,10 +339,10 @@
 // "savedactions_1"
 // "savedactions_2"
 // where the largest id is the most recent file
-absl::StatusOr<uint64_t> PersistentStorageManagerImpl::findLargestFileId(
+std::optional<uint64_t>
+PersistentStorageManagerImpl::findLargestFileId(
     absl::Span<const std::string> files) {
-  std::error_code ec;
-  uint64_t largest_id = 0;
+  std::optional<uint64_t> largest_id;
   for (const auto& entry : files) {
     std::vector<absl::string_view> file_name_parts = absl::StrSplit(entry, '_');
     if (file_name_parts.size() != 2) {
@@ -353,7 +359,7 @@
       LOG(WARNING) << "unknown file name prefix" << entry;
       continue;
     }
-    if (file_id > largest_id) {
+    if (!largest_id.has_value() || file_id > *largest_id) {
       largest_id = file_id;
     }
   }
@@ -382,9 +388,11 @@
   if (!files.ok()) {
     return files.status();
   }
-  auto largest = findLargestFileId(files.value());
-  if (!largest.ok()) {
-    return largest.status();
+  std::optional<uint64_t> largest = findLargestFileId(files.value());
+  if (!largest.has_value()) {
+    LOG(ERROR)
+        << "No largest file found, but expected to find files to delete.";
+    return absl::OkStatus();
   }
 
   std::string largest_file_name = MakeFileName(largest.value());
diff --git a/persistent_storage_impl.h b/persistent_storage_impl.h
index 2577dcb..d555709 100644
--- a/persistent_storage_impl.h
+++ b/persistent_storage_impl.h
@@ -2,6 +2,7 @@
 #define PRODUCTION_BORG_MGMT_NODE_PROXY_SAFEPOWER_SAFEPOWER_AGENT_PERSISTENT_STORAGE_IMPL_H_
 
 #include <cstdint>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -76,7 +77,7 @@
             borg_mgmt::node_proxy::safepower::utils::FlightRecordRequest,
             safepower_agent_persistence_proto::SavedAction, FlightRecordHash,
                       FlightRecordEq>& map);
-  absl::StatusOr<uint64_t> findLargestFileId(
+  std::optional<uint64_t> findLargestFileId(
       absl::Span<const std::string> files);
   absl::StatusOr<std::vector<std::string> > listFiles(
       absl::string_view file_path);
diff --git a/persistent_storage_impl_test.cc b/persistent_storage_impl_test.cc
index 1c62c06..1711a06 100644
--- a/persistent_storage_impl_test.cc
+++ b/persistent_storage_impl_test.cc
@@ -3,6 +3,7 @@
 #include <cstdint>
 #include <filesystem>  //  NOLINT(build/c++17)
 #include <fstream>
+#include <ios>
 #include <iterator>
 #include <string>
 #include <vector>
@@ -124,6 +125,16 @@
   EXPECT_EQ(result_str, orig_str);
 }
 
+TEST(PersistentStorageBMC, ReadBeforeWriteReturnsEmpty) {
+  std::string dir_name = CreateNewTestDir();
+  safepower_agent_config::PersistentStorageConfig config;
+  config.set_dir_path(dir_name);
+  persistent_storage::PersistentStorageManagerImpl persist(config);
+  auto result = persist.ReadSavedActions();
+  EXPECT_EQ(result.status(), absl::OkStatus());
+  EXPECT_EQ(result->saved_action_records_size(), 0);
+}
+
 TEST(PersistentStorageBMC, WriteStateAndModifyAndRead) {
   std::string dir_name = CreateNewTestDir();
   auto saved_actions = GetSavedActionsWhole();
@@ -243,6 +254,76 @@
   EXPECT_THAT(files, IsEmpty());
 }
 
+void WriteActionsToFile(absl::string_view file_path,
+                        const SavedActions& actions) {
+  std::ofstream ofs(std::string(file_path),
+                    std::ios_base::binary | std::ios_base::out);
+  ASSERT_TRUE(ofs.is_open());
+  ASSERT_TRUE(actions.SerializeToOstream(&ofs));
+  ofs.close();
+  ASSERT_FALSE(ofs.fail());
+}
+
+TEST(PersistentStorageBMC, FindLargestFileIdIgnoresInvalidFileNames) {
+  std::string dir_name = CreateNewTestDir();
+  safepower_agent_config::PersistentStorageConfig config;
+  config.set_dir_path(dir_name);
+  persistent_storage::PersistentStorageManagerImpl persist(config);
+
+  auto actions1 = GetSavedActionsWhole("actions1");
+  auto actions2 = GetSavedActionsWhole("actions2");
+  auto actions_other = GetSavedActionsWhole("other");
+
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_1"), actions1);
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_foo"),
+                     actions_other);
+  WriteActionsToFile(absl::StrCat(dir_name, "/notsavedactions_3"),
+                     actions_other);
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions"), actions_other);
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_"), actions_other);
+
+  auto result = persist.ReadSavedActions();
+  ASSERT_EQ(result.status(), absl::OkStatus());
+  ASSERT_EQ(result->saved_action_records_size(), 1);
+  EXPECT_EQ(result->saved_action_records(0)
+                .actions()
+                .original_request()
+                .flight_record()
+                .flight_name(),
+            "actions1");
+
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_2"), actions2);
+  result = persist.ReadSavedActions();
+  ASSERT_EQ(result.status(), absl::OkStatus());
+  ASSERT_EQ(result->saved_action_records_size(), 1);
+  EXPECT_EQ(result->saved_action_records(0)
+                .actions()
+                .original_request()
+                .flight_record()
+                .flight_name(),
+            "actions2");
+}
+
+TEST(PersistentStorageBMC, ReadSavedActionsIgnoresInvalidFileContent) {
+  std::string dir_name = CreateNewTestDir();
+  safepower_agent_config::PersistentStorageConfig config;
+  config.set_dir_path(dir_name);
+  persistent_storage::PersistentStorageManagerImpl persist(config);
+
+  auto actions1 = GetSavedActionsWhole("actions1");
+  WriteActionsToFile(absl::StrCat(dir_name, "/savedactions_1"), actions1);
+  // Overwrite the file with invalid content.
+  std::ofstream ofs(absl::StrCat(dir_name, "/savedactions_1"),
+                    std::ios_base::binary | std::ios_base::out);
+  ASSERT_TRUE(ofs.is_open());
+  ofs << "invalid proto content";
+  ofs.close();
+  ASSERT_FALSE(ofs.fail());
+
+  auto result = persist.ReadSavedActions();
+  ASSERT_FALSE(result.ok());
+}
+
 }  // namespace
 
 }  // namespace persistent_storage