using System.Reflection; using Hua.Todo.Application.Services.Meeting; using Hua.Todo.Application.Services.Meeting.Models; using Hua.Todo.Core.Entities; using Xunit; namespace Hua.Todo.Host.Tests.Meeting; /// /// 工单 03-03 AI 任务拆分服务测试。 /// public class MeetingAiBreakdownTests { [Fact] public void MeetingTaskSuggestion_DefaultProperties() { var suggestion = new MeetingTaskSuggestion { Title = "整理需求文档", Priority = TaskPriority.High, Reason = "会议中提及需要在周五前完成" }; Assert.Equal("整理需求文档", suggestion.Title); Assert.Equal(TaskPriority.High, suggestion.Priority); Assert.Equal("会议中提及需要在周五前完成", suggestion.Reason); } [Fact] public void MeetingTaskSuggestion_DefaultValues() { var suggestion = new MeetingTaskSuggestion(); Assert.Equal(string.Empty, suggestion.Title); Assert.Equal(TaskPriority.Medium, suggestion.Priority); Assert.Equal(string.Empty, suggestion.Reason); } [Fact] public void BreakdownRequest_CanBeCreated() { var taskId = Guid.NewGuid(); var request = new BreakdownRequest { TaskId = taskId, Notes = "会议讨论了三个议题" }; Assert.Equal(taskId, request.TaskId); Assert.Equal("会议讨论了三个议题", request.Notes); } [Fact] public void BreakdownRequest_NotesCanBeNull() { var taskId = Guid.NewGuid(); var request = new BreakdownRequest { TaskId = taskId, Notes = null }; Assert.Equal(taskId, request.TaskId); Assert.Null(request.Notes); } [Fact] public void BreakdownResponse_ContainsCorrectData() { var suggestions = new List { new() { Title = "任务A", Priority = TaskPriority.High, Reason = "原因A" }, new() { Title = "任务B", Priority = TaskPriority.Low, Reason = "原因B" } }; var taskId = Guid.NewGuid(); var response = new BreakdownResponse { TaskId = taskId, Suggestions = suggestions }; Assert.Equal(taskId, response.TaskId); Assert.Equal(2, response.Suggestions.Count); Assert.Equal("任务A", response.Suggestions[0].Title); } [Fact] public void BreakdownResponse_DefaultSuggestionsIsEmpty() { var response = new BreakdownResponse(); Assert.NotNull(response.Suggestions); Assert.Empty(response.Suggestions); } [Fact] public void SubTaskCreateItem_CanBeCreated() { var item = new SubTaskCreateItem { Title = "子任务标题", Priority = TaskPriority.High }; Assert.Equal("子任务标题", item.Title); Assert.Equal(TaskPriority.High, item.Priority); } [Fact] public void SubTaskCreateItem_DefaultPriorityIsMedium() { var item = new SubTaskCreateItem(); Assert.Equal(TaskPriority.Medium, item.Priority); } [Fact] public void ConfirmBreakdownRequest_CanBeCreated() { var taskId = Guid.NewGuid(); var request = new ConfirmBreakdownRequest { TaskId = taskId, SubTasks = new List { new() { Title = "任务1" }, new() { Title = "任务2", Priority = TaskPriority.High } } }; Assert.Equal(taskId, request.TaskId); Assert.Equal(2, request.SubTasks.Count); Assert.Equal("任务1", request.SubTasks[0].Title); Assert.Equal(TaskPriority.High, request.SubTasks[1].Priority); } [Fact] public void ConfirmBreakdownRequest_DefaultSubTasksIsEmpty() { var request = new ConfirmBreakdownRequest(); Assert.NotNull(request.SubTasks); Assert.Empty(request.SubTasks); } [Fact] public void BatchCreateResult_CanBeCreated() { var result = new BatchCreateResult { CreatedCount = 3, SubTasks = new List { new() { Id = Guid.NewGuid(), Title = "任务1" }, new() { Id = Guid.NewGuid(), Title = "任务2" }, new() { Id = Guid.NewGuid(), Title = "任务3" } } }; Assert.Equal(3, result.CreatedCount); Assert.Equal(3, result.SubTasks.Count); Assert.Equal("任务1", result.SubTasks[0].Title); } [Fact] public void BatchCreateResult_DefaultSubTasksIsEmpty() { var result = new BatchCreateResult(); Assert.Equal(0, result.CreatedCount); Assert.NotNull(result.SubTasks); Assert.Empty(result.SubTasks); } /// /// 测试 ParseBreakdownJson 解析有效的 LLM JSON 响应。 /// [Fact] public void ParseBreakdownJson_ValidJson_ReturnsSuggestions() { string json = """ { "suggestions": [ { "title": "整理需求文档", "priority": 1, "reason": "会议中决定需要整理" }, { "title": "安排评审会议", "priority": 2, "reason": "下周前需要完成评审" } ] } """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Equal(2, result.Count); Assert.Equal("整理需求文档", result[0].Title); Assert.Equal(TaskPriority.Medium, result[0].Priority); Assert.Equal("会议中决定需要整理", result[0].Reason); Assert.Equal("安排评审会议", result[1].Title); Assert.Equal(TaskPriority.High, result[1].Priority); Assert.Equal("下周前需要完成评审", result[1].Reason); } /// /// 测试 ParseBreakdownJson 解析被 markdown 代码块包裹的 LLM 响应。 /// [Fact] public void ParseBreakdownJson_MarkdownWrapped_StripsMarkdown() { string json = """ ```json { "suggestions": [ { "title": "更新项目计划", "priority": 1, "reason": "里程碑已调整" } ] } ``` """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Single(result); Assert.Equal("更新项目计划", result[0].Title); } /// /// 测试 ParseBreakdownJson 处理空建议列表。 /// [Fact] public void ParseBreakdownJson_EmptySuggestions_ReturnsEmptyList() { string json = """{"suggestions":[]}"""; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Empty(result); } /// /// 测试 ParseBreakdownJson 过滤标题为空的条目。 /// [Fact] public void ParseBreakdownJson_FiltersEmptyTitle() { string json = """ { "suggestions": [ { "title": "", "priority": 1, "reason": "空标题" }, { "title": "有效任务", "priority": 0, "reason": "有标题" } ] } """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Single(result); Assert.Equal("有效任务", result[0].Title); } /// /// 测试 ParseBreakdownJson 过滤空白标题的条目。 /// [Fact] public void ParseBreakdownJson_FiltersWhitespaceTitle() { string json = """ { "suggestions": [ { "title": " ", "priority": 1, "reason": "空白标题" }, { "title": "有效任务", "priority": 0, "reason": "有标题" } ] } """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Single(result); Assert.Equal("有效任务", result[0].Title); } /// /// 测试 ParseBreakdownJson 处理无效 JSON。 /// [Fact] public void ParseBreakdownJson_InvalidJson_ReturnsEmptyList() { string json = "这不是有效的 JSON"; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Empty(result); } /// /// 测试 ParseBreakdownJson 处理空字符串。 /// [Fact] public void ParseBreakdownJson_EmptyString_ReturnsEmptyList() { var result = MeetingAiBreakdownService.ParseBreakdownJson(""); Assert.Empty(result); } /// /// 测试 ParseBreakdownJson 返回的条目标题已 Trim。 /// [Fact] public void ParseBreakdownJson_TrimsTitleWhitespace() { string json = """ { "suggestions": [ { "title": " 前后空格 ", "priority": 1, "reason": "测试" } ] } """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Single(result); Assert.Equal("前后空格", result[0].Title); } /// /// 测试 ParseBreakdownJson 处理 Reason 为 null 的条目。 /// [Fact] public void ParseBreakdownJson_NullReason_ReturnsEmptyReason() { string json = """ { "suggestions": [ { "title": "测试任务", "priority": 1 } ] } """; var result = MeetingAiBreakdownService.ParseBreakdownJson(json); Assert.Single(result); Assert.Equal("测试任务", result[0].Title); Assert.Equal(string.Empty, result[0].Reason); } /// /// 测试 IMeetingService 接口包含 RequestBreakdownAsync 方法。 /// [Fact] public void IMeetingService_HasRequestBreakdownAsyncMethod() { var method = typeof(IMeetingService).GetMethod("RequestBreakdownAsync"); Assert.NotNull(method); Assert.Equal(typeof(Task), method!.ReturnType); } /// /// 测试 IMeetingService 接口包含 ConfirmBreakdownAsync 方法。 /// [Fact] public void IMeetingService_HasConfirmBreakdownAsyncMethod() { var method = typeof(IMeetingService).GetMethod("ConfirmBreakdownAsync"); Assert.NotNull(method); Assert.Equal(typeof(Task), method!.ReturnType); } /// /// 测试 MeetingAiBreakdownService 类型存在并具有所需方法。 /// [Fact] public void MeetingAiBreakdownService_HasAnalyzeAsyncMethod() { var method = typeof(MeetingAiBreakdownService).GetMethod("AnalyzeAsync"); Assert.NotNull(method); } /// /// 测试 MeetingAiBreakdownService 类型存在并具有 ConfirmAndCreateAsync 方法。 /// [Fact] public void MeetingAiBreakdownService_HasConfirmAndCreateAsyncMethod() { var method = typeof(MeetingAiBreakdownService).GetMethod("ConfirmAndCreateAsync"); Assert.NotNull(method); } /// /// 测试 MeetingAiBreakdownService 类型存在并具有 ParseBreakdownJson 公开静态方法。 /// [Fact] public void MeetingAiBreakdownService_HasParseBreakdownJsonMethod() { var method = typeof(MeetingAiBreakdownService).GetMethod("ParseBreakdownJson", BindingFlags.Public | BindingFlags.Static); Assert.NotNull(method); Assert.True(method!.IsStatic); } }