fix
This commit is contained in:
@@ -316,4 +316,105 @@ public class BudgetApplicationTest : BaseApplicationTest
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetCategoryStatsAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStatsAsync_Should_Include_Trend_And_Description_In_Month_Stats()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2026, 2, 14);
|
||||
var category = BudgetCategory.Expense;
|
||||
|
||||
var serviceResponse = new BudgetCategoryStats
|
||||
{
|
||||
Month = new BudgetStatsDto
|
||||
{
|
||||
Limit = 3000,
|
||||
Current = 1200,
|
||||
Rate = 40,
|
||||
Trend = new List<decimal?> { 100, 200, 300, 400, 500, null, null },
|
||||
Description = "<table><tr><th>日期</th><th>金额</th></tr></table>"
|
||||
},
|
||||
Year = new BudgetStatsDto
|
||||
{
|
||||
Limit = 36000,
|
||||
Current = 5000,
|
||||
Rate = 13.89m,
|
||||
Trend = new List<decimal?> { 1000, 2000, 3000, null },
|
||||
Description = "<table><tr><th>月份</th><th>金额</th></tr></table>"
|
||||
}
|
||||
};
|
||||
|
||||
_budgetService.GetCategoryStatsAsync(category, referenceDate).Returns(serviceResponse);
|
||||
|
||||
// Act
|
||||
var result = await _application.GetCategoryStatsAsync(category, referenceDate);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
|
||||
// 验证 Month 数据
|
||||
result.Month.Limit.Should().Be(3000);
|
||||
result.Month.Current.Should().Be(1200);
|
||||
result.Month.Remaining.Should().Be(1800);
|
||||
result.Month.UsagePercentage.Should().Be(40);
|
||||
result.Month.Trend.Should().NotBeNull();
|
||||
result.Month.Trend.Should().HaveCount(7);
|
||||
result.Month.Trend[0].Should().Be(100);
|
||||
result.Month.Trend[5].Should().BeNull();
|
||||
result.Month.Description.Should().NotBeEmpty();
|
||||
result.Month.Description.Should().Contain("<table>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStatsAsync_Should_Include_Trend_And_Description_In_Year_Stats()
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2026, 2, 14);
|
||||
var category = BudgetCategory.Income;
|
||||
|
||||
var serviceResponse = new BudgetCategoryStats
|
||||
{
|
||||
Month = new BudgetStatsDto
|
||||
{
|
||||
Limit = 5000,
|
||||
Current = 3000,
|
||||
Rate = 60,
|
||||
Trend = new List<decimal?> { 500, 1000, 1500, 2000, 2500, 3000 },
|
||||
Description = "<p>月度收入明细</p>"
|
||||
},
|
||||
Year = new BudgetStatsDto
|
||||
{
|
||||
Limit = 60000,
|
||||
Current = 10000,
|
||||
Rate = 16.67m,
|
||||
Trend = new List<decimal?> { 5000, 10000, null, null, null, null, null, null, null, null, null, null },
|
||||
Description = "<p>年度收入明细</p>"
|
||||
}
|
||||
};
|
||||
|
||||
_budgetService.GetCategoryStatsAsync(category, referenceDate).Returns(serviceResponse);
|
||||
|
||||
// Act
|
||||
var result = await _application.GetCategoryStatsAsync(category, referenceDate);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
|
||||
// 验证 Year 数据
|
||||
result.Year.Limit.Should().Be(60000);
|
||||
result.Year.Current.Should().Be(10000);
|
||||
result.Year.Remaining.Should().Be(50000);
|
||||
result.Year.UsagePercentage.Should().Be(16.67m);
|
||||
result.Year.Trend.Should().NotBeNull();
|
||||
result.Year.Trend.Should().HaveCount(12);
|
||||
result.Year.Trend[0].Should().Be(5000);
|
||||
result.Year.Trend[1].Should().Be(10000);
|
||||
result.Year.Trend[2].Should().BeNull();
|
||||
result.Year.Description.Should().NotBeEmpty();
|
||||
result.Year.Description.Should().Contain("年度收入明细");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -441,4 +441,82 @@ public class TransactionCategoryApplicationTest : BaseApplicationTest
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DeleteIconAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteIconAsync_存在的分类_应成功删除图标()
|
||||
{
|
||||
// Arrange
|
||||
var category = new TransactionCategory
|
||||
{
|
||||
Id = 1,
|
||||
Name = "测试",
|
||||
Type = TransactionType.Expense,
|
||||
Icon = """["<svg>icon1</svg>","<svg>icon2</svg>"]"""
|
||||
};
|
||||
_categoryRepository.GetByIdAsync(1).Returns(category);
|
||||
_categoryRepository.UpdateAsync(Arg.Any<TransactionCategory>()).Returns(true);
|
||||
|
||||
// Act
|
||||
await _application.DeleteIconAsync(1);
|
||||
|
||||
// Assert
|
||||
await _categoryRepository.Received(1).UpdateAsync(Arg.Is<TransactionCategory>(
|
||||
c => c.Id == 1 && c.Icon == null
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteIconAsync_分类不存在_应抛出NotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
_categoryRepository.GetByIdAsync(999).Returns((TransactionCategory?)null);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _application.DeleteIconAsync(999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteIconAsync_更新失败_应抛出BusinessException()
|
||||
{
|
||||
// Arrange
|
||||
var category = new TransactionCategory
|
||||
{
|
||||
Id = 1,
|
||||
Name = "测试",
|
||||
Type = TransactionType.Expense,
|
||||
Icon = """["<svg>icon1</svg>"]"""
|
||||
};
|
||||
_categoryRepository.GetByIdAsync(1).Returns(category);
|
||||
_categoryRepository.UpdateAsync(Arg.Any<TransactionCategory>()).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BusinessException>(() => _application.DeleteIconAsync(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteIconAsync_无图标的分类_应成功()
|
||||
{
|
||||
// Arrange
|
||||
var category = new TransactionCategory
|
||||
{
|
||||
Id = 1,
|
||||
Name = "测试",
|
||||
Type = TransactionType.Expense,
|
||||
Icon = null
|
||||
};
|
||||
_categoryRepository.GetByIdAsync(1).Returns(category);
|
||||
_categoryRepository.UpdateAsync(Arg.Any<TransactionCategory>()).Returns(true);
|
||||
|
||||
// Act
|
||||
await _application.DeleteIconAsync(1);
|
||||
|
||||
// Assert
|
||||
await _categoryRepository.Received(1).UpdateAsync(Arg.Is<TransactionCategory>(
|
||||
c => c.Id == 1 && c.Icon == null
|
||||
));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
467
WebApi.Test/Service/ClassificationIconPromptProviderTest.cs
Normal file
467
WebApi.Test/Service/ClassificationIconPromptProviderTest.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Service.AI;
|
||||
using Service.AppSettingModel;
|
||||
|
||||
namespace WebApi.Test.Service;
|
||||
|
||||
/// <summary>
|
||||
/// ClassificationIconPromptProvider 单元测试
|
||||
/// </summary>
|
||||
public class ClassificationIconPromptProviderTest : BaseTest
|
||||
{
|
||||
private readonly ClassificationIconPromptProvider _provider;
|
||||
private readonly ILogger<ClassificationIconPromptProvider> _logger;
|
||||
private readonly IOptions<IconPromptSettings> _config;
|
||||
|
||||
public ClassificationIconPromptProviderTest()
|
||||
{
|
||||
_logger = Substitute.For<ILogger<ClassificationIconPromptProvider>>();
|
||||
_config = Options.Create(new IconPromptSettings
|
||||
{
|
||||
EnableNewPrompt = true,
|
||||
GrayScaleRatio = 1.0,
|
||||
StyleStrength = 0.7,
|
||||
ColorScheme = "single-color"
|
||||
});
|
||||
_provider = new ClassificationIconPromptProvider(_logger, _config);
|
||||
}
|
||||
|
||||
#region GetPrompt Tests
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_支出分类_应包含正确的上下文信息()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain(categoryName);
|
||||
prompt.Should().Contain("支出");
|
||||
prompt.Should().Contain("分类名称");
|
||||
prompt.Should().Contain("分类类型");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_收入分类_应包含正确的上下文信息()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "工资";
|
||||
const TransactionType categoryType = TransactionType.Income;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain(categoryName);
|
||||
prompt.Should().Contain("收入");
|
||||
prompt.Should().Contain("分类名称");
|
||||
prompt.Should().Contain("分类类型");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_不计入收支分类_应包含正确的上下文信息()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "转账";
|
||||
const TransactionType categoryType = TransactionType.None;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain(categoryName);
|
||||
prompt.Should().Contain("不计入收支");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_应包含设计要求()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("24x24");
|
||||
prompt.Should().Contain("viewBox=\"0 0 24 24\"");
|
||||
prompt.Should().Contain("扁平化、单色、极致简约");
|
||||
prompt.Should().Contain("避免渐变和阴影");
|
||||
prompt.Should().Contain("保持线条简洁");
|
||||
prompt.Should().Contain("高对比度");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_应包含SVG格式要求()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "购物";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("JSON 数组格式");
|
||||
prompt.Should().Contain("5 个完整的 SVG 字符串");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_应包含返回格式说明()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "娱乐";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("[\"<svg>...</svg>\", \"<svg>...</svg>\", \"<svg>...</svg>\", \"<svg>...</svg>\", \"<svg>...</svg>\"]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_多次调用_应生成相同的提示词结构()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt1 = _provider.GetPrompt(categoryName, categoryType);
|
||||
var prompt2 = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt1.Should().Be(prompt2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_不同分类_应生成不同的提示词()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName1 = "餐饮";
|
||||
const string categoryName2 = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt1 = _provider.GetPrompt(categoryName1, categoryType);
|
||||
var prompt2 = _provider.GetPrompt(categoryName2, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt1.Should().NotBe(prompt2);
|
||||
prompt1.Should().Contain(categoryName1);
|
||||
prompt2.Should().Contain(categoryName2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSingleIconPrompt Tests
|
||||
|
||||
[Fact]
|
||||
public void GetSingleIconPrompt_支出分类_应包含正确的上下文信息()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetSingleIconPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain(categoryName);
|
||||
prompt.Should().Contain("支出");
|
||||
prompt.Should().Contain("分类名称");
|
||||
prompt.Should().Contain("分类类型");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSingleIconPrompt_应包含单个图标要求()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetSingleIconPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("生成 1 个");
|
||||
prompt.Should().Contain("24x24");
|
||||
prompt.Should().Contain("viewBox=\"0 0 24 24\"");
|
||||
prompt.Should().Contain("扁平化、单色、极致简约");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSingleIconPrompt_应包含返回格式说明()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "购物";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetSingleIconPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("JSON 数组格式");
|
||||
prompt.Should().Contain("1 个完整的 SVG 字符串");
|
||||
prompt.Should().Contain("[\"<svg>...</svg>\"]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSingleIconPrompt_应与GetPrompt生成不同的提示词()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var singlePrompt = _provider.GetSingleIconPrompt(categoryName, categoryType);
|
||||
var multiPrompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
singlePrompt.Should().NotBe(multiPrompt);
|
||||
singlePrompt.Should().Contain("生成 1 个");
|
||||
multiPrompt.Should().Contain("生成 5 个");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PromptTemplateEngine Tests (3.1)
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplacePlaceholders_应正确替换单个占位符()
|
||||
{
|
||||
// Arrange
|
||||
var template = "分类名称:{{category_name}},类型:{{category_type}}";
|
||||
var placeholders = new Dictionary<string, string>
|
||||
{
|
||||
["category_name"] = "餐饮",
|
||||
["category_type"] = "支出"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplacePlaceholders(template, placeholders);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("分类名称:餐饮,类型:支出");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplacePlaceholders_应正确替换多个占位符()
|
||||
{
|
||||
// Arrange
|
||||
var template = "颜色方案:{{color_scheme}},简约度:{{style_strength}}";
|
||||
var placeholders = new Dictionary<string, string>
|
||||
{
|
||||
["color_scheme"] = "single-color",
|
||||
["style_strength"] = "0.8 - 高度简约"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplacePlaceholders(template, placeholders);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("颜色方案:single-color,简约度:0.8 - 高度简约");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplacePlaceholders_空模板应返回原字符串()
|
||||
{
|
||||
// Arrange
|
||||
var template = string.Empty;
|
||||
var placeholders = new Dictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplacePlaceholders(template, placeholders);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplacePlaceholders_空占位符字典应返回原字符串()
|
||||
{
|
||||
// Arrange
|
||||
var template = "测试模板字符串";
|
||||
var placeholders = new Dictionary<string, string>();
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplacePlaceholders(template, placeholders);
|
||||
|
||||
// Assert
|
||||
result.Should().Be("测试模板字符串");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 新版提示词生成逻辑测试 (3.2)
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_新版提示词_应包含简约风格要求()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("扁平化、单色、极致简约");
|
||||
prompt.Should().Contain("避免渐变和阴影");
|
||||
prompt.Should().Contain("保持线条简洁");
|
||||
prompt.Should().Contain("高对比度");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_新版提示词_应包含颜色方案占位符替换()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("颜色方案:single-color");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrompt_新版提示词_应包含风格强度描述()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "娱乐";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
// Act
|
||||
var prompt = _provider.GetPrompt(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
prompt.Should().Contain("简约度:0.7");
|
||||
prompt.Should().Contain("高度简约(去除所有装饰)");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 抽象分类特殊处理测试 (3.3)
|
||||
|
||||
[Fact]
|
||||
public void IconPromptSettings_初始化_应包含默认抽象分类配置()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new IconPromptSettings();
|
||||
|
||||
// Assert
|
||||
config.AbstractCategories.Should().ContainKey("其他");
|
||||
config.AbstractCategories.Should().ContainKey("通用");
|
||||
config.AbstractCategories.Should().ContainKey("未知");
|
||||
config.AbstractCategories["其他"].GeometryShape.Should().Be("circle");
|
||||
config.AbstractCategories["其他"].ColorCode.Should().Be("#9E9E9E");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IconPromptSettings_抽象分类配置_可以自定义()
|
||||
{
|
||||
// Arrange
|
||||
var config = new IconPromptSettings();
|
||||
|
||||
// Act
|
||||
config.AbstractCategories["自定义分类"] = new AbstractCategoryConfig
|
||||
{
|
||||
GeometryShape = "hexagon",
|
||||
ColorCode = "#FF5722"
|
||||
};
|
||||
|
||||
// Assert
|
||||
config.AbstractCategories["自定义分类"].GeometryShape.Should().Be("hexagon");
|
||||
config.AbstractCategories["自定义分类"].ColorCode.Should().Be("#FF5722");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 风格强度参数注入测试 (3.4)
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplaceForIconGeneration_风格强度09_应显示极度简约()
|
||||
{
|
||||
// Arrange
|
||||
var template = "简约度:{{style_strength}}";
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplaceForIconGeneration(
|
||||
template,
|
||||
"餐饮",
|
||||
"支出",
|
||||
"single-color",
|
||||
0.95);
|
||||
|
||||
// Assert
|
||||
result.Should().Contain("0.9");
|
||||
result.Should().Contain("极度简约(仅保留最核心元素)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplaceForIconGeneration_风格强度07_应显示高度简约()
|
||||
{
|
||||
// Arrange
|
||||
var template = "简约度:{{style_strength}}";
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplaceForIconGeneration(
|
||||
template,
|
||||
"交通",
|
||||
"支出",
|
||||
"single-color",
|
||||
0.7);
|
||||
|
||||
// Assert
|
||||
result.Should().Contain("0.7");
|
||||
result.Should().Contain("高度简约(去除所有装饰)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplaceForIconGeneration_风格强度05_应显示简约()
|
||||
{
|
||||
// Arrange
|
||||
var template = "简约度:{{style_strength}}";
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplaceForIconGeneration(
|
||||
template,
|
||||
"购物",
|
||||
"支出",
|
||||
"single-color",
|
||||
0.5);
|
||||
|
||||
// Assert
|
||||
result.Should().Contain("0.5");
|
||||
result.Should().Contain("简约(保留必要细节)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptTemplateEngine_ReplaceForIconGeneration_风格强度03_应显示适中()
|
||||
{
|
||||
// Arrange
|
||||
var template = "简约度:{{style_strength}}";
|
||||
|
||||
// Act
|
||||
var result = PromptTemplateEngine.ReplaceForIconGeneration(
|
||||
template,
|
||||
"娱乐",
|
||||
"支出",
|
||||
"single-color",
|
||||
0.3);
|
||||
|
||||
// Assert
|
||||
result.Should().Contain("0.3");
|
||||
result.Should().Contain("适中");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
263
WebApi.Test/Service/SmartHandleServiceTest.cs
Normal file
263
WebApi.Test/Service/SmartHandleServiceTest.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using NSubstitute;
|
||||
using Service.AI;
|
||||
using Service.Transaction;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WebApi.Test.Service;
|
||||
|
||||
/// <summary>
|
||||
/// SmartHandleService 单元测试
|
||||
/// </summary>
|
||||
public class SmartHandleServiceTest : BaseTest
|
||||
{
|
||||
private readonly IOpenAiService _openAiService;
|
||||
private readonly ITransactionRecordRepository _transactionRecordRepository;
|
||||
private readonly ITransactionStatisticsService _transactionStatisticsService;
|
||||
private readonly ITextSegmentService _textSegmentService;
|
||||
private readonly ILogger<SmartHandleService> _logger;
|
||||
private readonly ITransactionCategoryRepository _categoryRepository;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IClassificationIconPromptProvider _iconPromptProvider;
|
||||
private readonly SmartHandleService _service;
|
||||
|
||||
public SmartHandleServiceTest()
|
||||
{
|
||||
_openAiService = Substitute.For<IOpenAiService>();
|
||||
_transactionRecordRepository = Substitute.For<ITransactionRecordRepository>();
|
||||
_transactionStatisticsService = Substitute.For<ITransactionStatisticsService>();
|
||||
_textSegmentService = Substitute.For<ITextSegmentService>();
|
||||
_logger = Substitute.For<ILogger<SmartHandleService>>();
|
||||
_categoryRepository = Substitute.For<ITransactionCategoryRepository>();
|
||||
_configService = Substitute.For<IConfigService>();
|
||||
_iconPromptProvider = Substitute.For<IClassificationIconPromptProvider>();
|
||||
|
||||
_service = new SmartHandleService(
|
||||
_transactionRecordRepository,
|
||||
_transactionStatisticsService,
|
||||
_textSegmentService,
|
||||
_logger,
|
||||
_categoryRepository,
|
||||
_openAiService,
|
||||
_configService,
|
||||
_iconPromptProvider
|
||||
);
|
||||
}
|
||||
|
||||
#region GenerateSingleCategoryIconAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSingleCategoryIconAsync_AI返回带Markdown标记的JSON_应成功解析()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const string aiResponseWithMarkdown = """
|
||||
```json
|
||||
["<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>"]
|
||||
```
|
||||
""";
|
||||
|
||||
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(aiResponseWithMarkdown);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().Contain("<svg");
|
||||
result.Should().Contain("viewBox=\"0 0 24 24\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSingleCategoryIconAsync_AI返回纯JSON_应成功解析()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const string aiResponsePureJson = """
|
||||
["<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"5\" y=\"5\" width=\"14\" height=\"14\" fill=\"#4a90e2\"/></svg>"]
|
||||
""";
|
||||
|
||||
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(aiResponsePureJson);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().Contain("<svg");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSingleCategoryIconAsync_AI返回空响应_应返回null()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "购物";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
|
||||
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns((string?)null);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSingleCategoryIconAsync_AI返回无效JSON_应返回null()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "娱乐";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const string invalidJsonResponse = "这是一个文本响应,不是 JSON";
|
||||
|
||||
_iconPromptProvider.GetSingleIconPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(invalidJsonResponse);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateSingleCategoryIconAsync(categoryName, categoryType);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenerateCategoryIconsAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCategoryIconsAsync_AI返回带Markdown标记的JSON_应成功解析()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "餐饮";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const int iconCount = 5;
|
||||
const string aiResponseWithMarkdown = """
|
||||
```json
|
||||
[
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"12\" rx=\"10\" ry=\"6\" fill=\"#6c5ce7\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><polygon points=\"12,2 22,8 22,16 12,22 2,16 2,8\" fill=\"#a29bfe\"/></svg>"
|
||||
]
|
||||
```
|
||||
""";
|
||||
|
||||
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(aiResponseWithMarkdown);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().HaveCount(iconCount);
|
||||
result.Should().AllSatisfy(icon => icon.Should().Contain("<svg"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCategoryIconsAsync_AI返回纯JSON_应成功解析()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "交通";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const int iconCount = 5;
|
||||
const string aiResponsePureJson = """
|
||||
[
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><ellipse cx=\"12\" cy=\"12\" rx=\"10\" ry=\"6\" fill=\"#6c5ce7\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><polygon points=\"12,2 22,8 22,16 12,22 2,16 2,8\" fill=\"#a29bfe\"/></svg>"
|
||||
]
|
||||
""";
|
||||
|
||||
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(aiResponsePureJson);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Should().HaveCount(iconCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCategoryIconsAsync_AI返回空响应_应返回null()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "购物";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const int iconCount = 5;
|
||||
|
||||
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns((string?)null);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCategoryIconsAsync_AI返回图标数量不正确_应返回null()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "娱乐";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const int iconCount = 5;
|
||||
const string aiResponseWith3Icons = """
|
||||
[
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"#ff6b6b\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" fill=\"#4ecdc4\"/></svg>",
|
||||
"<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2L2 22h20L12 2z\" fill=\"#f5a623\"/></svg>"
|
||||
]
|
||||
""";
|
||||
|
||||
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(aiResponseWith3Icons);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCategoryIconsAsync_AI返回无效JSON_应返回null()
|
||||
{
|
||||
// Arrange
|
||||
const string categoryName = "其他";
|
||||
const TransactionType categoryType = TransactionType.Expense;
|
||||
const int iconCount = 5;
|
||||
const string invalidJsonResponse = "这是一个文本响应,不是 JSON";
|
||||
|
||||
_iconPromptProvider.GetPrompt(categoryName, categoryType).Returns("system prompt");
|
||||
_openAiService.ChatAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>())
|
||||
.Returns(invalidJsonResponse);
|
||||
|
||||
// Act
|
||||
var result = await _service.GenerateCategoryIconsAsync(categoryName, categoryType, iconCount);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user