< Summary

Information
Class: Orchestrator.Commands.Utility.CopyFirestoreContext.CopyFirestoreContextCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Utility/CopyFirestoreContext/CopyFirestoreContextCommand.cs
Line coverage
89%
Covered lines: 107
Uncovered lines: 12
Coverable lines: 119
Total lines: 229
Line coverage: 89.9%
Branch coverage
75%
Covered branches: 30
Total branches: 40
Branch coverage: 75%
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()70.83%262484.62%
LoadSourceContextDocumentsAsync()80%101090.91%
LoadSourceKpiDocumentsAsync()100%44100%
SplitCsvOption(...)50%22100%
.ctor(...)100%11100%
.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
 116    public CopyFirestoreContextCommand(
 117        IAnsiConsole console,
 118        IFirebaseServiceFactory firebaseServiceFactory,
 119        ILogger<CopyFirestoreContextCommand> logger)
 20    {
 121        _console = console;
 122        _firebaseServiceFactory = firebaseServiceFactory;
 123        _logger = logger;
 124    }
 25
 26    protected override async Task<int> ExecuteAsync(
 27        CommandContext context,
 28        CopyFirestoreContextSettings settings,
 29        CancellationToken cancellationToken)
 30    {
 31        try
 32        {
 133            var prefixes = SplitCsvOption(settings.ContextPrefix);
 134            var kpiDocumentNames = SplitCsvOption(settings.KpiDocument);
 135            var competition = CompetitionResolver.ResolveCompetition(
 136                settings.Competition,
 137                communityContext: settings.TargetCommunityContext);
 138            var repositoryCompetition = CompetitionResolver.ToRepositoryCompetitionArgument(competition);
 39
 140            _console.MarkupLine($"[green]Copy Firestore context command initialized[/]");
 141            _console.MarkupLine($"[blue]Source community context:[/] [yellow]{settings.SourceCommunityContext}[/]");
 142            _console.MarkupLine($"[blue]Target community context:[/] [yellow]{settings.TargetCommunityContext}[/]");
 143            _console.MarkupLine($"[blue]Using competition:[/] [yellow]{competition}[/]");
 144            if (settings.DryRun)
 45            {
 146                _console.MarkupLine("[magenta]Dry run mode enabled - no Firestore documents will be written[/]");
 47            }
 48
 149            var contextRepository = _firebaseServiceFactory.CreateContextRepository(repositoryCompetition);
 150            var kpiRepository = _firebaseServiceFactory.CreateKpiRepository(repositoryCompetition);
 51
 152            var sourceContextDocuments = await LoadSourceContextDocumentsAsync(
 153                contextRepository,
 154                settings.SourceCommunityContext,
 155                prefixes,
 156                cancellationToken);
 157            var sourceKpiDocuments = await LoadSourceKpiDocumentsAsync(
 158                kpiRepository,
 159                settings.SourceCommunityContext,
 160                kpiDocumentNames,
 161                cancellationToken);
 62
 163            if (sourceContextDocuments.MissingMessages.Count > 0 || sourceKpiDocuments.MissingMessages.Count > 0)
 64            {
 165                foreach (var message in sourceContextDocuments.MissingMessages.Concat(sourceKpiDocuments.MissingMessages
 66                {
 167                    _console.MarkupLine($"[red]{message}[/]");
 68                }
 69
 170                return 1;
 71            }
 72
 173            if (settings.Verbose)
 74            {
 075                foreach (var document in sourceContextDocuments.Documents)
 76                {
 077                    _console.MarkupLine($"[dim]  Context: {document.DocumentName} (version {document.Version})[/]");
 78                }
 79
 080                foreach (var document in sourceKpiDocuments.Documents)
 81                {
 082                    _console.MarkupLine($"[dim]  KPI: {document.DocumentName} (version {document.Version})[/]");
 83                }
 84            }
 85
 186            if (settings.DryRun)
 87            {
 188                _console.MarkupLine($"[magenta]Would copy {sourceContextDocuments.Documents.Count} context document(s) a
 189                return 0;
 90            }
 91
 192            var savedContextCount = 0;
 193            var unchangedContextCount = 0;
 194            foreach (var document in sourceContextDocuments.Documents)
 95            {
 196                var savedVersion = await contextRepository.SaveContextDocumentAsync(
 197                    document.DocumentName,
 198                    document.Content,
 199                    settings.TargetCommunityContext,
 1100                    cancellationToken);
 101
 1102                if (savedVersion.HasValue)
 103                {
 1104                    savedContextCount++;
 105                }
 106                else
 107                {
 0108                    unchangedContextCount++;
 109                }
 110            }
 111
 1112            var savedKpiCount = 0;
 1113            foreach (var document in sourceKpiDocuments.Documents)
 114            {
 1115                await kpiRepository.SaveKpiDocumentAsync(
 1116                    document.DocumentName,
 1117                    document.Content,
 1118                    document.Description,
 1119                    settings.TargetCommunityContext,
 1120                    cancellationToken);
 1121                savedKpiCount++;
 122            }
 123
 1124            _console.MarkupLine($"[green]Copied {savedContextCount} context document(s) and {savedKpiCount} KPI document
 1125            if (unchangedContextCount > 0)
 126            {
 0127                _console.MarkupLine($"[dim]Unchanged context document(s): {unchangedContextCount}[/]");
 128            }
 129
 1130            return 0;
 131        }
 0132        catch (Exception ex)
 133        {
 0134            _logger.LogError(ex, "Error in copy-firestore-context command");
 0135            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 0136            return 1;
 137        }
 1138    }
 139
 140    private async Task<ContextLoadResult> LoadSourceContextDocumentsAsync(
 141        IContextRepository contextRepository,
 142        string sourceCommunityContext,
 143        IReadOnlyList<string> prefixes,
 144        CancellationToken cancellationToken)
 145    {
 1146        if (prefixes.Count == 0)
 147        {
 0148            return new ContextLoadResult([], []);
 149        }
 150
 1151        var documentNames = await contextRepository.GetContextDocumentNamesAsync(sourceCommunityContext, cancellationTok
 1152        var selectedDocumentNames = documentNames
 1153            .Where(name => prefixes.Any(prefix => name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
 1154            .Distinct(StringComparer.OrdinalIgnoreCase)
 1155            .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
 1156            .ToList();
 157
 1158        var missingMessages = new List<string>();
 1159        if (selectedDocumentNames.Count == 0)
 160        {
 1161            missingMessages.Add($"No source context documents found for prefix(es): {string.Join(", ", prefixes)}");
 1162            return new ContextLoadResult([], missingMessages);
 163        }
 164
 1165        var documents = new List<ContextDocument>();
 1166        foreach (var documentName in selectedDocumentNames)
 167        {
 1168            var document = await contextRepository.GetLatestContextDocumentAsync(
 1169                documentName,
 1170                sourceCommunityContext,
 1171                cancellationToken);
 172
 1173            if (document is null)
 174            {
 0175                missingMessages.Add($"Missing source context document: {documentName}");
 176            }
 177            else
 178            {
 1179                documents.Add(document);
 180            }
 1181        }
 182
 1183        return new ContextLoadResult(documents, missingMessages);
 1184    }
 185
 186    private async Task<KpiLoadResult> LoadSourceKpiDocumentsAsync(
 187        IKpiRepository kpiRepository,
 188        string sourceCommunityContext,
 189        IReadOnlyList<string> kpiDocumentNames,
 190        CancellationToken cancellationToken)
 191    {
 1192        var documents = new List<KpiDocument>();
 1193        var missingMessages = new List<string>();
 194
 1195        foreach (var documentName in kpiDocumentNames)
 196        {
 1197            var document = await kpiRepository.GetKpiDocumentAsync(
 1198                documentName,
 1199                sourceCommunityContext,
 1200                cancellationToken);
 201
 1202            if (document is null)
 203            {
 1204                missingMessages.Add($"Missing source KPI document: {documentName}");
 205            }
 206            else
 207            {
 1208                documents.Add(document);
 209            }
 1210        }
 211
 1212        return new KpiLoadResult(documents, missingMessages);
 1213    }
 214
 215    private static IReadOnlyList<string> SplitCsvOption(string? value)
 216    {
 1217        return string.IsNullOrWhiteSpace(value)
 1218            ? []
 1219            : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
 220    }
 221
 1222    private sealed record ContextLoadResult(
 1223        IReadOnlyList<ContextDocument> Documents,
 1224        IReadOnlyList<string> MissingMessages);
 225
 1226    private sealed record KpiLoadResult(
 1227        IReadOnlyList<KpiDocument> Documents,
 1228        IReadOnlyList<string> MissingMessages);
 229}