backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -0,0 +1,617 @@
/**
* @file code_gen_tool_test.cc
* @brief Unit tests for the CodeGenTool AI agent tools
*
* Tests the code generation functionality including ASM templates,
* placeholder substitution, freespace detection, and hook validation.
*/
#include "cli/service/agent/tools/code_gen_tool.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <map>
#include <string>
#include <vector>
#include "absl/status/status.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;
// =============================================================================
// AsmTemplate Structure Tests
// =============================================================================
TEST(AsmTemplateTest, StructureHasExpectedFields) {
AsmTemplate tmpl;
tmpl.name = "test";
tmpl.code_template = "NOP";
tmpl.required_params = {"PARAM1", "PARAM2"};
tmpl.description = "Test template";
EXPECT_EQ(tmpl.name, "test");
EXPECT_EQ(tmpl.code_template, "NOP");
EXPECT_THAT(tmpl.required_params, SizeIs(2));
EXPECT_EQ(tmpl.description, "Test template");
}
// =============================================================================
// FreeSpaceRegion Tests
// =============================================================================
TEST(FreeSpaceRegionTest, StructureHasExpectedFields) {
FreeSpaceRegion region;
region.start = 0x1F8000;
region.end = 0x1FFFFF;
region.description = "Bank $3F freespace";
region.free_percent = 95;
EXPECT_EQ(region.start, 0x1F8000u);
EXPECT_EQ(region.end, 0x1FFFFFu);
EXPECT_EQ(region.description, "Bank $3F freespace");
EXPECT_EQ(region.free_percent, 95);
}
TEST(FreeSpaceRegionTest, SizeCalculation) {
FreeSpaceRegion region;
region.start = 0x1F8000;
region.end = 0x1FFFFF;
// 0x1FFFFF - 0x1F8000 + 1 = 0x8000 = 32768 bytes
EXPECT_EQ(region.Size(), 0x8000u);
}
TEST(FreeSpaceRegionTest, SizeCalculationSingleByte) {
FreeSpaceRegion region;
region.start = 0x100;
region.end = 0x100;
EXPECT_EQ(region.Size(), 1u);
}
TEST(FreeSpaceRegionTest, SizeCalculationLargeRegion) {
FreeSpaceRegion region;
region.start = 0x000000;
region.end = 0x3FFFFF;
// 4MB region
EXPECT_EQ(region.Size(), 0x400000u);
}
// =============================================================================
// CodeGenerationDiagnostic Tests
// =============================================================================
TEST(CodeGenerationDiagnosticTest, SeverityStringInfo) {
CodeGenerationDiagnostic diag;
diag.severity = CodeGenerationDiagnostic::Severity::kInfo;
EXPECT_EQ(diag.SeverityString(), "info");
}
TEST(CodeGenerationDiagnosticTest, SeverityStringWarning) {
CodeGenerationDiagnostic diag;
diag.severity = CodeGenerationDiagnostic::Severity::kWarning;
EXPECT_EQ(diag.SeverityString(), "warning");
}
TEST(CodeGenerationDiagnosticTest, SeverityStringError) {
CodeGenerationDiagnostic diag;
diag.severity = CodeGenerationDiagnostic::Severity::kError;
EXPECT_EQ(diag.SeverityString(), "error");
}
TEST(CodeGenerationDiagnosticTest, StructureHasExpectedFields) {
CodeGenerationDiagnostic diag;
diag.severity = CodeGenerationDiagnostic::Severity::kWarning;
diag.message = "Test warning";
diag.address = 0x008000;
EXPECT_EQ(diag.message, "Test warning");
EXPECT_EQ(diag.address, 0x008000u);
}
// =============================================================================
// CodeGenerationResult Tests
// =============================================================================
TEST(CodeGenerationResultTest, DefaultConstruction) {
CodeGenerationResult result;
// success is uninitialized by default - not testing its value
EXPECT_TRUE(result.generated_code.empty());
EXPECT_TRUE(result.diagnostics.empty());
EXPECT_TRUE(result.symbols.empty());
}
TEST(CodeGenerationResultTest, AddInfoAddsDiagnostic) {
CodeGenerationResult result;
result.success = true;
result.AddInfo("Test info", 0x008000);
EXPECT_THAT(result.diagnostics, SizeIs(1));
EXPECT_EQ(result.diagnostics[0].severity,
CodeGenerationDiagnostic::Severity::kInfo);
EXPECT_EQ(result.diagnostics[0].message, "Test info");
EXPECT_EQ(result.diagnostics[0].address, 0x008000u);
EXPECT_TRUE(result.success); // Info doesn't change success
}
TEST(CodeGenerationResultTest, AddWarningAddsDiagnostic) {
CodeGenerationResult result;
result.success = true;
result.AddWarning("Test warning", 0x00A000);
EXPECT_THAT(result.diagnostics, SizeIs(1));
EXPECT_EQ(result.diagnostics[0].severity,
CodeGenerationDiagnostic::Severity::kWarning);
EXPECT_EQ(result.diagnostics[0].message, "Test warning");
EXPECT_TRUE(result.success); // Warning doesn't change success
}
TEST(CodeGenerationResultTest, AddErrorSetsFailure) {
CodeGenerationResult result;
result.success = true;
result.AddError("Test error", 0x00C000);
EXPECT_THAT(result.diagnostics, SizeIs(1));
EXPECT_EQ(result.diagnostics[0].severity,
CodeGenerationDiagnostic::Severity::kError);
EXPECT_EQ(result.diagnostics[0].message, "Test error");
EXPECT_FALSE(result.success); // Error sets success to false
}
TEST(CodeGenerationResultTest, MultipleDiagnostics) {
CodeGenerationResult result;
result.success = true;
result.AddInfo("Info 1");
result.AddWarning("Warning 1");
result.AddError("Error 1");
result.AddInfo("Info 2");
EXPECT_THAT(result.diagnostics, SizeIs(4));
EXPECT_FALSE(result.success); // Because we added an error
}
TEST(CodeGenerationResultTest, SymbolsMap) {
CodeGenerationResult result;
result.symbols["MyLabel"] = 0x1F8000;
result.symbols["OtherLabel"] = 0x00A000;
EXPECT_EQ(result.symbols["MyLabel"], 0x1F8000u);
EXPECT_EQ(result.symbols["OtherLabel"], 0x00A000u);
}
// =============================================================================
// Tool Name Tests
// =============================================================================
TEST(CodeGenToolsTest, AsmHookToolName) {
CodeGenAsmHookTool tool;
EXPECT_EQ(tool.GetName(), "codegen-asm-hook");
}
TEST(CodeGenToolsTest, FreespacePatchToolName) {
CodeGenFreespacePatchTool tool;
EXPECT_EQ(tool.GetName(), "codegen-freespace-patch");
}
TEST(CodeGenToolsTest, SpriteTemplateToolName) {
CodeGenSpriteTemplateTool tool;
EXPECT_EQ(tool.GetName(), "codegen-sprite-template");
}
TEST(CodeGenToolsTest, EventHandlerToolName) {
CodeGenEventHandlerTool tool;
EXPECT_EQ(tool.GetName(), "codegen-event-handler");
}
TEST(CodeGenToolsTest, AllToolNamesStartWithCodegen) {
CodeGenAsmHookTool hook;
CodeGenFreespacePatchTool freespace;
CodeGenSpriteTemplateTool sprite;
CodeGenEventHandlerTool event;
EXPECT_THAT(hook.GetName(), HasSubstr("codegen-"));
EXPECT_THAT(freespace.GetName(), HasSubstr("codegen-"));
EXPECT_THAT(sprite.GetName(), HasSubstr("codegen-"));
EXPECT_THAT(event.GetName(), HasSubstr("codegen-"));
}
TEST(CodeGenToolsTest, AllToolNamesAreUnique) {
CodeGenAsmHookTool hook;
CodeGenFreespacePatchTool freespace;
CodeGenSpriteTemplateTool sprite;
CodeGenEventHandlerTool event;
std::vector<std::string> names = {hook.GetName(), freespace.GetName(),
sprite.GetName(), event.GetName()};
std::set<std::string> unique_names(names.begin(), names.end());
EXPECT_EQ(unique_names.size(), names.size())
<< "All code gen tool names should be unique";
}
// =============================================================================
// Tool Usage String Tests
// =============================================================================
TEST(CodeGenToolsTest, AsmHookToolUsageFormat) {
CodeGenAsmHookTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--nop-fill"));
}
TEST(CodeGenToolsTest, FreespacePatchToolUsageFormat) {
CodeGenFreespacePatchTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--size"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--prefer-bank"));
}
TEST(CodeGenToolsTest, SpriteTemplateToolUsageFormat) {
CodeGenSpriteTemplateTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--name"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--init-code"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--main-code"));
}
TEST(CodeGenToolsTest, EventHandlerToolUsageFormat) {
CodeGenEventHandlerTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--type"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
EXPECT_THAT(tool.GetUsage(), HasSubstr("--custom-code"));
}
// =============================================================================
// CodeGenToolBase Method Tests (via concrete class)
// =============================================================================
// Test class to expose protected methods for testing
class TestableCodeGenTool : public CodeGenToolBase {
public:
using CodeGenToolBase::DetectFreeSpace;
using CodeGenToolBase::FormatResultAsJson;
using CodeGenToolBase::FormatResultAsText;
using CodeGenToolBase::GetAllTemplates;
using CodeGenToolBase::GetHookLocationDescription;
using CodeGenToolBase::GetTemplate;
using CodeGenToolBase::IsKnownHookLocation;
using CodeGenToolBase::SubstitutePlaceholders;
using CodeGenToolBase::ValidateHookAddress;
std::string GetName() const override { return "testable-codegen"; }
std::string GetUsage() const override { return "test"; }
protected:
absl::Status ValidateArgs(
const resources::ArgumentParser& /*parser*/) override {
return absl::OkStatus();
}
absl::Status Execute(Rom* /*rom*/,
const resources::ArgumentParser& /*parser*/,
resources::OutputFormatter& /*formatter*/) override {
return absl::OkStatus();
}
};
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersSimple) {
TestableCodeGenTool tool;
std::string tmpl = "Hello {{NAME}}!";
std::map<std::string, std::string> params = {{"NAME", "World"}};
std::string result = tool.SubstitutePlaceholders(tmpl, params);
EXPECT_EQ(result, "Hello World!");
}
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersMultiple) {
TestableCodeGenTool tool;
std::string tmpl = "org ${{ADDRESS}}\n{{LABEL}}:\n JSR {{SUBROUTINE}}\n RTL";
std::map<std::string, std::string> params = {
{"ADDRESS", "1F8000"}, {"LABEL", "MyCode"}, {"SUBROUTINE", "DoStuff"}};
std::string result = tool.SubstitutePlaceholders(tmpl, params);
EXPECT_THAT(result, HasSubstr("org $1F8000"));
EXPECT_THAT(result, HasSubstr("MyCode:"));
EXPECT_THAT(result, HasSubstr("JSR DoStuff"));
}
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersNoMatch) {
TestableCodeGenTool tool;
std::string tmpl = "No placeholders here";
std::map<std::string, std::string> params = {{"UNUSED", "value"}};
std::string result = tool.SubstitutePlaceholders(tmpl, params);
EXPECT_EQ(result, "No placeholders here");
}
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersMissingParam) {
TestableCodeGenTool tool;
std::string tmpl = "Hello {{NAME}} and {{OTHER}}!";
std::map<std::string, std::string> params = {{"NAME", "World"}};
std::string result = tool.SubstitutePlaceholders(tmpl, params);
// Missing param should remain as placeholder
EXPECT_THAT(result, HasSubstr("World"));
EXPECT_THAT(result, HasSubstr("{{OTHER}}"));
}
TEST(CodeGenToolBaseTest, GetAllTemplatesNotEmpty) {
TestableCodeGenTool tool;
const auto& templates = tool.GetAllTemplates();
EXPECT_FALSE(templates.empty());
}
TEST(CodeGenToolBaseTest, GetAllTemplatesContainsExpectedTemplates) {
TestableCodeGenTool tool;
const auto& templates = tool.GetAllTemplates();
std::vector<std::string> names;
for (const auto& tmpl : templates) {
names.push_back(tmpl.name);
}
EXPECT_THAT(names, Contains("nmi_hook"));
EXPECT_THAT(names, Contains("sprite"));
EXPECT_THAT(names, Contains("freespace_alloc"));
EXPECT_THAT(names, Contains("jsl_hook"));
EXPECT_THAT(names, Contains("event_handler"));
}
TEST(CodeGenToolBaseTest, GetTemplateFound) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("sprite");
ASSERT_TRUE(result.ok()) << result.status().message();
EXPECT_EQ(result->name, "sprite");
EXPECT_FALSE(result->code_template.empty());
EXPECT_FALSE(result->required_params.empty());
}
TEST(CodeGenToolBaseTest, GetTemplateNotFound) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("nonexistent");
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound);
}
TEST(CodeGenToolBaseTest, IsKnownHookLocationTrue) {
TestableCodeGenTool tool;
// Known hook: EnableForceBlank at 0x00893D
EXPECT_TRUE(tool.IsKnownHookLocation(0x00893D));
}
TEST(CodeGenToolBaseTest, IsKnownHookLocationFalse) {
TestableCodeGenTool tool;
EXPECT_FALSE(tool.IsKnownHookLocation(0x000000));
EXPECT_FALSE(tool.IsKnownHookLocation(0xFFFFFF));
}
TEST(CodeGenToolBaseTest, GetHookLocationDescriptionKnown) {
TestableCodeGenTool tool;
std::string desc = tool.GetHookLocationDescription(0x00893D);
EXPECT_EQ(desc, "EnableForceBlank");
}
TEST(CodeGenToolBaseTest, GetHookLocationDescriptionUnknown) {
TestableCodeGenTool tool;
std::string desc = tool.GetHookLocationDescription(0x000000);
EXPECT_EQ(desc, "Unknown");
}
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsSuccess) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
result.generated_code = "NOP";
std::string json = tool.FormatResultAsJson(result);
EXPECT_THAT(json, HasSubstr("\"success\": true"));
}
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsCode) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
result.generated_code = "LDA #$00";
std::string json = tool.FormatResultAsJson(result);
EXPECT_THAT(json, HasSubstr("\"code\":"));
EXPECT_THAT(json, HasSubstr("LDA"));
}
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsDiagnostics) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
result.AddInfo("Test info");
std::string json = tool.FormatResultAsJson(result);
EXPECT_THAT(json, HasSubstr("\"diagnostics\":"));
EXPECT_THAT(json, HasSubstr("Test info"));
}
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsStatus) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
std::string text = tool.FormatResultAsText(result);
EXPECT_THAT(text, HasSubstr("SUCCESS"));
}
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsFailedStatus) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = false;
result.AddError("Something failed");
std::string text = tool.FormatResultAsText(result);
EXPECT_THAT(text, HasSubstr("FAILED"));
}
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsGeneratedCode) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
result.generated_code = "JSL MyRoutine\nRTL";
std::string text = tool.FormatResultAsText(result);
EXPECT_THAT(text, HasSubstr("Generated Code:"));
EXPECT_THAT(text, HasSubstr("JSL MyRoutine"));
}
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsSymbols) {
TestableCodeGenTool tool;
CodeGenerationResult result;
result.success = true;
result.symbols["MyLabel"] = 0x1F8000;
std::string text = tool.FormatResultAsText(result);
EXPECT_THAT(text, HasSubstr("Symbols:"));
EXPECT_THAT(text, HasSubstr("MyLabel"));
}
// =============================================================================
// Template Content Tests
// =============================================================================
TEST(AsmTemplateTest, NmiHookTemplateHasRequiredParams) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("nmi_hook");
ASSERT_TRUE(result.ok());
EXPECT_THAT(result->required_params, Contains("LABEL"));
EXPECT_THAT(result->required_params, Contains("NMI_HOOK_ADDRESS"));
EXPECT_THAT(result->required_params, Contains("CUSTOM_CODE"));
}
TEST(AsmTemplateTest, SpriteTemplateHasRequiredParams) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("sprite");
ASSERT_TRUE(result.ok());
EXPECT_THAT(result->required_params, Contains("SPRITE_NAME"));
EXPECT_THAT(result->required_params, Contains("INIT_CODE"));
EXPECT_THAT(result->required_params, Contains("MAIN_CODE"));
}
TEST(AsmTemplateTest, JslHookTemplateHasRequiredParams) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("jsl_hook");
ASSERT_TRUE(result.ok());
EXPECT_THAT(result->required_params, Contains("HOOK_ADDRESS"));
EXPECT_THAT(result->required_params, Contains("LABEL"));
}
TEST(AsmTemplateTest, EventHandlerTemplateHasRequiredParams) {
TestableCodeGenTool tool;
auto result = tool.GetTemplate("event_handler");
ASSERT_TRUE(result.ok());
EXPECT_THAT(result->required_params, Contains("EVENT_TYPE"));
EXPECT_THAT(result->required_params, Contains("HOOK_ADDRESS"));
EXPECT_THAT(result->required_params, Contains("LABEL"));
EXPECT_THAT(result->required_params, Contains("CUSTOM_CODE"));
}
TEST(AsmTemplateTest, AllTemplatesHaveDescriptions) {
TestableCodeGenTool tool;
const auto& templates = tool.GetAllTemplates();
for (const auto& tmpl : templates) {
EXPECT_FALSE(tmpl.description.empty())
<< "Template '" << tmpl.name << "' has no description";
}
}
TEST(AsmTemplateTest, AllTemplatesHaveCode) {
TestableCodeGenTool tool;
const auto& templates = tool.GetAllTemplates();
for (const auto& tmpl : templates) {
EXPECT_FALSE(tmpl.code_template.empty())
<< "Template '" << tmpl.name << "' has no code template";
}
}
// =============================================================================
// Template Substitution Integration Tests
// =============================================================================
TEST(AsmTemplateTest, SpriteTemplateSubstitution) {
TestableCodeGenTool tool;
auto tmpl_result = tool.GetTemplate("sprite");
ASSERT_TRUE(tmpl_result.ok());
std::map<std::string, std::string> params = {
{"SPRITE_NAME", "MyCustomSprite"},
{"INIT_CODE", "LDA #$00 : STA $0F50, X"},
{"MAIN_CODE", "JSR Sprite_Move"},
};
std::string code =
tool.SubstitutePlaceholders(tmpl_result->code_template, params);
EXPECT_THAT(code, HasSubstr("MyCustomSprite:"));
EXPECT_THAT(code, HasSubstr("LDA #$00 : STA $0F50, X"));
EXPECT_THAT(code, HasSubstr("JSR Sprite_Move"));
// Should not have unsubstituted placeholders
EXPECT_THAT(code, Not(HasSubstr("{{SPRITE_NAME}}")));
EXPECT_THAT(code, Not(HasSubstr("{{INIT_CODE}}")));
EXPECT_THAT(code, Not(HasSubstr("{{MAIN_CODE}}")));
}
TEST(AsmTemplateTest, JslHookTemplateSubstitution) {
TestableCodeGenTool tool;
auto tmpl_result = tool.GetTemplate("jsl_hook");
ASSERT_TRUE(tmpl_result.ok());
std::map<std::string, std::string> params = {
{"HOOK_ADDRESS", "008040"},
{"LABEL", "MyHook"},
{"NOP_FILL", "NOP\n NOP"},
};
std::string code =
tool.SubstitutePlaceholders(tmpl_result->code_template, params);
EXPECT_THAT(code, HasSubstr("org $008040"));
EXPECT_THAT(code, HasSubstr("JSL MyHook"));
}
} // namespace
} // namespace tools
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,362 @@
/**
* @file memory_inspector_tool_test.cc
* @brief Unit tests for the MemoryInspectorTool AI agent tools
*
* Tests the memory inspection functionality including analyzing,
* searching, comparing, checking, and region listing tools.
*/
#include "cli/service/agent/tools/memory_inspector_tool.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
namespace cli {
namespace agent {
namespace tools {
namespace {
using ::testing::Contains;
using ::testing::Ge;
using ::testing::HasSubstr;
using ::testing::Le;
using ::testing::Not;
using ::testing::SizeIs;
// =============================================================================
// ALTTPMemoryMap Tests
// =============================================================================
TEST(ALTTPMemoryMapTest, WRAMBoundsAreCorrect) {
EXPECT_EQ(ALTTPMemoryMap::kWRAMStart, 0x7E0000u);
EXPECT_EQ(ALTTPMemoryMap::kWRAMEnd, 0x7FFFFFu);
}
TEST(ALTTPMemoryMapTest, IsWRAMDetectsValidAddresses) {
// Valid WRAM addresses
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7E0000));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7E8000));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7FFFFF));
// Invalid addresses (outside WRAM)
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x000000));
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x7DFFFF));
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x800000));
}
TEST(ALTTPMemoryMapTest, IsSpriteTableDetectsValidAddresses) {
// Valid sprite table addresses
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0D00));
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0D50));
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0FFF));
// Invalid addresses (outside sprite table)
EXPECT_FALSE(ALTTPMemoryMap::IsSpriteTable(0x7E0CFF));
EXPECT_FALSE(ALTTPMemoryMap::IsSpriteTable(0x7E1000));
}
TEST(ALTTPMemoryMapTest, IsSaveDataDetectsValidAddresses) {
// Valid save data addresses
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF000));
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF360)); // Rupees
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF4FF));
// Invalid addresses (outside save data)
EXPECT_FALSE(ALTTPMemoryMap::IsSaveData(0x7EEFFF));
EXPECT_FALSE(ALTTPMemoryMap::IsSaveData(0x7EF500));
}
TEST(ALTTPMemoryMapTest, KnownAddressesAreInWRAM) {
// All known addresses should be in WRAM range
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kGameMode));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kSubmodule));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kLinkXLow));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kLinkYLow));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kPlayerHealth));
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kSpriteType));
}
TEST(ALTTPMemoryMapTest, MaxSpritesIsCorrect) {
EXPECT_EQ(ALTTPMemoryMap::kMaxSprites, 16);
}
TEST(ALTTPMemoryMapTest, PlayerAddressesAreConsistent) {
// X and Y coordinates should be adjacent
EXPECT_EQ(ALTTPMemoryMap::kLinkXHigh - ALTTPMemoryMap::kLinkXLow, 1u);
EXPECT_EQ(ALTTPMemoryMap::kLinkYHigh - ALTTPMemoryMap::kLinkYLow, 1u);
}
// =============================================================================
// MemoryRegionInfo Structure Tests
// =============================================================================
TEST(MemoryRegionInfoTest, StructureHasExpectedFields) {
MemoryRegionInfo info;
info.name = "Test Region";
info.description = "Test description";
info.start_address = 0x7E0000;
info.end_address = 0x7EFFFF;
info.data_type = "byte";
EXPECT_EQ(info.name, "Test Region");
EXPECT_EQ(info.description, "Test description");
EXPECT_EQ(info.start_address, 0x7E0000u);
EXPECT_EQ(info.end_address, 0x7EFFFFu);
EXPECT_EQ(info.data_type, "byte");
}
// =============================================================================
// MemoryAnomaly Structure Tests
// =============================================================================
TEST(MemoryAnomalyTest, StructureHasExpectedFields) {
MemoryAnomaly anomaly;
anomaly.address = 0x7E0D00;
anomaly.type = "out_of_bounds";
anomaly.description = "Sprite X position out of bounds";
anomaly.severity = 3;
EXPECT_EQ(anomaly.address, 0x7E0D00u);
EXPECT_EQ(anomaly.type, "out_of_bounds");
EXPECT_THAT(anomaly.description, HasSubstr("Sprite"));
EXPECT_THAT(anomaly.severity, Ge(1));
EXPECT_THAT(anomaly.severity, Le(5));
}
// =============================================================================
// PatternMatch Structure Tests
// =============================================================================
TEST(PatternMatchTest, StructureHasExpectedFields) {
PatternMatch match;
match.address = 0x7E0D00;
match.matched_bytes = {0x12, 0x34, 0x56};
match.context = "Sprite Table";
EXPECT_EQ(match.address, 0x7E0D00u);
EXPECT_THAT(match.matched_bytes, SizeIs(3));
EXPECT_EQ(match.context, "Sprite Table");
}
// =============================================================================
// MemoryAnalyzeTool Tests
// =============================================================================
TEST(MemoryAnalyzeToolTest, GetNameReturnsCorrectName) {
MemoryAnalyzeTool tool;
EXPECT_EQ(tool.GetName(), "memory-analyze");
}
TEST(MemoryAnalyzeToolTest, GetUsageContainsAddress) {
MemoryAnalyzeTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
}
TEST(MemoryAnalyzeToolTest, GetUsageContainsLength) {
MemoryAnalyzeTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--length"));
}
TEST(MemoryAnalyzeToolTest, GetDescriptionIsNotEmpty) {
MemoryAnalyzeTool tool;
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
}
TEST(MemoryAnalyzeToolTest, DoesNotRequireLabels) {
MemoryAnalyzeTool tool;
EXPECT_FALSE(tool.RequiresLabels());
}
// =============================================================================
// MemorySearchTool Tests
// =============================================================================
TEST(MemorySearchToolTest, GetNameReturnsCorrectName) {
MemorySearchTool tool;
EXPECT_EQ(tool.GetName(), "memory-search");
}
TEST(MemorySearchToolTest, GetUsageContainsPattern) {
MemorySearchTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--pattern"));
}
TEST(MemorySearchToolTest, GetUsageContainsStartEnd) {
MemorySearchTool tool;
std::string usage = tool.GetUsage();
EXPECT_THAT(usage, HasSubstr("--start"));
EXPECT_THAT(usage, HasSubstr("--end"));
}
TEST(MemorySearchToolTest, GetDescriptionIsNotEmpty) {
MemorySearchTool tool;
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
}
TEST(MemorySearchToolTest, DoesNotRequireLabels) {
MemorySearchTool tool;
EXPECT_FALSE(tool.RequiresLabels());
}
// =============================================================================
// MemoryCompareTool Tests
// =============================================================================
TEST(MemoryCompareToolTest, GetNameReturnsCorrectName) {
MemoryCompareTool tool;
EXPECT_EQ(tool.GetName(), "memory-compare");
}
TEST(MemoryCompareToolTest, GetUsageContainsAddress) {
MemoryCompareTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
}
TEST(MemoryCompareToolTest, GetUsageContainsExpected) {
MemoryCompareTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--expected"));
}
TEST(MemoryCompareToolTest, GetDescriptionIsNotEmpty) {
MemoryCompareTool tool;
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
}
TEST(MemoryCompareToolTest, DoesNotRequireLabels) {
MemoryCompareTool tool;
EXPECT_FALSE(tool.RequiresLabels());
}
// =============================================================================
// MemoryCheckTool Tests
// =============================================================================
TEST(MemoryCheckToolTest, GetNameReturnsCorrectName) {
MemoryCheckTool tool;
EXPECT_EQ(tool.GetName(), "memory-check");
}
TEST(MemoryCheckToolTest, GetUsageContainsRegion) {
MemoryCheckTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--region"));
}
TEST(MemoryCheckToolTest, GetDescriptionIsNotEmpty) {
MemoryCheckTool tool;
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
}
TEST(MemoryCheckToolTest, DoesNotRequireLabels) {
MemoryCheckTool tool;
EXPECT_FALSE(tool.RequiresLabels());
}
// =============================================================================
// MemoryRegionsTool Tests
// =============================================================================
TEST(MemoryRegionsToolTest, GetNameReturnsCorrectName) {
MemoryRegionsTool tool;
EXPECT_EQ(tool.GetName(), "memory-regions");
}
TEST(MemoryRegionsToolTest, GetUsageContainsFilter) {
MemoryRegionsTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--filter"));
}
TEST(MemoryRegionsToolTest, GetUsageContainsFormat) {
MemoryRegionsTool tool;
EXPECT_THAT(tool.GetUsage(), HasSubstr("--format"));
}
TEST(MemoryRegionsToolTest, GetDescriptionIsNotEmpty) {
MemoryRegionsTool tool;
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
}
TEST(MemoryRegionsToolTest, DoesNotRequireLabels) {
MemoryRegionsTool tool;
EXPECT_FALSE(tool.RequiresLabels());
}
// =============================================================================
// Tool Name Uniqueness Tests
// =============================================================================
TEST(MemoryToolsTest, AllToolNamesAreUnique) {
MemoryAnalyzeTool analyze;
MemorySearchTool search;
MemoryCompareTool compare;
MemoryCheckTool check;
MemoryRegionsTool regions;
std::vector<std::string> names = {
analyze.GetName(), search.GetName(), compare.GetName(),
check.GetName(), regions.GetName()};
// Check all names are unique
std::set<std::string> unique_names(names.begin(), names.end());
EXPECT_EQ(unique_names.size(), names.size())
<< "All memory tool names should be unique";
}
TEST(MemoryToolsTest, AllToolNamesStartWithMemory) {
MemoryAnalyzeTool analyze;
MemorySearchTool search;
MemoryCompareTool compare;
MemoryCheckTool check;
MemoryRegionsTool regions;
// All memory tools should have names starting with "memory-"
EXPECT_THAT(analyze.GetName(), HasSubstr("memory-"));
EXPECT_THAT(search.GetName(), HasSubstr("memory-"));
EXPECT_THAT(compare.GetName(), HasSubstr("memory-"));
EXPECT_THAT(check.GetName(), HasSubstr("memory-"));
EXPECT_THAT(regions.GetName(), HasSubstr("memory-"));
}
// =============================================================================
// Memory Address Constants Validation
// =============================================================================
TEST(ALTTPMemoryMapTest, SpriteTableAddressesAreSequential) {
// Sprite tables should be at sequential offsets
uint32_t sprite_y_low = ALTTPMemoryMap::kSpriteYLow;
uint32_t sprite_x_low = ALTTPMemoryMap::kSpriteXLow;
uint32_t sprite_y_high = ALTTPMemoryMap::kSpriteYHigh;
uint32_t sprite_x_high = ALTTPMemoryMap::kSpriteXHigh;
// Each table is 16 bytes (one per sprite)
EXPECT_EQ(sprite_x_low - sprite_y_low, 0x10u);
EXPECT_EQ(sprite_y_high - sprite_x_low, 0x10u);
EXPECT_EQ(sprite_x_high - sprite_y_high, 0x10u);
}
TEST(ALTTPMemoryMapTest, OAMBufferSizeIsCorrect) {
uint32_t oam_size =
ALTTPMemoryMap::kOAMBufferEnd - ALTTPMemoryMap::kOAMBuffer + 1;
// OAM buffer should be 544 bytes (512 for main OAM + 32 for high table)
EXPECT_EQ(oam_size, 0x220u); // 544 bytes
}
TEST(ALTTPMemoryMapTest, SRAMRegionSizeIsCorrect) {
uint32_t sram_size =
ALTTPMemoryMap::kSRAMEnd - ALTTPMemoryMap::kSRAMStart + 1;
// SRAM region should be 0x500 bytes (1280 bytes)
EXPECT_EQ(sram_size, 0x500u);
}
} // namespace
} // namespace tools
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,555 @@
/**
* @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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#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<uint8_t> data = {0x00, 0x01, 0x02, 0x03};
auto hash = ProjectToolUtils::ComputeSHA256(data.data(), data.size());
EXPECT_EQ(hash.size(), 32u);
}
TEST(ProjectToolUtilsTest, ComputeSHA256IsDeterministic) {
std::vector<uint8_t> 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<uint8_t> data1 = {0x00, 0x01, 0x02};
std::vector<uint8_t> 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<uint8_t, 32> checksum;
checksum.fill(0xAB);
std::string formatted = ProjectToolUtils::FormatChecksum(checksum);
EXPECT_EQ(formatted.size(), 64u);
}
TEST(ProjectToolUtilsTest, FormatChecksumIsHex) {
std::array<uint8_t, 32> checksum;
for (size_t i = 0; i < 32; ++i) {
checksum[i] = static_cast<uint8_t>(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<std::chrono::hours>(
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<uint8_t, 32> 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<uint8_t, 32> 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<uint8_t, 32> 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<uint8_t, 32> 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<std::string> names = {
status.GetName(), snapshot.GetName(), restore.GetName(),
export_tool.GetName(), import_tool.GetName(), diff.GetName()};
std::set<std::string> 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

View File

@@ -0,0 +1,311 @@
/**
* @file visual_analysis_tool_test.cc
* @brief Unit tests for visual analysis tools
*/
#include "cli/service/agent/tools/visual_analysis_tool.h"
#include <gtest/gtest.h>
#include <cmath>
#include <vector>
namespace yaze {
namespace cli {
namespace agent {
namespace tools {
namespace {
// Test fixture for VisualAnalysisBase helper functions
class VisualAnalysisBaseTest : public ::testing::Test {
protected:
// Create a test subclass to access protected methods
class TestableVisualAnalysis : public VisualAnalysisBase {
public:
std::string GetName() const override { return "test-visual-analysis"; }
std::string GetUsage() const override { return "test usage"; }
// Expose protected methods for testing
using VisualAnalysisBase::ComputePixelDifference;
using VisualAnalysisBase::ComputeStructuralSimilarity;
using VisualAnalysisBase::IsRegionEmpty;
using VisualAnalysisBase::FormatMatchesAsJson;
using VisualAnalysisBase::FormatRegionsAsJson;
protected:
absl::Status ValidateArgs(
const resources::ArgumentParser& /*parser*/) override {
return absl::OkStatus();
}
absl::Status Execute(Rom* /*rom*/,
const resources::ArgumentParser& /*parser*/,
resources::OutputFormatter& /*formatter*/) override {
return absl::OkStatus();
}
};
TestableVisualAnalysis tool_;
};
// =============================================================================
// ComputePixelDifference Tests
// =============================================================================
TEST_F(VisualAnalysisBaseTest, PixelDifference_IdenticalTiles_Returns100) {
std::vector<uint8_t> tile(64, 0x42); // All pixels same value
double similarity = tool_.ComputePixelDifference(tile, tile);
EXPECT_DOUBLE_EQ(similarity, 100.0);
}
TEST_F(VisualAnalysisBaseTest, PixelDifference_CompletelyDifferent_Returns0) {
std::vector<uint8_t> tile_a(64, 0x00); // All black
std::vector<uint8_t> tile_b(64, 0xFF); // All white
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
EXPECT_DOUBLE_EQ(similarity, 0.0);
}
TEST_F(VisualAnalysisBaseTest, PixelDifference_HalfDifferent_Returns50) {
std::vector<uint8_t> tile_a(64, 0x00);
std::vector<uint8_t> tile_b(64, 0x00);
// Make half the pixels maximally different
for (int i = 0; i < 32; ++i) {
tile_b[i] = 0xFF;
}
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
EXPECT_NEAR(similarity, 50.0, 0.01);
}
TEST_F(VisualAnalysisBaseTest, PixelDifference_EmptyTiles_Returns0) {
std::vector<uint8_t> empty;
double similarity = tool_.ComputePixelDifference(empty, empty);
EXPECT_DOUBLE_EQ(similarity, 0.0);
}
TEST_F(VisualAnalysisBaseTest, PixelDifference_DifferentSizes_Returns0) {
std::vector<uint8_t> tile_a(64, 0x42);
std::vector<uint8_t> tile_b(32, 0x42);
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
EXPECT_DOUBLE_EQ(similarity, 0.0);
}
// =============================================================================
// ComputeStructuralSimilarity Tests
// =============================================================================
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_IdenticalTiles_Returns100) {
std::vector<uint8_t> tile(64, 0x42);
double similarity = tool_.ComputeStructuralSimilarity(tile, tile);
EXPECT_GE(similarity, 99.0); // Should be very close to 100
}
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_DifferentTiles_ReturnsLow) {
std::vector<uint8_t> tile_a(64, 0x00);
std::vector<uint8_t> tile_b(64, 0xFF);
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
EXPECT_LT(similarity, 50.0);
}
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_SimilarPattern_ReturnsHigh) {
// Create two tiles with similar structure but slightly different values
std::vector<uint8_t> tile_a(64);
std::vector<uint8_t> tile_b(64);
for (int i = 0; i < 64; ++i) {
tile_a[i] = i % 16; // Pattern 0-15 repeated
tile_b[i] = (i % 16) + 1; // Same pattern, shifted by 1
}
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
EXPECT_GT(similarity, 80.0); // Should be high due to similar structure
}
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_EmptyTiles_Returns0) {
std::vector<uint8_t> empty;
double similarity = tool_.ComputeStructuralSimilarity(empty, empty);
EXPECT_DOUBLE_EQ(similarity, 0.0);
}
// =============================================================================
// IsRegionEmpty Tests
// =============================================================================
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_AllZeros_ReturnsTrue) {
std::vector<uint8_t> data(64, 0x00);
EXPECT_TRUE(tool_.IsRegionEmpty(data));
}
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_AllFF_ReturnsTrue) {
std::vector<uint8_t> data(64, 0xFF);
EXPECT_TRUE(tool_.IsRegionEmpty(data));
}
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_MostlyZeros_ReturnsTrue) {
std::vector<uint8_t> data(100, 0x00);
data[0] = 0x01; // Only 1% non-zero
EXPECT_TRUE(tool_.IsRegionEmpty(data)); // >95% zeros
}
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_HalfFilled_ReturnsFalse) {
std::vector<uint8_t> data(64, 0x00);
for (int i = 0; i < 32; ++i) {
data[i] = 0x42;
}
EXPECT_FALSE(tool_.IsRegionEmpty(data));
}
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_CompletelyFilled_ReturnsFalse) {
std::vector<uint8_t> data(64, 0x42);
EXPECT_FALSE(tool_.IsRegionEmpty(data));
}
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_EmptyVector_ReturnsTrue) {
std::vector<uint8_t> data;
EXPECT_TRUE(tool_.IsRegionEmpty(data));
}
// =============================================================================
// JSON Formatting Tests
// =============================================================================
TEST_F(VisualAnalysisBaseTest, FormatMatchesAsJson_EmptyList_ReturnsValidJson) {
std::vector<TileSimilarityMatch> matches;
std::string json = tool_.FormatMatchesAsJson(matches);
EXPECT_TRUE(json.find("\"matches\": []") != std::string::npos);
EXPECT_TRUE(json.find("\"total_matches\": 0") != std::string::npos);
}
TEST_F(VisualAnalysisBaseTest, FormatMatchesAsJson_SingleMatch_ReturnsValidJson) {
std::vector<TileSimilarityMatch> matches = {
{.tile_id = 42, .similarity_score = 95.5, .sheet_index = 1,
.x_position = 16, .y_position = 8}
};
std::string json = tool_.FormatMatchesAsJson(matches);
EXPECT_TRUE(json.find("\"tile_id\": 42") != std::string::npos);
EXPECT_TRUE(json.find("\"similarity_score\": 95.50") != std::string::npos);
EXPECT_TRUE(json.find("\"sheet_index\": 1") != std::string::npos);
EXPECT_TRUE(json.find("\"total_matches\": 1") != std::string::npos);
}
TEST_F(VisualAnalysisBaseTest, FormatRegionsAsJson_EmptyList_ReturnsValidJson) {
std::vector<UnusedRegion> regions;
std::string json = tool_.FormatRegionsAsJson(regions);
EXPECT_TRUE(json.find("\"unused_regions\": []") != std::string::npos);
EXPECT_TRUE(json.find("\"total_regions\": 0") != std::string::npos);
EXPECT_TRUE(json.find("\"total_free_tiles\": 0") != std::string::npos);
}
TEST_F(VisualAnalysisBaseTest, FormatRegionsAsJson_SingleRegion_ReturnsValidJson) {
std::vector<UnusedRegion> regions = {
{.sheet_index = 5, .x = 0, .y = 0, .width = 16, .height = 8, .tile_count = 2}
};
std::string json = tool_.FormatRegionsAsJson(regions);
EXPECT_TRUE(json.find("\"sheet_index\": 5") != std::string::npos);
EXPECT_TRUE(json.find("\"width\": 16") != std::string::npos);
EXPECT_TRUE(json.find("\"tile_count\": 2") != std::string::npos);
EXPECT_TRUE(json.find("\"total_free_tiles\": 2") != std::string::npos);
}
// =============================================================================
// TileSimilarityMatch Struct Tests
// =============================================================================
TEST(TileSimilarityMatchTest, DefaultInitialization) {
TileSimilarityMatch match = {};
EXPECT_EQ(match.tile_id, 0);
EXPECT_DOUBLE_EQ(match.similarity_score, 0.0);
EXPECT_EQ(match.sheet_index, 0);
EXPECT_EQ(match.x_position, 0);
EXPECT_EQ(match.y_position, 0);
}
// =============================================================================
// UnusedRegion Struct Tests
// =============================================================================
TEST(UnusedRegionTest, DefaultInitialization) {
UnusedRegion region = {};
EXPECT_EQ(region.sheet_index, 0);
EXPECT_EQ(region.x, 0);
EXPECT_EQ(region.y, 0);
EXPECT_EQ(region.width, 0);
EXPECT_EQ(region.height, 0);
EXPECT_EQ(region.tile_count, 0);
}
// =============================================================================
// PaletteUsageStats Struct Tests
// =============================================================================
TEST(PaletteUsageStatsTest, DefaultInitialization) {
PaletteUsageStats stats = {};
EXPECT_EQ(stats.palette_index, 0);
EXPECT_EQ(stats.usage_count, 0);
EXPECT_DOUBLE_EQ(stats.usage_percentage, 0.0);
EXPECT_TRUE(stats.used_by_maps.empty());
}
// =============================================================================
// TileUsageEntry Struct Tests
// =============================================================================
TEST(TileUsageEntryTest, DefaultInitialization) {
TileUsageEntry entry = {};
EXPECT_EQ(entry.tile_id, 0);
EXPECT_EQ(entry.usage_count, 0);
EXPECT_DOUBLE_EQ(entry.usage_percentage, 0.0);
EXPECT_TRUE(entry.locations.empty());
}
// =============================================================================
// Constants Tests
// =============================================================================
TEST(VisualAnalysisConstantsTest, TileConstants) {
EXPECT_EQ(VisualAnalysisBase::kTileWidth, 8);
EXPECT_EQ(VisualAnalysisBase::kTileHeight, 8);
EXPECT_EQ(VisualAnalysisBase::kTilePixels, 64);
EXPECT_EQ(VisualAnalysisBase::kSheetWidth, 128);
EXPECT_EQ(VisualAnalysisBase::kSheetHeight, 32);
EXPECT_EQ(VisualAnalysisBase::kTilesPerRow, 16);
EXPECT_EQ(VisualAnalysisBase::kMaxSheets, 223);
}
// =============================================================================
// Edge Case Tests
// =============================================================================
TEST_F(VisualAnalysisBaseTest, PixelDifference_LargeTile_HandlesCorrectly) {
// Test with a larger tile (256 pixels = 16x16)
std::vector<uint8_t> tile_a(256, 0x80);
std::vector<uint8_t> tile_b(256, 0x80);
tile_b[0] = 0x00; // Single pixel difference
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
// (255/256 pixels same) = 99.6% similar after accounting for intensity diff
EXPECT_GT(similarity, 99.0);
}
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_GradientPattern_MatchesWell) {
// Create gradient patterns
std::vector<uint8_t> tile_a(64);
std::vector<uint8_t> tile_b(64);
for (int i = 0; i < 64; ++i) {
tile_a[i] = i * 4; // Gradient 0-252
tile_b[i] = i * 4 + 2; // Same gradient, offset by 2
}
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
EXPECT_GT(similarity, 90.0); // Same structure
}
} // namespace
} // namespace tools
} // namespace agent
} // namespace cli
} // namespace yaze