using Hua.Todo.Application.Models; using Hua.Todo.Application.Services.Interfaces; using Hua.Todo.Application.Services.Voice; using Hua.Todo.Application.Services.Voice.Models; using Hua.Todo.Core.Entities; using Hua.Todo.Core.Services; using Hua.Todo.Core.Voice; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Hua.Todo.Host.Tests; /// /// 语音意图解析与命令执行器单元测试。 /// 覆盖 RuleIntentParser(离线规则解析)、VoiceCommandExecutor(命令执行)、模型序列化。 /// public class VoiceIntentParserTests { #region RuleIntentParser 测试 [Fact] public void Parse_Create_ReturnsCreateIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("创建任务 开会").Result; Assert.Equal(VoiceIntent.CREATE, result.Intent); Assert.Equal("开会", result.Params["title"]); Assert.True(result.Confidence >= 0.8); } [Fact] public void Parse_Create_WithShortVerb_ReturnsCreateIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("加 测试").Result; Assert.Equal(VoiceIntent.CREATE, result.Intent); Assert.Equal("测试", result.Params["title"]); } [Fact] public void Parse_Delete_ReturnsDeleteIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("删除任务 开会").Result; Assert.Equal(VoiceIntent.DELETE, result.Intent); Assert.Equal("开会", result.Params["targetTitle"]); } [Fact] public void Parse_Complete_ReturnsCompleteIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("完成任务 开会").Result; Assert.Equal(VoiceIntent.COMPLETE, result.Intent); Assert.Equal("开会", result.Params["targetTitle"]); } [Fact] public void Parse_Uncomplete_ReturnsUncompleteIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("取消完成 开会").Result; Assert.Equal(VoiceIntent.UNCOMPLETE, result.Intent); Assert.Equal("开会", result.Params["targetTitle"]); } [Fact] public void Parse_Update_ReturnsUpdateIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("修改任务 开会 改为 团队会议").Result; Assert.Equal(VoiceIntent.UPDATE, result.Intent); Assert.Equal("开会", result.Params["targetTitle"]); Assert.Equal("团队会议", result.Params["newTitle"]); } [Fact] public void Parse_Query_ReturnsQueryIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("查看未完成任务").Result; Assert.Equal(VoiceIntent.QUERY, result.Intent); Assert.Equal("未完成任务", result.Params["filter"]); } [Fact] public void Parse_Query_HighPriority_ReturnsQueryIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("列出高优先级任务").Result; Assert.Equal(VoiceIntent.QUERY, result.Intent); Assert.Equal("高优先级任务", result.Params["filter"]); } [Fact] public void Parse_AddSubtask_ReturnsAddSubtaskIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("给任务开会添加子任务 准备PPT").Result; Assert.Equal(VoiceIntent.ADD_SUBTASK, result.Intent); Assert.Equal("任务开会", result.Params["parentTitle"]); Assert.Equal("准备PPT", result.Params["subTitle"]); } [Fact] public void Parse_Unknown_ReturnsUnknownIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("今天天气真好").Result; Assert.Equal(VoiceIntent.UNKNOWN, result.Intent); Assert.Equal(0.0, result.Confidence); } [Fact] public void Parse_AiBreakdown_ReturnsAiBreakdownIntent() { var parser = new RuleIntentParser(); var result = parser.ParseAsync("帮我拆分 开会").Result; Assert.Equal(VoiceIntent.AI_BREAKDOWN, result.Intent); Assert.Equal("开会", result.Params["targetTitle"]); } [Fact] public void Parse_EmptyText_ReturnsUnknown() { var parser = new RuleIntentParser(); var result = parser.ParseAsync(" ").Result; Assert.Equal(VoiceIntent.UNKNOWN, result.Intent); } #endregion #region VoiceCommandExecutor 测试(Mock ITaskService) [Fact] public async Task Execute_Create_Success() { var mockService = new MockTaskServiceForVoice(); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.CREATE, Params = new Dictionary { ["title"] = "新任务" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Contains("已创建任务", response.Result.Message); Assert.Single(mockService.CreatedTasks); Assert.Equal("新任务", mockService.CreatedTasks[0].Title); } [Fact] public async Task Execute_Create_WithPriority_Success() { var mockService = new MockTaskServiceForVoice(); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.CREATE, Params = new Dictionary { ["title"] = "紧急任务", ["priority"] = "高" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Equal(TaskPriority.High, mockService.CreatedTasks[0].Priority); } [Fact] public async Task Execute_Delete_Success() { var mockService = new MockTaskServiceForVoice(); var taskId = mockService.AddExistingTask("测试任务", false); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.DELETE, Params = new Dictionary { ["targetTitle"] = "测试任务" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Contains("已删除任务", response.Result.Message); Assert.Single(mockService.DeletedIds); Assert.Equal(taskId, mockService.DeletedIds[0]); } [Fact] public async Task Execute_Complete_Success() { var mockService = new MockTaskServiceForVoice(); mockService.AddExistingTask("待完成", false); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.COMPLETE, Params = new Dictionary { ["targetTitle"] = "待完成" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Contains("已完成任务", response.Result.Message); } [Fact] public async Task Execute_Ambiguity_ReturnsCandidates() { var mockService = new MockTaskServiceForVoice(); mockService.AddExistingTask("开会"); mockService.AddExistingTask("开会讨论方案"); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.DELETE, Params = new Dictionary { ["targetTitle"] = "开会" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.False(response.Result.Success); Assert.True(response.Result.Ambiguity); Assert.Equal(2, response.Result.Candidates.Count); } [Fact] public async Task Execute_NotFound_ReturnsError() { var mockService = new MockTaskServiceForVoice(); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.DELETE, Params = new Dictionary { ["targetTitle"] = "不存在的任务" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.False(response.Result.Success); Assert.False(response.Result.Ambiguity); } [Fact] public async Task Execute_AddSubTask_Success() { var mockService = new MockTaskServiceForVoice(); var parentTaskId = mockService.AddExistingTask("父任务"); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.ADD_SUBTASK, Params = new Dictionary { ["parentTitle"] = "父任务", ["subTitle"] = "子任务" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Contains("添加子任务", response.Result.Message); Assert.Equal(parentTaskId, mockService.CreatedTasks[0].ParentTaskId); } [Fact] public async Task Execute_Uncomplete_Success() { var mockService = new MockTaskServiceForVoice(); mockService.AddExistingTask("已完成的任务", true); // IsCompleted = true var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.UNCOMPLETE, Params = new Dictionary { ["targetTitle"] = "已完成的任务" }, Confidence = 0.9 }; var response = await executor.ExecuteAsync(intentResult); Assert.True(response.Result.Success); Assert.Contains("已取消完成", response.Result.Message); } [Fact] public async Task Execute_Confirmation_Complete_Success() { var mockService = new MockTaskServiceForVoice(); var confirmTaskId = mockService.AddExistingTask("确认测试", false); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var response = await executor.ExecuteConfirmedAsync(VoiceIntent.COMPLETE, confirmTaskId); Assert.True(response.Result.Success); Assert.Contains("已完成任务", response.Result.Message); } [Fact] public async Task Execute_Confirmation_Delete_Success() { var mockService = new MockTaskServiceForVoice(); var deleteConfirmId = mockService.AddExistingTask("删除确认", false); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var response = await executor.ExecuteConfirmedAsync(VoiceIntent.DELETE, deleteConfirmId); Assert.True(response.Result.Success); Assert.Contains("已删除任务", response.Result.Message); } [Fact] public async Task Execute_UnknownIntent_ReturnsError() { var mockService = new MockTaskServiceForVoice(); var executor = new VoiceCommandExecutor(mockService, NullLogger.Instance); var intentResult = new VoiceIntentResult { Intent = VoiceIntent.UNKNOWN, Confidence = 0.0 }; var response = await executor.ExecuteAsync(intentResult); Assert.False(response.Result.Success); Assert.Equal("没听懂,请再说一次", response.Result.Message); Assert.Equal(VoiceIntent.UNKNOWN, response.Intent); } #endregion #region HybridVoiceIntentParser 离线降级测试 [Fact] public async Task HybridParser_OfflineMode_UsesRuleParser() { // 设置离线模式环境变量 Environment.SetEnvironmentVariable("VOICE_ONLINE", "false"); try { var parser = new HybridVoiceIntentParser(null!, new RuleIntentParser(), NullLogger.Instance); var result = await parser.ParseAsync("创建任务 测试"); Assert.Equal(VoiceIntent.CREATE, result.Intent); Assert.Equal("测试", result.Params["title"]); } finally { Environment.SetEnvironmentVariable("VOICE_ONLINE", null); } } #endregion #region AiBreakdownService 响应解析测试 [Fact] public async Task AiBreakdown_NoTask_ReturnsEmptyList() { var mockService = new MockTaskServiceForVoice(); var mockLlm = new MockLlmClientService(""); var breakdownService = new AiBreakdownService(mockLlm, mockService, NullLogger.Instance); var request = new AiBreakdownRequest { TaskId = Guid.NewGuid() }; var response = await breakdownService.GetBreakdownAsync(request); Assert.NotNull(response); Assert.Empty(response.Suggestions); } [Fact] public async Task AiBreakdown_Confirm_CreatesSubTasks() { var mockService = new MockTaskServiceForVoice(); var mockLlm = new MockLlmClientService(""); var breakdownService = new AiBreakdownService(mockLlm, mockService, NullLogger.Instance); var confirmParentId = Guid.NewGuid(); var confirmRequest = new AiBreakdownConfirmRequest { ParentTaskId = confirmParentId, SubTasks = new List { new() { Title = "子任务1", Priority = TaskPriority.High }, new() { Title = "子任务2", Priority = TaskPriority.Medium } } }; var created = await breakdownService.ConfirmBreakdownAsync(confirmRequest); Assert.Equal(2, created.Count); Assert.Equal("子任务1", created[0].Title); Assert.Equal(TaskPriority.High, created[0].Priority); Assert.Equal(confirmParentId, created[0].ParentTaskId); } #endregion } #region Mock 实现 /// /// Mock ITaskService 实现,用于 VoiceCommandExecutor 和 AiBreakdownService 测试。 /// 跟踪创建、删除操作并支持预设任务数据。 /// public class MockTaskServiceForVoice : ITaskService { private readonly List _tasks = new(); private Guid _nextId = Guid.NewGuid(); /// /// 跟踪已创建的任务。 /// public List CreatedTasks { get; } = new(); /// /// 跟踪已删除的 ID。 /// public List DeletedIds { get; } = new(); /// /// 预设一个已有任务,返回创建的任务 ID 供后续断言使用。 /// public Guid AddExistingTask(string title, bool isCompleted = false) { var id = Guid.NewGuid(); _tasks.Add(new TaskDto { Id = id, Title = title, Priority = TaskPriority.Medium, IsCompleted = isCompleted, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, SubTasks = new List() }); return id; } public Task> GetAllTasksAsync() => Task.FromResult(_tasks.ToList()); public Task GetTaskByIdAsync(Guid id) => Task.FromResult(_tasks.FirstOrDefault(t => t.Id == id)); public Task> GetActiveTasksAsync() => Task.FromResult(_tasks.Where(t => !t.IsCompleted).ToList()); public Task> GetCompletedTasksAsync() => Task.FromResult(_tasks.Where(t => t.IsCompleted).ToList()); public Task CreateTaskAsync(CreateTaskDto dto) { var task = new TaskDto { Id = Guid.NewGuid(), Title = dto.Title, Priority = dto.Priority, IsCompleted = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, ParentTaskId = dto.ParentTaskId, SubTasks = new List() }; _tasks.Add(task); CreatedTasks.Add(task); return Task.FromResult(task); } public Task UpdateTaskAsync(UpdateTaskDto dto) { var task = _tasks.FirstOrDefault(t => t.Id == dto.Id); if (task == null) throw new KeyNotFoundException($"Task with ID {dto.Id} not found"); if (!string.IsNullOrEmpty(dto.Title)) task.Title = dto.Title; if (dto.Priority.HasValue) task.Priority = dto.Priority.Value; task.UpdatedAt = DateTime.UtcNow; return Task.FromResult(task); } public Task ToggleCompleteAsync(Guid id) { var task = _tasks.FirstOrDefault(t => t.Id == id); if (task == null) throw new KeyNotFoundException($"Task with ID {id} not found"); task.IsCompleted = !task.IsCompleted; task.UpdatedAt = DateTime.UtcNow; return Task.FromResult(task); } public Task DeleteTaskAsync(Guid id) { DeletedIds.Add(id); _tasks.RemoveAll(t => t.Id == id); return Task.CompletedTask; } public Task> GetSubTasksAsync(Guid parentTaskId) { return Task.FromResult(_tasks.Where(t => t.ParentTaskId == parentTaskId).ToList()); } } /// /// Mock ILlmClientService 实现,返回预设文本或抛出异常。 /// public class MockLlmClientService : ILlmClientService { private readonly string _presetResponse; /// /// 创建 Mock LLM 客户端。 /// /// 预设的 LLM 响应文本。 public MockLlmClientService(string presetResponse) { _presetResponse = presetResponse; } /// /// 返回预设响应(或抛出异常以模拟 LLM 不可用)。 /// public Task SendAsync(string prompt, CancellationToken ct = default) { if (string.IsNullOrEmpty(_presetResponse)) throw new HttpRequestException("LLM unavailable"); return Task.FromResult(_presetResponse); } } #endregion