/** * @file project_tool_test.cc * @brief Unit tests for the ProjectTool AI agent tools * * Tests the project management functionality including snapshots, * edit serialization, checksum computation, and project diffing. */ #include "cli/service/agent/tools/project_tool.h" #include #include #include #include #include #include #include #include #include "absl/status/status.h" #include "cli/service/agent/agent_context.h" namespace yaze { namespace cli { namespace agent { namespace tools { namespace { using ::testing::Contains; using ::testing::Eq; using ::testing::HasSubstr; using ::testing::IsEmpty; using ::testing::Not; using ::testing::SizeIs; namespace fs = std::filesystem; // ============================================================================= // EditFileHeader Tests // ============================================================================= TEST(EditFileHeaderTest, MagicConstantIsYAZE) { // "YAZE" in ASCII = 0x59 0x41 0x5A 0x45 = 0x59415A45 (big endian) // But stored as 0x59415A45 which is little-endian "EZAY" or big-endian "YAZE" EXPECT_EQ(EditFileHeader::kMagic, 0x59415A45u); } TEST(EditFileHeaderTest, CurrentVersionIsOne) { EXPECT_EQ(EditFileHeader::kCurrentVersion, 1u); } TEST(EditFileHeaderTest, DefaultValuesAreCorrect) { EditFileHeader header; EXPECT_EQ(header.magic, EditFileHeader::kMagic); EXPECT_EQ(header.version, EditFileHeader::kCurrentVersion); } TEST(EditFileHeaderTest, HasRomChecksumField) { EditFileHeader header; EXPECT_EQ(header.base_rom_sha256.size(), 32u); } // ============================================================================= // SerializedEdit Tests // ============================================================================= TEST(SerializedEditTest, StructureHasExpectedFields) { SerializedEdit edit; edit.address = 0x008000; edit.length = 4; EXPECT_EQ(edit.address, 0x008000u); EXPECT_EQ(edit.length, 4u); } TEST(SerializedEditTest, StructureSize) { // SerializedEdit should be 8 bytes (2 uint32_t) EXPECT_EQ(sizeof(SerializedEdit), 8u); } // ============================================================================= // ProjectToolUtils Tests // ============================================================================= TEST(ProjectToolUtilsTest, ComputeSHA256ProducesCorrectLength) { std::vector data = {0x00, 0x01, 0x02, 0x03}; auto hash = ProjectToolUtils::ComputeSHA256(data.data(), data.size()); EXPECT_EQ(hash.size(), 32u); } TEST(ProjectToolUtilsTest, ComputeSHA256IsDeterministic) { std::vector data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" auto hash1 = ProjectToolUtils::ComputeSHA256(data.data(), data.size()); auto hash2 = ProjectToolUtils::ComputeSHA256(data.data(), data.size()); EXPECT_EQ(hash1, hash2); } TEST(ProjectToolUtilsTest, ComputeSHA256DifferentDataDifferentHash) { std::vector data1 = {0x00, 0x01, 0x02}; std::vector data2 = {0x00, 0x01, 0x03}; auto hash1 = ProjectToolUtils::ComputeSHA256(data1.data(), data1.size()); auto hash2 = ProjectToolUtils::ComputeSHA256(data2.data(), data2.size()); EXPECT_NE(hash1, hash2); } TEST(ProjectToolUtilsTest, ComputeSHA256EmptyInput) { auto hash = ProjectToolUtils::ComputeSHA256(nullptr, 0); EXPECT_EQ(hash.size(), 32u); // SHA-256 of empty string is well-known // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 EXPECT_EQ(hash[0], 0xe3); EXPECT_EQ(hash[1], 0xb0); EXPECT_EQ(hash[2], 0xc4); } TEST(ProjectToolUtilsTest, FormatChecksumProduces64Chars) { std::array checksum; checksum.fill(0xAB); std::string formatted = ProjectToolUtils::FormatChecksum(checksum); EXPECT_EQ(formatted.size(), 64u); } TEST(ProjectToolUtilsTest, FormatChecksumIsHex) { std::array checksum; for (size_t i = 0; i < 32; ++i) { checksum[i] = static_cast(i); } std::string formatted = ProjectToolUtils::FormatChecksum(checksum); // Should only contain hex characters for (char c : formatted) { EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) << "Non-hex character: " << c; } } TEST(ProjectToolUtilsTest, FormatTimestampProducesISO8601) { auto now = std::chrono::system_clock::now(); std::string formatted = ProjectToolUtils::FormatTimestamp(now); // Should match ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ EXPECT_THAT(formatted, HasSubstr("T")); EXPECT_THAT(formatted, HasSubstr("Z")); EXPECT_GE(formatted.size(), 20u); } TEST(ProjectToolUtilsTest, ParseTimestampRoundTrip) { auto original = std::chrono::system_clock::now(); std::string formatted = ProjectToolUtils::FormatTimestamp(original); auto parsed_result = ProjectToolUtils::ParseTimestamp(formatted); ASSERT_TRUE(parsed_result.ok()) << parsed_result.status().message(); // Due to second-precision and timezone handling (gmtime vs mktime), // allow for timezone differences (up to 24 hours) auto parsed = *parsed_result; auto diff = std::chrono::duration_cast( original - parsed).count(); EXPECT_LE(std::abs(diff), 24) << "Timestamp difference exceeds 24 hours"; } TEST(ProjectToolUtilsTest, ParseTimestampInvalidFormat) { auto result = ProjectToolUtils::ParseTimestamp("invalid"); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); } // ============================================================================= // ProjectSnapshot Tests // ============================================================================= TEST(ProjectSnapshotTest, DefaultConstruction) { ProjectSnapshot snapshot; EXPECT_TRUE(snapshot.name.empty()); EXPECT_TRUE(snapshot.description.empty()); EXPECT_TRUE(snapshot.edits.empty()); EXPECT_TRUE(snapshot.metadata.empty()); } TEST(ProjectSnapshotTest, HasAllRequiredFields) { ProjectSnapshot snapshot; snapshot.name = "test-snapshot"; snapshot.description = "Test description"; snapshot.created = std::chrono::system_clock::now(); RomEdit edit; edit.address = 0x008000; edit.old_value = {0x00}; edit.new_value = {0x01}; edit.description = "Test edit"; edit.timestamp = std::chrono::system_clock::now(); snapshot.edits.push_back(edit); snapshot.metadata["author"] = "test"; snapshot.rom_checksum.fill(0xAB); EXPECT_EQ(snapshot.name, "test-snapshot"); EXPECT_EQ(snapshot.description, "Test description"); EXPECT_THAT(snapshot.edits, SizeIs(1)); EXPECT_EQ(snapshot.metadata["author"], "test"); EXPECT_EQ(snapshot.rom_checksum[0], 0xAB); } // ============================================================================= // ProjectManager Tests // ============================================================================= class ProjectManagerTest : public ::testing::Test { protected: void SetUp() override { // Create a temporary directory for tests test_dir_ = fs::temp_directory_path() / "yaze_project_test"; fs::create_directories(test_dir_); } void TearDown() override { // Clean up if (fs::exists(test_dir_)) { fs::remove_all(test_dir_); } } fs::path test_dir_; }; TEST_F(ProjectManagerTest, IsNotInitializedByDefault) { ProjectManager manager; EXPECT_FALSE(manager.IsInitialized()); } TEST_F(ProjectManagerTest, InitializeCreatesProjectDirectory) { ProjectManager manager; auto status = manager.Initialize(test_dir_.string()); ASSERT_TRUE(status.ok()) << status.message(); EXPECT_TRUE(manager.IsInitialized()); EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project")); EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project" / "snapshots")); EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project" / "project.json")); } TEST_F(ProjectManagerTest, ListSnapshotsEmptyInitially) { ProjectManager manager; auto status = manager.Initialize(test_dir_.string()); ASSERT_TRUE(status.ok()); auto snapshots = manager.ListSnapshots(); EXPECT_TRUE(snapshots.empty()); } TEST_F(ProjectManagerTest, CreateSnapshotEmptyNameFails) { ProjectManager manager; manager.Initialize(test_dir_.string()); std::array checksum; checksum.fill(0); auto status = manager.CreateSnapshot("", "description", {}, checksum); EXPECT_FALSE(status.ok()); EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); } TEST_F(ProjectManagerTest, CreateSnapshotDuplicateNameFails) { ProjectManager manager; manager.Initialize(test_dir_.string()); std::array checksum; checksum.fill(0); auto status1 = manager.CreateSnapshot("test", "first", {}, checksum); ASSERT_TRUE(status1.ok()); auto status2 = manager.CreateSnapshot("test", "second", {}, checksum); EXPECT_FALSE(status2.ok()); EXPECT_EQ(status2.code(), absl::StatusCode::kAlreadyExists); } TEST_F(ProjectManagerTest, CreateAndListSnapshot) { ProjectManager manager; manager.Initialize(test_dir_.string()); std::array checksum; checksum.fill(0xAB); auto status = manager.CreateSnapshot("v1.0", "Initial version", {}, checksum); ASSERT_TRUE(status.ok()); auto snapshots = manager.ListSnapshots(); EXPECT_THAT(snapshots, Contains("v1.0")); } TEST_F(ProjectManagerTest, GetSnapshotNotFound) { ProjectManager manager; manager.Initialize(test_dir_.string()); auto result = manager.GetSnapshot("nonexistent"); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound); } TEST_F(ProjectManagerTest, DeleteSnapshotNotFound) { ProjectManager manager; manager.Initialize(test_dir_.string()); auto status = manager.DeleteSnapshot("nonexistent"); EXPECT_FALSE(status.ok()); EXPECT_EQ(status.code(), absl::StatusCode::kNotFound); } TEST_F(ProjectManagerTest, CreateGetDeleteSnapshot) { ProjectManager manager; manager.Initialize(test_dir_.string()); std::array checksum; checksum.fill(0xAB); // Create auto create_status = manager.CreateSnapshot("test", "desc", {}, checksum); ASSERT_TRUE(create_status.ok()); // Get auto get_result = manager.GetSnapshot("test"); ASSERT_TRUE(get_result.ok()); EXPECT_EQ(get_result->name, "test"); EXPECT_EQ(get_result->description, "desc"); // Delete auto delete_status = manager.DeleteSnapshot("test"); ASSERT_TRUE(delete_status.ok()); // Verify deleted auto verify_result = manager.GetSnapshot("test"); EXPECT_FALSE(verify_result.ok()); } // ============================================================================= // Tool Name Tests // ============================================================================= TEST(ProjectToolsTest, ProjectStatusToolName) { ProjectStatusTool tool; EXPECT_EQ(tool.GetName(), "project-status"); } TEST(ProjectToolsTest, ProjectSnapshotToolName) { ProjectSnapshotTool tool; EXPECT_EQ(tool.GetName(), "project-snapshot"); } TEST(ProjectToolsTest, ProjectRestoreToolName) { ProjectRestoreTool tool; EXPECT_EQ(tool.GetName(), "project-restore"); } TEST(ProjectToolsTest, ProjectExportToolName) { ProjectExportTool tool; EXPECT_EQ(tool.GetName(), "project-export"); } TEST(ProjectToolsTest, ProjectImportToolName) { ProjectImportTool tool; EXPECT_EQ(tool.GetName(), "project-import"); } TEST(ProjectToolsTest, ProjectDiffToolName) { ProjectDiffTool tool; EXPECT_EQ(tool.GetName(), "project-diff"); } TEST(ProjectToolsTest, AllToolNamesStartWithProject) { ProjectStatusTool status; ProjectSnapshotTool snapshot; ProjectRestoreTool restore; ProjectExportTool export_tool; ProjectImportTool import_tool; ProjectDiffTool diff; EXPECT_THAT(status.GetName(), HasSubstr("project-")); EXPECT_THAT(snapshot.GetName(), HasSubstr("project-")); EXPECT_THAT(restore.GetName(), HasSubstr("project-")); EXPECT_THAT(export_tool.GetName(), HasSubstr("project-")); EXPECT_THAT(import_tool.GetName(), HasSubstr("project-")); EXPECT_THAT(diff.GetName(), HasSubstr("project-")); } TEST(ProjectToolsTest, AllToolNamesAreUnique) { ProjectStatusTool status; ProjectSnapshotTool snapshot; ProjectRestoreTool restore; ProjectExportTool export_tool; ProjectImportTool import_tool; ProjectDiffTool diff; std::vector names = { status.GetName(), snapshot.GetName(), restore.GetName(), export_tool.GetName(), import_tool.GetName(), diff.GetName()}; std::set unique_names(names.begin(), names.end()); EXPECT_EQ(unique_names.size(), names.size()) << "All project tool names should be unique"; } // ============================================================================= // Tool Usage String Tests // ============================================================================= TEST(ProjectToolsTest, StatusToolUsageFormat) { ProjectStatusTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("project-status")); EXPECT_THAT(tool.GetUsage(), HasSubstr("--format")); } TEST(ProjectToolsTest, SnapshotToolUsageFormat) { ProjectSnapshotTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("--name")); EXPECT_THAT(tool.GetUsage(), HasSubstr("--description")); } TEST(ProjectToolsTest, RestoreToolUsageFormat) { ProjectRestoreTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("--name")); } TEST(ProjectToolsTest, ExportToolUsageFormat) { ProjectExportTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("--path")); EXPECT_THAT(tool.GetUsage(), HasSubstr("--include-rom")); } TEST(ProjectToolsTest, ImportToolUsageFormat) { ProjectImportTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("--path")); } TEST(ProjectToolsTest, DiffToolUsageFormat) { ProjectDiffTool tool; EXPECT_THAT(tool.GetUsage(), HasSubstr("--snapshot1")); EXPECT_THAT(tool.GetUsage(), HasSubstr("--snapshot2")); } // ============================================================================= // RequiresLabels Tests // ============================================================================= TEST(ProjectToolsTest, NoToolsRequireLabels) { ProjectStatusTool status; ProjectSnapshotTool snapshot; ProjectRestoreTool restore; ProjectExportTool export_tool; ProjectImportTool import_tool; ProjectDiffTool diff; EXPECT_FALSE(status.RequiresLabels()); EXPECT_FALSE(snapshot.RequiresLabels()); EXPECT_FALSE(restore.RequiresLabels()); EXPECT_FALSE(export_tool.RequiresLabels()); EXPECT_FALSE(import_tool.RequiresLabels()); EXPECT_FALSE(diff.RequiresLabels()); } // ============================================================================= // Snapshot Serialization Round-Trip Test // ============================================================================= class SnapshotSerializationTest : public ::testing::Test { protected: void SetUp() override { test_file_ = fs::temp_directory_path() / "test_snapshot.edits"; } void TearDown() override { if (fs::exists(test_file_)) { fs::remove(test_file_); } } fs::path test_file_; }; TEST_F(SnapshotSerializationTest, SaveAndLoadRoundTrip) { // Create snapshot with edits ProjectSnapshot original; original.name = "test-snapshot"; original.description = "Test description"; original.created = std::chrono::system_clock::now(); original.rom_checksum.fill(0xAB); RomEdit edit1; edit1.address = 0x008000; edit1.old_value = {0x00, 0x01, 0x02}; edit1.new_value = {0x10, 0x11, 0x12}; original.edits.push_back(edit1); RomEdit edit2; edit2.address = 0x00A000; edit2.old_value = {0xFF}; edit2.new_value = {0x00}; original.edits.push_back(edit2); original.metadata["author"] = "test"; original.metadata["version"] = "1.0"; // Save auto save_status = original.SaveToFile(test_file_.string()); ASSERT_TRUE(save_status.ok()) << save_status.message(); ASSERT_TRUE(fs::exists(test_file_)); // Load auto load_result = ProjectSnapshot::LoadFromFile(test_file_.string()); ASSERT_TRUE(load_result.ok()) << load_result.status().message(); const ProjectSnapshot& loaded = *load_result; // Verify EXPECT_EQ(loaded.name, original.name); EXPECT_EQ(loaded.description, original.description); EXPECT_EQ(loaded.rom_checksum, original.rom_checksum); ASSERT_EQ(loaded.edits.size(), original.edits.size()); for (size_t i = 0; i < loaded.edits.size(); ++i) { EXPECT_EQ(loaded.edits[i].address, original.edits[i].address); EXPECT_EQ(loaded.edits[i].old_value, original.edits[i].old_value); EXPECT_EQ(loaded.edits[i].new_value, original.edits[i].new_value); } EXPECT_EQ(loaded.metadata, original.metadata); } TEST_F(SnapshotSerializationTest, LoadNonexistentFileFails) { auto result = ProjectSnapshot::LoadFromFile("/nonexistent/path/file.edits"); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound); } TEST_F(SnapshotSerializationTest, LoadInvalidFileFails) { // Create an invalid file std::ofstream file(test_file_, std::ios::binary); file << "invalid data"; file.close(); auto result = ProjectSnapshot::LoadFromFile(test_file_.string()); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); } } // namespace } // namespace tools } // namespace agent } // namespace cli } // namespace yaze