diff --git a/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs b/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs index c98383bc..6206b8e3 100644 --- a/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs +++ b/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs @@ -11,7 +11,9 @@ public sealed class LearningDatasetService public async Task ExportJsonAsync(string outputPath, int limit = 1000) { - var rows = await _reader.ReadTrainingExamplesAsync(limit); + var normalizedOutputPath = RequireValue(outputPath, nameof(outputPath)); + var normalizedLimit = Math.Clamp(limit, 1, 10000); + var rows = await _reader.ReadTrainingExamplesAsync(normalizedLimit); var payload = new { formula_id = "ENGINE_HISTORY_TRAINING_DATASET_V1", @@ -21,9 +23,19 @@ public sealed class LearningDatasetService source = "engine_history.training_example_v1", rows }; - var path = Path.GetFullPath(outputPath); + var path = Path.GetFullPath(normalizedOutputPath); Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); return path; } + + private static string RequireValue(string value, string parameterName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Value is required.", parameterName); + } + + return value.Trim(); + } } diff --git a/src/dotnet/QuantEngine.Core.Tests/LearningDatasetServiceTests.cs b/src/dotnet/QuantEngine.Core.Tests/LearningDatasetServiceTests.cs new file mode 100644 index 00000000..bf94c439 --- /dev/null +++ b/src/dotnet/QuantEngine.Core.Tests/LearningDatasetServiceTests.cs @@ -0,0 +1,54 @@ +using Moq; +using QuantEngine.Application.Services; +using QuantEngine.Core.Interfaces; + +namespace QuantEngine.Core.Tests; + +public class LearningDatasetServiceTests +{ + [Fact] + public async Task ExportJsonAsync_TrimsPathAndClampsLimit() + { + var reader = new Mock(MockBehavior.Strict); + reader.Setup(r => r.ReadTrainingExamplesAsync(10000)).ReturnsAsync([]); + + var service = new LearningDatasetService(reader.Object); + var root = FindRepoRoot(); + var outPath = Path.Combine(root, "Temp", "learning_dataset_test.json"); + + if (File.Exists(outPath)) + { + File.Delete(outPath); + } + + var result = await service.ExportJsonAsync($" {outPath} ", 50000); + + Assert.Equal(Path.GetFullPath(outPath), result); + Assert.True(File.Exists(result)); + var text = await File.ReadAllTextAsync(result); + Assert.Contains("\"gate\": \"DATA_MISSING\"", text); + reader.VerifyAll(); + } + + [Fact] + public async Task ExportJsonAsync_RejectsBlankPath() + { + var service = new LearningDatasetService(new Mock().Object); + await Assert.ThrowsAsync(() => service.ExportJsonAsync(" ", 10)); + } + + private static string FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + current = current.Parent; + } + + throw new InvalidOperationException("Repository root not found."); + } +}