- Create higher-level AI services: - StoryGenerationService (uses MistralService) - WritingFeedbackService (uses MistralService) - SpeechExerciseService (uses VoskService) - AudioGenerationService (uses TtsService) - AiFallbackService (fallback mechanisms for service failures) - Register AiFallbackService in Program.cs DI container - Add comprehensive unit tests for all Phase 5 services: - AiFallbackServiceTests (14 tests) - AudioGenerationServiceTests (16 tests) - MistralServiceTests (16 tests) - SpeechExerciseServiceTests (12 tests) - StoryGenerationServiceTests (13 tests) - WritingFeedbackServiceTests (11 tests) - VoskServiceTests (15 tests) - TtsServiceTests (21 tests) - Update feature document (ai-services.md) to mark Phase 5 as complete Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
264 lines
8.4 KiB
C#
264 lines
8.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GermanApp.Application.Services;
|
|
using GermanApp.Domain.Interfaces;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
using Moq;
|
|
|
|
namespace GermanApp.Tests.Unit.Application.Services;
|
|
|
|
[TestClass]
|
|
public class WritingFeedbackServiceTests
|
|
{
|
|
private Mock<IMistralService> _mockMistralService;
|
|
private Mock<ILogger<WritingFeedbackService>> _mockLogger;
|
|
private WritingFeedbackService _service;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_mockMistralService = new Mock<IMistralService>();
|
|
_mockLogger = new Mock<ILogger<WritingFeedbackService>>();
|
|
_service = new WritingFeedbackService(
|
|
_mockMistralService.Object,
|
|
_mockLogger.Object);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideFeedbackAsync_WithValidText_ReturnsFeedback()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna und wohne in Berlin.";
|
|
var level = "A1";
|
|
var expectedFeedback = "Good job! Your sentence structure is correct.";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync(expectedFeedback);
|
|
|
|
// Act
|
|
var result = await _service.ProvideFeedbackAsync(
|
|
userText, level, null, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(expectedFeedback, result);
|
|
_mockMistralService.Verify(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken), Times.Once);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideFeedbackAsync_WithCustomPrompt_IncludesPrompt()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var customPrompt = "Focus on grammar and vocabulary.";
|
|
var expectedFeedback = "Feedback with custom prompt...";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, customPrompt, cancellationToken))
|
|
.ReturnsAsync(expectedFeedback);
|
|
|
|
// Act
|
|
var result = await _service.ProvideFeedbackAsync(
|
|
userText, level, customPrompt, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.AreEqual(expectedFeedback, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideFeedbackAsync_WithEmptyFeedback_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync(string.Empty);
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.ProvideFeedbackAsync(userText, level, null, cancellationToken);
|
|
Assert.Fail("Expected InvalidOperationException was not thrown");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideFeedbackAsync_WithNullFeedback_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync((string?)null);
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.ProvideFeedbackAsync(userText, level, null, cancellationToken);
|
|
Assert.Fail("Expected InvalidOperationException was not thrown");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideFeedbackAsync_WhenMistralThrows_ThrowsAiServiceException()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ThrowsAsync(new Exception("API Error"));
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
await _service.ProvideFeedbackAsync(userText, level, null, cancellationToken);
|
|
Assert.Fail("Expected AiServiceException was not thrown");
|
|
}
|
|
catch (AiServiceException)
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ProvideStructuredFeedbackAsync_WithValidText_ReturnsStructuredFeedback()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var feedbackText = "Good job!\nGrammar: Correct\nSuggestion: Use more vocabulary";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync(feedbackText);
|
|
|
|
// Act
|
|
var result = await _service.ProvideStructuredFeedbackAsync(
|
|
userText, level, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(userText, result.OriginalText);
|
|
Assert.AreEqual(feedbackText, result.FeedbackText);
|
|
Assert.IsNotNull(result.GrammarCorrections);
|
|
Assert.IsNotNull(result.ImprovementSuggestions);
|
|
Assert.IsNotNull(result.Encouragement);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task CheckGrammarAsync_WithValidText_ReturnsCorrections()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var feedbackText = "Grammar: Incorrect -> Correct: Ich heiße Anna";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync(feedbackText);
|
|
|
|
// Act
|
|
var result = await _service.CheckGrammarAsync(
|
|
userText, level, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(1, result.Count);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task SuggestImprovementsAsync_WithValidText_ReturnsSuggestions()
|
|
{
|
|
// Arrange
|
|
var userText = "Ich heisse Anna.";
|
|
var level = "A1";
|
|
var feedbackText = "Suggestion: Add more details about yourself";
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
userText, level, null, cancellationToken))
|
|
.ReturnsAsync(feedbackText);
|
|
|
|
// Act
|
|
var result = await _service.SuggestImprovementsAsync(
|
|
userText, level, cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.IsTrue(result.Count > 0);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithWorkingService_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
"Ich heisse Anna.", "A1", null, cancellationToken))
|
|
.ReturnsAsync("Test feedback");
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsTrue(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WhenServiceFails_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
"Ich heisse Anna.", "A1", null, cancellationToken))
|
|
.ThrowsAsync(new Exception("Error"));
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task TestServiceAsync_WithEmptyResult_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var cancellationToken = CancellationToken.None;
|
|
_mockMistralService.Setup(s => s.GenerateWritingFeedbackAsync(
|
|
"Ich heisse Anna.", "A1", null, cancellationToken))
|
|
.ReturnsAsync(string.Empty);
|
|
|
|
// Act
|
|
var result = await _service.TestServiceAsync(cancellationToken);
|
|
|
|
// Assert
|
|
Assert.IsFalse(result);
|
|
}
|
|
}
|