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