< Summary

Information
Class: Orchestrator.Commands.Observability.PrepareRepeatedMatch.PrepareRepeatedMatchCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/PrepareRepeatedMatch/PrepareRepeatedMatchCommand.cs
Line coverage
88%
Covered lines: 124
Uncovered lines: 16
Coverable lines: 140
Total lines: 220
Line coverage: 88.5%
Branch coverage
56%
Covered branches: 17
Total branches: 30
Branch coverage: 56.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()62.5%8894.03%
BuildRepeatedMatchDatasetMetadata(...)100%22100%
GetStartsAt(...)50%8883.33%
LoadCompletedOutcomeAsync()50%10869.23%
ResolveOutputDirectory(...)50%4222.22%
WriteJsonFileAsync<T>(...)100%11100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/PrepareRepeatedMatch/PrepareRepeatedMatchCommand.cs

#LineLine coverage
 1using System.Text.Json;
 2using EHonda.KicktippAi.Core;
 3using Microsoft.Extensions.Logging;
 4using Orchestrator.Commands.Observability.Experiments;
 5using Orchestrator.Commands.Observability.ExportExperimentDataset;
 6using Orchestrator.Infrastructure.Factories;
 7using Spectre.Console;
 8using Spectre.Console.Cli;
 9
 10namespace Orchestrator.Commands.Observability.PrepareRepeatedMatch;
 11
 12public sealed class PrepareRepeatedMatchCommand : AsyncCommand<PrepareRepeatedMatchSettings>
 13{
 14    private readonly IAnsiConsole _console;
 15    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 16    private readonly ILogger<PrepareRepeatedMatchCommand> _logger;
 17
 118    public PrepareRepeatedMatchCommand(
 119        IAnsiConsole console,
 120        IFirebaseServiceFactory firebaseServiceFactory,
 121        ILogger<PrepareRepeatedMatchCommand> logger)
 22    {
 123        _console = console;
 124        _firebaseServiceFactory = firebaseServiceFactory;
 125        _logger = logger;
 126    }
 27
 28    protected override async Task<int> ExecuteAsync(CommandContext context, PrepareRepeatedMatchSettings settings, Cance
 29    {
 30        try
 31        {
 132            var matchOutcomeRepository = _firebaseServiceFactory.CreateMatchOutcomeRepository();
 133            var outcome = await LoadCompletedOutcomeAsync(matchOutcomeRepository, settings, cancellationToken);
 134            var sourceDatasetName = ExperimentArtifactSupport.BuildSourceDatasetName(settings.CommunityContext);
 135            var sourceItem = ExperimentArtifactSupport.BuildHostedDatasetItem(outcome);
 136            var sliceKey = string.IsNullOrWhiteSpace(settings.SliceKey)
 137                ? $"repeat-{settings.SampleSize}"
 138                : settings.SliceKey.Trim();
 139            var sourcePoolKey = string.IsNullOrWhiteSpace(settings.SourcePoolKey)
 140                ? ExperimentArtifactSupport.BuildRepeatedMatchSourcePoolKey(settings.Matchday!.Value, settings.HomeTeam,
 141                : settings.SourcePoolKey.Trim();
 142            var datasetName = settings.DatasetName
 143                ?? $"{sourceDatasetName}/repeated-match/{sourcePoolKey}/{sliceKey}";
 144            var datasetDescription = string.IsNullOrWhiteSpace(settings.DatasetDescription)
 145                ? null
 146                : settings.DatasetDescription.Trim();
 147            var outputDirectory = ResolveOutputDirectory(settings.OutputDirectory, settings.CommunityContext, sourcePool
 148            var sliceArtifactPath = Path.Combine(outputDirectory, "slice-dataset.json");
 149            var sliceManifestPath = Path.Combine(outputDirectory, "slice-manifest.json");
 50
 151            Directory.CreateDirectory(outputDirectory);
 52
 153            var startsAt = GetStartsAt(sourceItem);
 154            var sourceItems = Enumerable.Range(1, settings.SampleSize)
 155                .Select(index =>
 156                {
 157                    var sliceDatasetItemId = ExperimentArtifactSupport.BuildRepeatedSliceDatasetItemId(
 158                        sourceItem.Id,
 159                        sliceKey,
 160                        index,
 161                        settings.SampleSize);
 162
 163                    return new PreparedExperimentSourceItem(
 164                        sourceItem.Id,
 165                        sliceDatasetItemId,
 166                        sourceItem.Id,
 167                        sourceItem.Metadata.Competition,
 168                        sourceItem.Metadata.Season,
 169                        sourceItem.Metadata.CommunityContext,
 170                        sourceItem.Metadata.Matchday,
 171                        sourceItem.Metadata.MatchdayLabel,
 172                        sourceItem.Metadata.HomeTeam,
 173                        sourceItem.Metadata.AwayTeam,
 174                        startsAt,
 175                        sourceItem.Metadata.TippSpielId,
 176                        sourceItem.ExpectedOutput.HomeGoals,
 177                        sourceItem.ExpectedOutput.AwayGoals);
 178                })
 179                .ToList();
 80
 181            var bundle = PreparedExperimentBundleBuilder.Build(
 182                sourceItems,
 183                settings.CommunityContext,
 184                sourceDatasetName,
 185                datasetName,
 186                sliceKey,
 187                "repeated-match",
 188                "repeated-match",
 189                sourcePoolKey,
 190                null,
 191                datasetDescription,
 192                BuildRepeatedMatchDatasetMetadata(sourceItem, settings.SampleSize, datasetDescription));
 93
 194            await WriteJsonFileAsync(sliceArtifactPath, bundle.Artifact, cancellationToken);
 195            await WriteJsonFileAsync(sliceManifestPath, bundle.Manifest, cancellationToken);
 96
 197            var summary = new
 198            {
 199                mode = "repeated-match",
 1100                settings.CommunityContext,
 1101                datasetName = bundle.Manifest.SliceDatasetName,
 1102                bundle.Manifest.SourceDatasetName,
 1103                bundle.Manifest.SourcePoolKey,
 1104                bundle.Manifest.SliceKey,
 1105                bundle.Manifest.SampleSize,
 1106                settings.HomeTeam,
 1107                settings.AwayTeam,
 1108                Matchday = settings.Matchday,
 1109                datasetDescription = bundle.Artifact.DatasetDescription,
 1110                datasetMetadata = bundle.Artifact.DatasetMetadata,
 1111                bundle.Manifest.SelectedItemIds,
 1112                bundle.Manifest.SelectedItemIdsHash,
 1113                outputDirectory,
 1114                sliceArtifactPath,
 1115                sliceManifestPath
 1116            };
 117
 1118            _console.WriteLine(JsonSerializer.Serialize(summary, PreparedExperimentCommandSupport.JsonOptions));
 1119            return 0;
 120        }
 0121        catch (Exception ex)
 122        {
 0123            _logger.LogError(ex, "Error preparing repeated-match experiment artifact");
 0124            _console.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
 0125            return 1;
 126        }
 1127    }
 128
 129    private static IReadOnlyDictionary<string, object?> BuildRepeatedMatchDatasetMetadata(
 130        HostedMatchExperimentDatasetItem sourceItem,
 131        int repetitionCount,
 132        string? datasetDescription)
 133    {
 1134        var actualResult = $"{sourceItem.ExpectedOutput.HomeGoals}:{sourceItem.ExpectedOutput.AwayGoals}";
 1135        var actualResultDisplay = $"{sourceItem.Metadata.HomeTeam} {sourceItem.ExpectedOutput.HomeGoals} - {sourceItem.E
 1136        var metadata = new Dictionary<string, object?>
 1137        {
 1138            ["fixture"] = $"{sourceItem.Metadata.HomeTeam} vs {sourceItem.Metadata.AwayTeam}",
 1139            ["actualResult"] = actualResult,
 1140            ["actualResultDisplay"] = actualResultDisplay,
 1141            ["matchday"] = sourceItem.Metadata.Matchday,
 1142            ["repetitionCount"] = repetitionCount
 1143        };
 144
 1145        if (!string.IsNullOrWhiteSpace(datasetDescription))
 146        {
 1147            metadata["datasetDescription"] = datasetDescription;
 1148            metadata["interestingBecause"] = datasetDescription;
 149        }
 150
 1151        return metadata;
 152    }
 153
 154    private static string GetStartsAt(HostedMatchExperimentDatasetItem item)
 155    {
 1156        if (item.Input.ValueKind != JsonValueKind.Object
 1157            || !item.Input.TryGetProperty("startsAt", out var startsAt)
 1158            || startsAt.ValueKind != JsonValueKind.String
 1159            || string.IsNullOrWhiteSpace(startsAt.GetString()))
 160        {
 0161            throw new InvalidOperationException($"Dataset item '{item.Id}' is missing input.startsAt.");
 162        }
 163
 1164        return startsAt.GetString()!;
 165    }
 166
 167    private static async Task<PersistedMatchOutcome> LoadCompletedOutcomeAsync(
 168        IMatchOutcomeRepository matchOutcomeRepository,
 169        PrepareRepeatedMatchSettings settings,
 170        CancellationToken cancellationToken)
 171    {
 1172        var outcomes = await matchOutcomeRepository.GetMatchdayOutcomesAsync(
 1173            settings.Matchday!.Value,
 1174            settings.CommunityContext,
 1175            cancellationToken);
 176
 1177        var outcome = outcomes.FirstOrDefault(candidate =>
 1178            string.Equals(candidate.HomeTeam, settings.HomeTeam, StringComparison.OrdinalIgnoreCase)
 1179            && string.Equals(candidate.AwayTeam, settings.AwayTeam, StringComparison.OrdinalIgnoreCase));
 180
 1181        if (outcome is null)
 182        {
 0183            throw new InvalidOperationException(
 0184                $"No persisted match outcome was found for {settings.HomeTeam} vs {settings.AwayTeam} on matchday {setti
 185        }
 186
 1187        if (!outcome.HasOutcome || outcome.HomeGoals is null || outcome.AwayGoals is null)
 188        {
 0189            throw new InvalidOperationException(
 0190                $"The selected match does not have a completed persisted outcome yet: {settings.HomeTeam} vs {settings.A
 191        }
 192
 1193        return outcome;
 1194    }
 195
 196    private static string ResolveOutputDirectory(
 197        string? outputDirectoryOverride,
 198        string communityContext,
 199        string sourcePoolKey,
 200        string sliceKey)
 201    {
 1202        if (!string.IsNullOrWhiteSpace(outputDirectoryOverride))
 203        {
 1204            return Path.GetFullPath(outputDirectoryOverride);
 205        }
 206
 0207        return Path.GetFullPath(Path.Combine(
 0208            "artifacts",
 0209            "langfuse-experiments",
 0210            "repeated-match",
 0211            ExperimentArtifactSupport.Slugify(communityContext),
 0212            sourcePoolKey,
 0213            sliceKey));
 214    }
 215
 216    private static Task WriteJsonFileAsync<T>(string path, T value, CancellationToken cancellationToken)
 217    {
 1218        return File.WriteAllTextAsync(path, JsonSerializer.Serialize(value, PreparedExperimentCommandSupport.JsonOptions
 219    }
 220}