// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
using Aspire.Cli.Acquisition;
using Aspire.Cli.Agents;
using Aspire.Cli.Agents.Hooks;
using Aspire.Cli.Agents.AspireSkills;
using Aspire.Cli.Agents.Playwright;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Bundles;
using Aspire.Cli.Certificates;
using Aspire.Cli.Commands;
using Aspire.Cli.Commands.Sdk;
using Aspire.Cli.Documentation.ApiDocs;
using Aspire.Cli.DotNet;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.Layout;
using Aspire.Cli.Mcp;
using Aspire.Cli.Documentation.Docs;
using Aspire.Cli.NuGet;
using Aspire.Cli.Processes;
using Aspire.Cli.Projects;
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Secrets;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Templating;
using Aspire.Cli.Tests.Telemetry;
using Aspire.Cli.Tests.TestServices;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Spectre.Console;
using Aspire.Cli.Configuration;
using Aspire.Cli.Migrations;
using Aspire.Cli.Utils;
using Aspire.Cli.Utils.EnvironmentChecker;
using Aspire.Cli.Packaging;
using Aspire.Cli.Caching;
using Aspire.Cli.Diagnostics;
using Aspire.Cli.Npm;
using Aspire.Cli.Profiling;
namespace Aspire.Cli.Tests.Utils;
internal static class CliTestHelper
{
public static InstallSidecarReader CreateSidecarReader(ITestOutputHelper outputHelper)
{
var loggerFactory = LoggerFactory.Create(builder => builder.AddXunit(outputHelper));
return new InstallSidecarReader(loggerFactory.CreateLogger<InstallSidecarReader>());
}
public static ServiceProvider CreateExtensionServiceProvider(
TemporaryWorkspace workspace,
ITestOutputHelper outputHelper,
Action<string, string?, bool, DebugSessionOptions?> startDebugSessionCallback,
Action<CliServiceCollectionTestOptions>? configureOptions = null,
Action<IServiceCollection>? configureServices = null)
{
var services = CreateServiceCollection(workspace, outputHelper, testOptions =>
{
configureOptions?.Invoke(testOptions);
testOptions.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel();
testOptions.InteractionServiceFactory = sp => new TestExtensionInteractionService(sp)
{
StartDebugSessionCallback = startDebugSessionCallback
};
});
configureServices?.Invoke(services);
return services.BuildServiceProvider();
}
public static IServiceCollection CreateServiceCollection(TemporaryWorkspace workspace, ITestOutputHelper outputHelper, Action<CliServiceCollectionTestOptions>? configure = null)
{
var options = new CliServiceCollectionTestOptions(outputHelper, workspace.WorkspaceRoot);
configure?.Invoke(options);
var services = new ServiceCollection();
var configBuilder = new ConfigurationBuilder();
var configurationValues = new Dictionary<string, string?>();
// Populate feature flag configuration in in-memory collection.
options.ConfigurationCallback += config => {
foreach (var featureFlag in options.EnabledFeatures)
{
config[$"{KnownFeatures.FeaturePrefix}:{featureFlag}"] = "true";
}
foreach (var featureFlag in options.DisabledFeatures)
{
config[$"{KnownFeatures.FeaturePrefix}:{featureFlag}"] = "false";
}
};
options.ConfigurationCallback(configurationValues);
if (options.DisableAnsi)
{
configurationValues.TryAdd("NO_COLOR", "1");
}
configBuilder.AddInMemoryCollection(configurationValues);
var globalSettingsFilePath = Path.Combine(options.WorkingDirectory.FullName, ".aspire", "settings.global.json");
var globalSettingsFile = new FileInfo(globalSettingsFilePath);
ConfigurationHelper.RegisterSettingsFiles(configBuilder, options.WorkingDirectory, globalSettingsFile);
var configuration = configBuilder.Build();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging(b => b.SetMinimumLevel(LogLevel.Trace)).AddXunitLogging(outputHelper);
// Register logging options for test. The FileLoggerProvider is created inside the
// factory callback so the DI container owns the instance and disposes it (closing
// the log file handle) when the ServiceProvider is disposed.
var testLogsDirectory = Path.Combine(options.WorkingDirectory.FullName, ".aspire", "logs");
var testLogFilePath = FileLoggerProvider.GenerateLogFilePath(testLogsDirectory, TimeProvider.System);
services.AddSingleton(sp => new FileLoggerProvider(testLogFilePath, new TestStartupErrorWriter()));
services.AddSingleton(new Program.CliLoggingOptions(ConsoleLogLevel: null, DebugMode: false, LogsDirectory: testLogsDirectory, LogFilePath: testLogFilePath));
services.AddMemoryCache();
services.AddSingleton<ConsoleLogBufferContext>();
services.AddSingleton(options.ConsoleEnvironmentFactory);
services.AddSingleton(sp => sp.GetRequiredService<ConsoleEnvironment>().Out);
services.AddSingleton(options.TimeProvider);
services.AddSingleton(options.TelemetryFactory);
services.AddSingleton<ProfilingTelemetry>();
services.AddSingleton(options.ProjectLocatorFactory);
services.AddSingleton(options.SolutionLocatorFactory);
services.AddSingleton(options.ExtensionRpcTargetFactory);
services.AddTransient(options.ExtensionBackchannelFactory);
services.AddSingleton(options.InteractionServiceFactory);
services.AddSingleton(options.CertificateToolRunnerFactory);
services.AddSingleton(options.CertificateServiceFactory);
services.AddSingleton(options.NewCommandPrompterFactory);
services.AddSingleton<ITemplateVersionPrompter>(sp => (ITemplateVersionPrompter)sp.GetRequiredService<INewCommandPrompter>());
services.AddSingleton(options.AddCommandPrompterFactory);
services.AddSingleton(options.PublishCommandPrompterFactory);
services.AddTransient(options.DotNetCliExecutionFactoryFactory);
services.AddTransient(options.DotNetCliRunnerFactory);
services.AddTransient(options.NuGetPackageCacheFactory);
services.AddSingleton<TemplateNuGetConfigService>();
services.AddSingleton(options.TemplateProviderFactory);
services.TryAddEnumerable(ServiceDescriptor.Singleton<ITemplateFactory, DotNetTemplateFactory>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<ITemplateFactory, CliTemplateFactory>());
services.AddSingleton(options.ConfigurationServiceFactory);
services.AddSingleton(options.FeatureFlagsFactory);
services.AddSingleton(options.CliUpdateNotifierFactory);
services.AddSingleton<IDotNetSdkInstaller>(options.DotNetSdkInstallerFactory);
services.AddSingleton(options.PackagingServiceFactory);
services.AddSingleton(options.CliExecutionContextFactory);
services.AddSingleton(options.DiskCacheFactory);
services.AddSingleton<IAppHostInfoDiskCache, NullAppHostInfoDiskCache>();
services.AddSingleton<IAppHostInfoResolver, AppHostInfoResolver>();
services.AddSingleton(options.CliHostEnvironmentFactory);
services.AddSingleton(options.CliDownloaderFactory);
services.AddSingleton(options.ProcessPathProviderFactory);
services.AddSingleton(options.FirstTimeUseNoticeSentinelFactory);
services.AddSingleton(options.BannerServiceFactory);
services.AddSingleton<FallbackProjectParser>();
services.AddSingleton(options.ProjectUpdaterFactory);
services.AddSingleton<NuGetPackagePrefetcher>();
services.AddSingleton<IHostedService>(sp => sp.GetRequiredService<NuGetPackagePrefetcher>());
services.AddSingleton(options.AuxiliaryBackchannelMonitorFactory);
services.AddSingleton(options.AgentEnvironmentDetectorFactory);
services.AddSingleton(options.GitRepositoryFactory);
services.AddSingleton(options.NpmRunnerFactory);
services.AddSingleton(options.NpmProvenanceCheckerFactory);
services.AddSingleton(options.AspireSkillsInstallerFactory);
services.AddSingleton(options.PlaywrightCliRunnerFactory);
services.AddSingleton<PlaywrightCliInstaller>();
services.AddSingleton<ITelemetryHookInstaller, TelemetryHookInstaller>();
services.AddSingleton(options.TelemetryHookConfiguratorFactory);
services.AddSingleton(options.ScaffoldingServiceFactory);
services.AddSingleton<IAppHostServerProjectFactory, AppHostServerProjectFactory>();
services.AddSingleton<IAppHostServerSessionFactory, AppHostServerSessionFactory>();
services.AddSingleton<ILanguageDiscovery, DefaultLanguageDiscovery>();
services.AddSingleton(options.LanguageServiceFactory);
services.AddSingleton<TemplateNuGetConfigService>();
// Bundle layout services - return null/no-op implementations to trigger SDK mode fallback
// This ensures backward compatibility: no layout found = use legacy SDK mode
services.AddSingleton(options.LayoutDiscoveryFactory);
services.AddTransient<LayoutProcessRunner>();
services.AddTransient<ProcessTreeGracefulShutdownService>();
// Mirror Program.cs so consumers (e.g. GuestAppHostProject) that depend on the
// interface receive the same ProcessTreeGracefulShutdownService instance the abstraction
// wraps. Without this, DI returns null and Run-path tests construct the project with
// a missing dependency, masking wiring regressions.
services.AddTransient<IProcessTreeGracefulShutdownSignaler>(sp => sp.GetRequiredService<ProcessTreeGracefulShutdownService>());
services.AddTransient<IAppHostStopper>(sp => sp.GetRequiredService<ProcessTreeGracefulShutdownService>());
services.AddTransient<OrphanedAppHostCollector>();
// Match Program.Main's ConsoleCancellationManager (5s finalDrainBudget) so tests exercise the
// same shutdown ladder budget as production. RunCommand and GuestAppHostProject require these
// services in production wiring. IGracefulShutdownWindow resolves to the same CCM instance,
// mirroring Program.cs.
services.AddSingleton(sp => new ConsoleCancellationManager(
finalDrainBudget: TimeSpan.FromSeconds(5)));
services.AddSingleton<IGracefulShutdownWindow>(sp => sp.GetRequiredService<ConsoleCancellationManager>());
services.AddSingleton(options.BundlePayloadProviderFactory);
services.AddSingleton(options.BundleServiceFactory);
services.AddSingleton<BundleNuGetService>();
services.AddSingleton<IInstallSidecarReader, InstallSidecarReader>();
services.AddSingleton<IPeerInstallProbe, PeerInstallProbe>();
services.AddSingleton<IInstallationDiscovery, InstallationDiscovery>();
services.AddSingleton<WingetFirstRunProbe>();
// Always register the null reader by default so unit tests don't reach into the
// actual user registry through WingetFirstRunProbe on Windows. Tests that need
// the real reader (or a fake) should replace the registration explicitly.
services.AddSingleton<IWindowsRegistryReader, NullWindowsRegistryReader>();
// IdentityChannelReader for AspireVersionCheck (doctor) — uses the same
// pattern as production wiring in Program.cs.
services.AddSingleton<IIdentityChannelReader>(_ => new IdentityChannelReader(typeof(Program).Assembly));
services.AddSingleton<IEnvironment, TestEnvironment>();
services.AddSingleton<ProfileCaptureState>();
services.AddSingleton<ProfileCaptureService>();
// AppHost project handlers - must match Program.cs registration pattern
services.AddSingleton<DotNetAppHostProject>();
services.AddSingleton<Func<LanguageInfo, GuestAppHostProject>>(sp =>
{
return language => ActivatorUtilities.CreateInstance<GuestAppHostProject>(sp, language);
});
services.AddSingleton(options.AppHostProjectFactory);
services.AddSingleton<IEnvironmentCheck, AspireVersionCheck>();
services.AddSingleton<IEnvironmentCheck, OperatingSystemCheck>();
services.AddSingleton<IEnvironmentCheck, WslEnvironmentCheck>();
services.AddSingleton<IEnvironmentCheck, DotNetSdkCheck>();
services.AddSingleton<IEnvironmentCheck, TypeScriptAppHostToolingCheck>();
services.AddSingleton<IEnvironmentCheck, DeprecatedWorkloadCheck>();
services.AddSingleton<IEnvironmentCheck, DevCertsCheck>();
services.AddSingleton<IEnvironmentCheck, ContainerRuntimeCheck>();
services.AddSingleton<IDcpConnectionChecker, TestDcpConnectionChecker>();
services.AddSingleton<IEnvironmentCheck, DcpConnectionHealthCheck>();
services.AddSingleton<IEnvironmentCheck, DeprecatedAgentConfigCheck>();
services.AddSingleton<IEnvironmentChecker, EnvironmentChecker>();
// MCP server transport
services.AddSingleton(options.McpServerTransportFactory);
// Documentation services - use test doubles
services.AddSingleton<IDocsCache, DocsCache>();
services.AddSingleton<IHttpClientFactory, TestHttpClientFactory>();
services.AddSingleton<IDocsFetcher, TestDocsFetcher>();
services.AddSingleton(options.DocsIndexServiceFactory);
services.AddSingleton(options.DocsSearchServiceFactory);
services.AddSingleton<IApiDocsCache, ApiDocsCache>();
services.AddSingleton<IApiDocsFetcher, TestApiDocsFetcher>();
services.AddSingleton(options.ApiDocsIndexServiceFactory);
services.AddSingleton<CommonCommandServices>();
services.AddSingleton<ResourceWaitService>();
services.AddTransient<AppHostConnectionResolver>();
services.AddTransient<RootCommand>();
services.AddTransient<NewCommand>();
services.AddTransient<InitCommand>();
services.AddTransient<AppHostLauncher>();
services.AddTransient<DcpWorkloadCleanupService>();
services.AddTransient<RunCommand>();
services.AddTransient<StopCommand>();
services.AddTransient<StartCommand>();
services.AddTransient<ResourceCommand>();
services.AddTransient<LsCommand>();
services.AddTransient<PsCommand>();
services.AddTransient<DescribeCommand>();
services.AddTransient<LogsCommand>();
services.AddTransient<TerminalCommand>();
services.AddTransient<TerminalAttachCommand>();
services.AddTransient<TerminalPsCommand>();
services.AddTransient<IntegrationPackageSearchService>();
services.AddTransient<IntegrationCommand>();
services.AddTransient<IntegrationListCommand>();
services.AddTransient<IntegrationSearchCommand>();
services.AddTransient<AddCommand>();
services.AddTransient<DeployCommand>();
services.AddTransient<DestroyCommand>();
services.AddTransient<DoCommand>();
services.AddTransient<PublishCommand>();
services.AddTransient<ConfigCommand>();
services.AddTransient<CacheCommand>();
services.AddTransient<CertificatesCommand>();
services.AddTransient<CertificatesCleanCommand>();
services.AddTransient<CertificatesTrustCommand>();
services.AddTransient<DoctorCommand>();
services.AddTransient<DashboardCommand>();
services.AddTransient<DashboardRunCommand>();
services.AddTransient<UpdateCommand>();
services.AddTransient<SetupCommand>();
services.AddTransient<McpCommand>();
services.AddTransient<McpStartCommand>();
services.AddTransient<McpInitCommand>();
services.AddTransient<McpToolsCommand>();
services.AddTransient<McpCallCommand>();
services.AddTransient<AgentCommand>();
services.AddTransient<AgentMcpCommand>();
services.AddTransient<AgentInitCommand>();
services.AddTransient<AgentTelemetryCommand>();
services.AddSingleton<ResourceColorMap>();
services.AddTransient<TelemetryCommand>();
services.AddTransient<TelemetryLogsCommand>();
services.AddTransient<TelemetrySpansCommand>();
services.AddTransient<TelemetryTracesCommand>();
services.AddTransient<ExportCommand>();
services.AddTransient<ExtensionInternalCommand>();
services.AddTransient<WaitCommand>();
services.AddTransient<RestoreCommand>();
services.AddSingleton<IMigration, TypeScriptAppHostMigration>();
services.AddTransient<SdkCommand>();
services.AddTransient<SdkGenerateCommand>();
services.AddTransient<SdkDumpCommand>();
services.AddTransient<SdkExportCommand>();
services.AddTransient<ApiCommand>();
services.AddTransient<ApiListCommand>();
services.AddTransient<ApiSearchCommand>();
services.AddTransient<ApiGetCommand>();
services.AddTransient<DocsCommand>();
services.AddTransient<DocsListCommand>();
services.AddTransient<DocsSearchCommand>();
services.AddTransient<DocsGetCommand>();
services.AddTransient<SecretCommand>();
services.AddTransient<SecretSetCommand>();
services.AddTransient<SecretGetCommand>();
services.AddTransient<SecretListCommand>();
services.AddTransient<SecretPathCommand>();
services.AddTransient<SecretDeleteCommand>();
services.AddTransient<SecretStoreResolver>();
#if DEBUG
services.AddTransient<RenderCommand>();
#endif
services.AddTransient(options.AppHostBackchannelFactory);
return services;
}
}
internal sealed class CliServiceCollectionTestOptions
{
private readonly ITestOutputHelper _outputHelper;
public CliServiceCollectionTestOptions(ITestOutputHelper outputHelper, DirectoryInfo workingDirectory)
{
_outputHelper = outputHelper;
WorkingDirectory = workingDirectory;
ProjectLocatorFactory = CreateDefaultProjectLocatorFactory;
SolutionLocatorFactory = CreateDefaultSolutionLocatorFactory;
ConfigurationServiceFactory = CreateDefaultConfigurationServiceFactory;
CliExecutionContextFactory = CreateDefaultCliExecutionContextFactory;
}
private CliExecutionContext CreateDefaultCliExecutionContextFactory(IServiceProvider provider)
{
return TestExecutionContextHelper.CreateExecutionContext(WorkingDirectory, packagesDirectory: PackagesDirectory);
}
public DirectoryInfo WorkingDirectory { get; set; }
public DirectoryInfo? PackagesDirectory { get; set; }
public Action<Dictionary<string, string?>> ConfigurationCallback { get; set; } = (Dictionary<string, string?> config) =>
{
};
public string[] EnabledFeatures { get; set; } = Array.Empty<string>();
public string[] DisabledFeatures { get; set; } = Array.Empty<string>();
public TestOutputTextWriter? OutputTextWriter { get; set; }
public StringWriter? ErrorTextWriter { get; set; }
public bool DisableAnsi { get; set; }
public TimeProvider TimeProvider { get; set; } = TimeProvider.System;
public Func<IServiceProvider, ConsoleEnvironment> ConsoleEnvironmentFactory => (IServiceProvider serviceProvider) =>
{
var outputTextWriter = OutputTextWriter ?? new TestOutputTextWriter(_outputHelper);
var errorTextWriter = ErrorTextWriter ?? new StringWriter();
var outConsole = CreateAnsiConsole(outputTextWriter, !DisableAnsi);
var errorConsole = CreateAnsiConsole(errorTextWriter, !DisableAnsi);
return new ConsoleEnvironment(outConsole, errorConsole);
};
private static IAnsiConsole CreateAnsiConsole(TextWriter textWriter, bool ansi = true)
{
var settings = new AnsiConsoleSettings()
{
Ansi = ansi ? AnsiSupport.Yes : AnsiSupport.No,
Interactive = InteractionSupport.Yes,
ColorSystem = ansi ? ColorSystemSupport.Standard : ColorSystemSupport.NoColors,
Out = new AnsiConsoleOutput(textWriter),
Enrichment = new ProfileEnrichment { UseDefaultEnrichers = false }
};
var console = AnsiConsole.Create(settings);
if (!ansi)
{
// Use a large width to prevent Spectre.Console from word-wrapping output lines.
console.Profile.Width = int.MaxValue;
// Disable link capabilities to prevent OSC 8 hyperlink sequences in output.
console.Profile.Capabilities.Links = false;
}
return console;
}
public Func<IServiceProvider, INewCommandPrompter> NewCommandPrompterFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
return new NewCommandPrompter(interactionService);
};
public Func<IServiceProvider, ICliUpdateNotifier> CliUpdateNotifierFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var logger = NullLoggerFactory.Instance.CreateLogger<CliUpdateNotifier>();
var nuGetPackageCache = serviceProvider.GetRequiredService<INuGetPackageCache>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
var processPathProvider = serviceProvider.GetRequiredService<IProcessPathProvider>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
return new CliUpdateNotifier(logger, nuGetPackageCache, interactionService, processPathProvider, executionContext);
};
public Func<IServiceProvider, IAddCommandPrompter> AddCommandPrompterFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
return new AddCommandPrompter(interactionService);
};
public Func<IServiceProvider, IPublishCommandPrompter> PublishCommandPrompterFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
return new PublishCommandPrompter(interactionService);
};
public Func<IServiceProvider, IConfigurationService> ConfigurationServiceFactory { get; set; }
public IConfigurationService CreateDefaultConfigurationServiceFactory(IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
return new ConfigurationService(configuration, executionContext, GetGlobalSettingsFile(WorkingDirectory), NullLogger<ConfigurationService>.Instance);
}
private static FileInfo GetGlobalSettingsFile(DirectoryInfo workingDirectory)
{
var globalSettingsFilePath = Path.Combine(workingDirectory.FullName, ".aspire", "settings.global.json");
return new FileInfo(globalSettingsFilePath);
}
public Func<IServiceProvider, IProjectLocator> ProjectLocatorFactory { get; set; }
public Func<IServiceProvider, ISolutionLocator> SolutionLocatorFactory { get; set; }
public Func<IServiceProvider, CliExecutionContext> CliExecutionContextFactory { get; set; }
public Func<IServiceProvider, IAppHostProjectFactory> AppHostProjectFactory { get; set; } = serviceProvider => ActivatorUtilities.CreateInstance<AppHostProjectFactory>(serviceProvider);
public Func<IServiceProvider, IFirstTimeUseNoticeSentinel> FirstTimeUseNoticeSentinelFactory { get; set; } = _ => new TestFirstTimeUseNoticeSentinel();
public Func<IServiceProvider, IBannerService> BannerServiceFactory { get; set; } = _ => new TestBannerService();
public IProjectLocator CreateDefaultProjectLocatorFactory(IServiceProvider serviceProvider)
{
var logger = serviceProvider.GetRequiredService<ILogger<ProjectLocator>>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
var configurationService = serviceProvider.GetRequiredService<IConfigurationService>();
var projectFactory = serviceProvider.GetService<IAppHostProjectFactory>() ?? new TestAppHostProjectFactory();
var languageDiscovery = serviceProvider.GetService<ILanguageDiscovery>() ?? new TestLanguageDiscovery();
var sdkInstaller = serviceProvider.GetRequiredService<IDotNetSdkInstaller>();
var gitRepository = serviceProvider.GetRequiredService<IGitRepository>();
var profilingTelemetry = serviceProvider.GetRequiredService<ProfilingTelemetry>();
var environment = serviceProvider.GetRequiredService<IEnvironment>();
var appHostCandidateFinder = serviceProvider.GetService<IAppHostCandidateFinder>()
?? new AppHostCandidateFinder(gitRepository, environment, profilingTelemetry, NullLogger<AppHostCandidateFinder>.Instance);
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
return new ProjectLocator(logger, executionContext, environment, interactionService, configurationService, projectFactory, languageDiscovery, sdkInstaller, appHostCandidateFinder, telemetry, configuration);
}
public ISolutionLocator CreateDefaultSolutionLocatorFactory(IServiceProvider serviceProvider)
{
var logger = serviceProvider.GetRequiredService<ILogger<SolutionLocator>>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
return new SolutionLocator(logger, interactionService);
}
public Func<IServiceProvider, AspireCliTelemetry> TelemetryFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
return TestTelemetryHelper.CreateInitializedTelemetry();
};
public Func<IServiceProvider, IProjectUpdater> ProjectUpdaterFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<ProjectUpdater>>();
var runner = serviceProvider.GetRequiredService<IDotNetCliRunner>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
var cache = serviceProvider.GetRequiredService<IMemoryCache>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var fallbackParser = serviceProvider.GetRequiredService<FallbackProjectParser>();
return new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser);
};
public Func<IServiceProvider, ICliHostEnvironment> CliHostEnvironmentFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
return new CliHostEnvironment(configuration, nonInteractive: true);
};
public Func<IServiceProvider, IInteractionService> InteractionServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var consoleEnvironment = serviceProvider.GetRequiredService<ConsoleEnvironment>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var hostEnvironment = serviceProvider.GetRequiredService<ICliHostEnvironment>();
var processPathProvider = serviceProvider.GetRequiredService<IProcessPathProvider>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logBufferContext = serviceProvider.GetRequiredService<ConsoleLogBufferContext>();
return new ConsoleInteractionService(consoleEnvironment, executionContext, hostEnvironment, processPathProvider, loggerFactory, logBufferContext);
};
public Func<IServiceProvider, ICertificateToolRunner> CertificateToolRunnerFactory { get; set; } = (IServiceProvider _) =>
{
// Use TestCertificateToolRunner by default to avoid calling real dotnet dev-certs
// which can be slow or block on macOS (keychain access prompts)
return new TestCertificateToolRunner();
};
public Func<IServiceProvider, ICertificateService> CertificateServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var certificateToolRunner = serviceProvider.GetRequiredService<ICertificateToolRunner>();
var interactiveService = serviceProvider.GetRequiredService<IInteractionService>();
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var hostEnvironment = serviceProvider.GetRequiredService<ICliHostEnvironment>();
var environment = serviceProvider.GetRequiredService<IEnvironment>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var logger = serviceProvider.GetRequiredService<ILogger<CertificateService>>();
return new CertificateService(certificateToolRunner, interactiveService, telemetry, hostEnvironment, environment, executionContext, logger);
};
public Func<IServiceProvider, IScaffoldingService> ScaffoldingServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var appHostServerProjectFactory = serviceProvider.GetRequiredService<IAppHostServerProjectFactory>();
var serverSessionFactory = serviceProvider.GetRequiredService<IAppHostServerSessionFactory>();
var languageDiscovery = serviceProvider.GetRequiredService<ILanguageDiscovery>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
var logger = serviceProvider.GetRequiredService<ILogger<ScaffoldingService>>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
return new ScaffoldingService(appHostServerProjectFactory, serverSessionFactory, languageDiscovery, interactionService, serviceProvider.GetRequiredService<IEnvironment>(), logger, executionContext, serviceProvider.GetRequiredService<ProfilingTelemetry>());
};
public Func<IServiceProvider, IProcessExecutionFactory> DotNetCliExecutionFactoryFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
return new TestProcessExecutionFactory();
};
public Func<IServiceProvider, IDotNetCliRunner> DotNetCliRunnerFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<DotNetCliRunner>>();
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var profilingTelemetry = serviceProvider.GetRequiredService<ProfilingTelemetry>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var features = serviceProvider.GetRequiredService<IFeatures>();
var diskCache = serviceProvider.GetRequiredService<IDiskCache>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var executionFactory = serviceProvider.GetRequiredService<IProcessExecutionFactory>();
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
return new DotNetCliRunner(logger, serviceProvider, telemetry, profilingTelemetry, configuration, diskCache, features, interactionService, executionContext, executionFactory, new HostEnvironment());
};
public Func<IServiceProvider, IDotNetSdkInstaller> DotNetSdkInstallerFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
return new TestDotNetSdkInstaller();
};
public Func<IServiceProvider, INuGetPackageCache> NuGetPackageCacheFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var runner = serviceProvider.GetRequiredService<IDotNetCliRunner>();
var cache = serviceProvider.GetRequiredService<IMemoryCache>();
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var features = serviceProvider.GetRequiredService<IFeatures>();
return new NuGetPackageCache(runner, cache, telemetry, features);
};
public Func<IServiceProvider, IAppHostCliBackchannel> AppHostBackchannelFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var logger = serviceProvider.GetRequiredService<ILogger<AppHostCliBackchannel>>();
var environment = serviceProvider.GetRequiredService<IEnvironment>();
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var profilingTelemetry = serviceProvider.GetRequiredService<ProfilingTelemetry>();
return new AppHostCliBackchannel(logger, environment, telemetry, profilingTelemetry);
};
public Func<IServiceProvider, IExtensionRpcTarget> ExtensionRpcTargetFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
return new ExtensionRpcTarget(configuration, executionContext);
};
public Func<IServiceProvider, IExtensionBackchannel> ExtensionBackchannelFactory { get; set; } = serviceProvider =>
{
var logger = serviceProvider.GetRequiredService<ILogger<ExtensionBackchannel>>();
var rpcTarget = serviceProvider.GetRequiredService<IExtensionRpcTarget>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
return new ExtensionBackchannel(logger, rpcTarget, configuration);
};
public Func<IServiceProvider, IFeatures> FeatureFlagsFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var logger = serviceProvider.GetRequiredService<ILogger<Features>>();
return new Features(configuration, logger);
};
public Func<IServiceProvider, ITemplateProvider> TemplateProviderFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var interactionService = serviceProvider.GetRequiredService<IInteractionService>();
var runner = serviceProvider.GetRequiredService<IDotNetCliRunner>();
var certificateService = serviceProvider.GetRequiredService<ICertificateService>();
var packagingService = serviceProvider.GetRequiredService<IPackagingService>();
var prompter = serviceProvider.GetRequiredService<INewCommandPrompter>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var features = serviceProvider.GetRequiredService<IFeatures>();
var configurationService = serviceProvider.GetRequiredService<IConfigurationService>();
var hostEnvironment = serviceProvider.GetRequiredService<ICliHostEnvironment>();
var sdkInstaller = serviceProvider.GetRequiredService<IDotNetSdkInstaller>();
var telemetry = serviceProvider.GetRequiredService<AspireCliTelemetry>();
var templateVersionPrompter = serviceProvider.GetRequiredService<ITemplateVersionPrompter>();
var languageDiscovery = serviceProvider.GetRequiredService<ILanguageDiscovery>();
var scaffoldingService = serviceProvider.GetRequiredService<IScaffoldingService>();
var cliTemplateLogger = serviceProvider.GetRequiredService<ILogger<CliTemplateFactory>>();
var templateNuGetConfigService = serviceProvider.GetRequiredService<TemplateNuGetConfigService>();
var dotNetFactory = new DotNetTemplateFactory(interactionService, runner, certificateService, prompter, executionContext, sdkInstaller, features, telemetry, hostEnvironment, templateNuGetConfigService, new HostEnvironment());
var projectFactory = serviceProvider.GetRequiredService<IAppHostProjectFactory>();
var cliFactory = new CliTemplateFactory(languageDiscovery, projectFactory, scaffoldingService, prompter, executionContext, interactionService, hostEnvironment, serviceProvider.GetRequiredService<IEnvironment>(), templateNuGetConfigService, cliTemplateLogger);
return new TemplateProvider([dotNetFactory, cliFactory]);
};
public Func<IServiceProvider, IPackagingService> PackagingServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var nuGetPackageCache = serviceProvider.GetRequiredService<INuGetPackageCache>();
var features = serviceProvider.GetRequiredService<IFeatures>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
return new PackagingService(executionContext, nuGetPackageCache, features, configuration, NullLogger<PackagingService>.Instance);
};
public Func<IServiceProvider, IDiskCache> DiskCacheFactory { get; set; } = (IServiceProvider serviceProvider) => new NullDiskCache();
public Func<IServiceProvider, ICliDownloader> CliDownloaderFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var tmpDirectory = new DirectoryInfo(Path.Combine(executionContext.WorkingDirectory.FullName, "tmp"));
return new TestCliDownloader(tmpDirectory);
};
public Func<IServiceProvider, IProcessPathProvider> ProcessPathProviderFactory { get; set; } = _ => new EnvironmentProcessPathProvider();
public Func<IServiceProvider, IAuxiliaryBackchannelMonitor> AuxiliaryBackchannelMonitorFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
return new TestAuxiliaryBackchannelMonitor();
};
public Func<IServiceProvider, IAgentEnvironmentDetector> AgentEnvironmentDetectorFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
return new AgentEnvironmentDetector([]);
};
public Func<IServiceProvider, IGitRepository> GitRepositoryFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var logger = serviceProvider.GetRequiredService<ILogger<GitRepository>>();
var profilingTelemetry = serviceProvider.GetRequiredService<ProfilingTelemetry>();
return new GitRepository(executionContext, serviceProvider.GetRequiredService<IEnvironment>(), logger, profilingTelemetry);
};
public Func<IServiceProvider, INpmRunner> NpmRunnerFactory { get; set; } = _ => new FakeNpmRunner();
public Func<IServiceProvider, INpmProvenanceChecker> NpmProvenanceCheckerFactory { get; set; } = _ => new FakeNpmProvenanceChecker();
public Func<IServiceProvider, IAspireSkillsInstaller> AspireSkillsInstallerFactory { get; set; } = serviceProvider => new FakeAspireSkillsInstaller(serviceProvider.GetRequiredService<CliExecutionContext>());
public Func<IServiceProvider, IPlaywrightCliRunner> PlaywrightCliRunnerFactory { get; set; } = _ => new FakePlaywrightCliRunner();
// Defaults to the real configurator (resolving ITelemetryHookInstaller/CliExecutionContext/IEnvironment
// from DI) so agent-init tests exercise the shipped behavior; a test can override it to simulate a
// failure and assert hook installation never aborts `agent init`.
public Func<IServiceProvider, ITelemetryHookConfigurator> TelemetryHookConfiguratorFactory { get; set; }
= serviceProvider => ActivatorUtilities.CreateInstance<TelemetryHookConfigurator>(serviceProvider);
public Func<IServiceProvider, ILanguageService> LanguageServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var projects = serviceProvider.GetServices<IAppHostProject>();
var defaultProject = projects.FirstOrDefault(p => p.LanguageId == KnownLanguageId.CSharp)
?? serviceProvider.GetService<DotNetAppHostProject>();
return new TestLanguageService { DefaultProject = defaultProject };
};
// Layout discovery - returns null by default (no bundle layout), causing SDK mode fallback
public Func<IServiceProvider, ILayoutDiscovery> LayoutDiscoveryFactory { get; set; } = _ => new NullLayoutDiscovery();
// Bundle service - returns no-op implementation by default (no embedded bundle)
public Func<IServiceProvider, IBundleService> BundleServiceFactory { get; set; } = _ => new NullBundleService();
// Bundle payload provider - returns no-payload provider by default
public Func<IServiceProvider, IBundlePayloadProvider> BundlePayloadProviderFactory { get; set; } = _ => new NullBundlePayloadProvider();
public Func<IServiceProvider, IMcpTransportFactory> McpServerTransportFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
return new StdioMcpTransportFactory(loggerFactory ?? NullLoggerFactory.Instance);
};
public Func<IServiceProvider, IDocsIndexService> DocsIndexServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var fetcher = serviceProvider.GetRequiredService<IDocsFetcher>();
var cache = serviceProvider.GetRequiredService<IDocsCache>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var logger = serviceProvider.GetRequiredService<ILogger<DocsIndexService>>();
return new DocsIndexService(fetcher, cache, configuration, logger);
};
public Func<IServiceProvider, IDocsSearchService> DocsSearchServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var indexService = serviceProvider.GetRequiredService<IDocsIndexService>();
var logger = serviceProvider.GetRequiredService<ILogger<DocsSearchService>>();
return new DocsSearchService(indexService, logger);
};
public Func<IServiceProvider, IApiDocsIndexService> ApiDocsIndexServiceFactory { get; set; } = (IServiceProvider serviceProvider) =>
{
var fetcher = serviceProvider.GetRequiredService<IApiDocsFetcher>();
var cache = serviceProvider.GetRequiredService<IApiDocsCache>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var logger = serviceProvider.GetRequiredService<ILogger<ApiDocsIndexService>>();
return new ApiDocsIndexService(fetcher, cache, configuration, logger);
};
}
/// <summary>
/// A layout discovery that always returns null (no bundle layout).
/// Used in tests to ensure SDK mode is used.
/// </summary>
internal sealed class NullLayoutDiscovery : ILayoutDiscovery
{
public LayoutConfiguration? DiscoverLayout(string? projectDirectory = null) => null;
public string? GetComponentPath(LayoutComponent component, string? projectDirectory = null) => null;
public bool IsBundleModeAvailable(string? projectDirectory = null) => false;
}
internal sealed class FixedLayoutDiscovery : ILayoutDiscovery
{
private readonly LayoutConfiguration? _layout;
private readonly Dictionary<LayoutComponent, string> _componentPaths = [];
public FixedLayoutDiscovery(LayoutConfiguration layout)
{
_layout = layout;
}
public FixedLayoutDiscovery(LayoutComponent component, string componentPath)
{
_componentPaths.Add(component, componentPath);
}
public LayoutConfiguration? DiscoverLayout(string? projectDirectory = null) => _layout;
public string? GetComponentPath(LayoutComponent component, string? projectDirectory = null) =>
_layout?.GetComponentPath(component) ??
(_componentPaths.TryGetValue(component, out var componentPath) ? componentPath : null);
public bool IsBundleModeAvailable(string? projectDirectory = null) => _layout is not null || _componentPaths.Count > 0;
}
/// <summary>
/// A no-op bundle service that never extracts anything.
/// Used in tests to ensure SDK mode fallback.
/// </summary>
internal sealed class NullBundleService : IBundleService
{
public bool IsBundle => false;
public Task EnsureExtractedAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<BundleExtractResult> ExtractAsync(string destinationPath, bool force = false, CancellationToken cancellationToken = default)
=> Task.FromResult(BundleExtractResult.NoPayload);
public Task<BundleLayoutLease?> EnsureExtractedAndAcquireLayoutAsync(string holderKind, string? commandName = null, CancellationToken cancellationToken = default)
=> Task.FromResult<BundleLayoutLease?>(null);
public string? GetDefaultExtractDir(string processPath) => null;
}
/// <summary>
/// A no-op payload provider that reports no payload available.
/// </summary>
internal sealed class NullBundlePayloadProvider : IBundlePayloadProvider
{
public bool HasPayload => false;
public Stream? OpenPayload() => null;
}
/// <summary>
/// A configurable bundle service for testing bundle-dependent behavior.
/// </summary>
internal sealed class TestBundleService(bool isBundle) : IBundleService
{
public bool IsBundle => isBundle;
public Layout.LayoutConfiguration? Layout { get; set; }
public Exception? EnsureExtractedException { get; set; }
public Func<CancellationToken, Task>? EnsureExtractedAsyncCallback { get; set; }
public Func<CancellationToken, Task>? EnsureExtractedAndAcquireLayoutAsyncCallback { get; set; }
public Task EnsureExtractedAsync(CancellationToken cancellationToken = default)
=> EnsureExtractedAsyncCallback?.Invoke(cancellationToken) ?? Task.CompletedTask;
public Task<BundleExtractResult> ExtractAsync(string destinationPath, bool force = false, CancellationToken cancellationToken = default)
=> Task.FromResult(isBundle ? BundleExtractResult.AlreadyUpToDate : BundleExtractResult.NoPayload);
public async Task<BundleLayoutLease?> EnsureExtractedAndAcquireLayoutAsync(string holderKind, string? commandName = null, CancellationToken cancellationToken = default)
{
if (EnsureExtractedException is not null)
{
throw EnsureExtractedException;
}
if (EnsureExtractedAndAcquireLayoutAsyncCallback is not null)
{
await EnsureExtractedAndAcquireLayoutAsyncCallback(cancellationToken);
}
return Layout is null ? null : new BundleLayoutLease(Layout, lease: null);
}
public string? GetDefaultExtractDir(string processPath) => null;
}
internal sealed class TestOutputTextWriter : TextWriter
{
private readonly ITestOutputHelper _outputHelper;
private readonly Action<string>? _onLine;
private readonly StringBuilder _buffer = new();
public List<string> Logs { get; } = new List<string>();
public TestOutputTextWriter(ITestOutputHelper outputHelper) : this(outputHelper, (IFormatProvider?)null)
{
}
public TestOutputTextWriter(ITestOutputHelper outputHelper, Action<string> onLine) : this(outputHelper, null, onLine)
{
}
public TestOutputTextWriter(ITestOutputHelper outputHelper, IFormatProvider? formatProvider) : this(outputHelper, formatProvider, null)
{
}
private TestOutputTextWriter(ITestOutputHelper outputHelper, IFormatProvider? formatProvider, Action<string>? onLine) : base(formatProvider)
{
_outputHelper = outputHelper;
_onLine = onLine;
}
public override Encoding Encoding => Encoding.UTF8;
public override void WriteLine(string? message)
{
_buffer.Append(message);
FlushLine();
}
public override void Write(string? message)
{
if (message is null)
{
return;
}
// Spectre.Console writes content and newlines via Write() calls.
// Split on newline boundaries so each logical line ends up as one Logs entry.
var remaining = message.AsSpan();
while (remaining.Length > 0)
{
var nlIndex = remaining.IndexOf('\n');
if (nlIndex < 0)
{
_buffer.Append(remaining);
break;
}
// Append everything before the newline (excluding any \r before \n)
var lineEnd = nlIndex > 0 && remaining[nlIndex - 1] == '\r' ? nlIndex - 1 : nlIndex;
_buffer.Append(remaining[..lineEnd]);
FlushLine();
remaining = remaining[(nlIndex + 1)..];
}
}
public override void Flush()
{
if (_buffer.Length > 0)
{
FlushLine();
}
base.Flush();
}
private void FlushLine()
{
var line = _buffer.ToString();
_buffer.Clear();
_outputHelper.WriteLine(line);
Logs.Add(line);
_onLine?.Invoke(line);
}
}