< Summary

Information
Class: Orchestrator.Commands.Utility.CopyFirestoreContext.CopyFirestoreContextCommand.KpiLoadResult
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Utility/CopyFirestoreContext/CopyFirestoreContextCommand.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 229
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Utility/CopyFirestoreContext/CopyFirestoreContextCommand.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using Microsoft.Extensions.Logging;
 3using Orchestrator.Infrastructure;
 4using Orchestrator.Infrastructure.Factories;
 5using Spectre.Console;
 6using Spectre.Console.Cli;
 7
 8namespace Orchestrator.Commands.Utility.CopyFirestoreContext;
 9
 10public sealed class CopyFirestoreContextCommand : AsyncCommand<CopyFirestoreContextSettings>
 11{
 12    private readonly IAnsiConsole _console;
 13    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 14    private readonly ILogger<CopyFirestoreContextCommand> _logger;
 15
 16    public CopyFirestoreContextCommand(
 17        IAnsiConsole console,
 18        IFirebaseServiceFactory firebaseServiceFactory,
 19        ILogger<CopyFirestoreContextCommand> logger)
 20    {
 21        _console = console;
 22        _firebaseServiceFactory = firebaseServiceFactory;
 23        _logger = logger;
 24    }
 25
 26    protected override async Task<int> ExecuteAsync(
 27        CommandContext context,
 28        CopyFirestoreContextSettings settings,
 29        CancellationToken cancellationToken)
 30    {
 31        try
 32        {
 33            var prefixes = SplitCsvOption(settings.ContextPrefix);
 34            var kpiDocumentNames = SplitCsvOption(settings.KpiDocument);
 35            var competition = CompetitionResolver.ResolveCompetition(
 36                settings.Competition,
 37                communityContext: settings.TargetCommunityContext);
 38            var repositoryCompetition = CompetitionResolver.ToRepositoryCompetitionArgument(competition);
 39
 40            _console.MarkupLine($"[green]Copy Firestore context command initialized[/]");
 41            _console.MarkupLine($"[blue]Source community context:[/] [yellow]{settings.SourceCommunityContext}[/]");
 42            _console.MarkupLine($"[blue]Target community context:[/] [yellow]{settings.TargetCommunityContext}[/]");
 43            _console.MarkupLine($"[blue]Using competition:[/] [yellow]{competition}[/]");
 44            if (settings.DryRun)
 45            {
 46                _console.MarkupLine("[magenta]Dry run mode enabled - no Firestore documents will be written[/]");
 47            }
 48
 49            var contextRepository = _firebaseServiceFactory.CreateContextRepository(repositoryCompetition);
 50            var kpiRepository = _firebaseServiceFactory.CreateKpiRepository(repositoryCompetition);
 51
 52            var sourceContextDocuments = await LoadSourceContextDocumentsAsync(
 53                contextRepository,
 54                settings.SourceCommunityContext,
 55                prefixes,
 56                cancellationToken);
 57            var sourceKpiDocuments = await LoadSourceKpiDocumentsAsync(
 58                kpiRepository,
 59                settings.SourceCommunityContext,
 60                kpiDocumentNames,
 61                cancellationToken);
 62
 63            if (sourceContextDocuments.MissingMessages.Count > 0 || sourceKpiDocuments.MissingMessages.Count > 0)
 64            {
 65                foreach (var message in sourceContextDocuments.MissingMessages.Concat(sourceKpiDocuments.MissingMessages
 66                {
 67                    _console.MarkupLine($"[red]{message}[/]");
 68                }
 69
 70                return 1;
 71            }
 72
 73            if (settings.Verbose)
 74            {
 75                foreach (var document in sourceContextDocuments.Documents)
 76                {
 77                    _console.MarkupLine($"[dim]  Context: {document.DocumentName} (version {document.Version})[/]");
 78                }
 79
 80                foreach (var document in sourceKpiDocuments.Documents)
 81                {
 82                    _console.MarkupLine($"[dim]  KPI: {document.DocumentName} (version {document.Version})[/]");
 83                }
 84            }
 85
 86            if (settings.DryRun)
 87            {
 88                _console.MarkupLine($"[magenta]Would copy {sourceContextDocuments.Documents.Count} context document(s) a
 89                return 0;
 90            }
 91
 92            var savedContextCount = 0;
 93            var unchangedContextCount = 0;
 94            foreach (var document in sourceContextDocuments.Documents)
 95            {
 96                var savedVersion = await contextRepository.SaveContextDocumentAsync(
 97                    document.DocumentName,
 98                    document.Content,
 99                    settings.TargetCommunityContext,
 100                    cancellationToken);
 101
 102                if (savedVersion.HasValue)
 103                {
 104                    savedContextCount++;
 105                }
 106                else
 107                {
 108                    unchangedContextCount++;
 109                }
 110            }
 111
 112            var savedKpiCount = 0;
 113            foreach (var document in sourceKpiDocuments.Documents)
 114            {
 115                await kpiRepository.SaveKpiDocumentAsync(
 116                    document.DocumentName,
 117                    document.Content,
 118                    document.Description,
 119                    settings.TargetCommunityContext,
 120                    cancellationToken);
 121                savedKpiCount++;
 122            }
 123
 124            _console.MarkupLine($"[green]Copied {savedContextCount} context document(s) and {savedKpiCount} KPI document
 125            if (unchangedContextCount > 0)
 126            {
 127                _console.MarkupLine($"[dim]Unchanged context document(s): {unchangedContextCount}[/]");
 128            }
 129
 130            return 0;
 131        }
 132        catch (Exception ex)
 133        {
 134            _logger.LogError(ex, "Error in copy-firestore-context command");
 135            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 136            return 1;
 137        }
 138    }
 139
 140    private async Task<ContextLoadResult> LoadSourceContextDocumentsAsync(
 141        IContextRepository contextRepository,
 142        string sourceCommunityContext,
 143        IReadOnlyList<string> prefixes,
 144        CancellationToken cancellationToken)
 145    {
 146        if (prefixes.Count == 0)
 147        {
 148            return new ContextLoadResult([], []);
 149        }
 150
 151        var documentNames = await contextRepository.GetContextDocumentNamesAsync(sourceCommunityContext, cancellationTok
 152        var selectedDocumentNames = documentNames
 153            .Where(name => prefixes.Any(prefix => name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
 154            .Distinct(StringComparer.OrdinalIgnoreCase)
 155            .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
 156            .ToList();
 157
 158        var missingMessages = new List<string>();
 159        if (selectedDocumentNames.Count == 0)
 160        {
 161            missingMessages.Add($"No source context documents found for prefix(es): {string.Join(", ", prefixes)}");
 162            return new ContextLoadResult([], missingMessages);
 163        }
 164
 165        var documents = new List<ContextDocument>();
 166        foreach (var documentName in selectedDocumentNames)
 167        {
 168            var document = await contextRepository.GetLatestContextDocumentAsync(
 169                documentName,
 170                sourceCommunityContext,
 171                cancellationToken);
 172
 173            if (document is null)
 174            {
 175                missingMessages.Add($"Missing source context document: {documentName}");
 176            }
 177            else
 178            {
 179                documents.Add(document);
 180            }
 181        }
 182
 183        return new ContextLoadResult(documents, missingMessages);
 184    }
 185
 186    private async Task<KpiLoadResult> LoadSourceKpiDocumentsAsync(
 187        IKpiRepository kpiRepository,
 188        string sourceCommunityContext,
 189        IReadOnlyList<string> kpiDocumentNames,
 190        CancellationToken cancellationToken)
 191    {
 192        var documents = new List<KpiDocument>();
 193        var missingMessages = new List<string>();
 194
 195        foreach (var documentName in kpiDocumentNames)
 196        {
 197            var document = await kpiRepository.GetKpiDocumentAsync(
 198                documentName,
 199                sourceCommunityContext,
 200                cancellationToken);
 201
 202            if (document is null)
 203            {
 204                missingMessages.Add($"Missing source KPI document: {documentName}");
 205            }
 206            else
 207            {
 208                documents.Add(document);
 209            }
 210        }
 211
 212        return new KpiLoadResult(documents, missingMessages);
 213    }
 214
 215    private static IReadOnlyList<string> SplitCsvOption(string? value)
 216    {
 217        return string.IsNullOrWhiteSpace(value)
 218            ? []
 219            : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
 220    }
 221
 222    private sealed record ContextLoadResult(
 223        IReadOnlyList<ContextDocument> Documents,
 224        IReadOnlyList<string> MissingMessages);
 225
 1226    private sealed record KpiLoadResult(
 1227        IReadOnlyList<KpiDocument> Documents,
 1228        IReadOnlyList<string> MissingMessages);
 229}