File: Mcp\Docs\DocsCacheTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using Aspire.Cli.Documentation.Docs;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Documentation.Docs;
 
public class DocsCacheTests(ITestOutputHelper outputHelper)
{
    private const string DefaultLlmsTxtUrl = DocsSourceConfiguration.DefaultLlmsTxtUrl;
 
    [Fact]
    public async Task FetchDocsAsync_PersistsFriendlyLlmsCacheFileName()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var memoryCache = new MemoryCache(new MemoryCacheOptions());
        var configuration = new ConfigurationBuilder().Build();
        var cache = CreateCache(workspace, memoryCache);
 
        using var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("# Content")
        };
 
        using var handler = new MockHttpMessageHandler(response);
        using var httpClient = new HttpClient(handler);
        var fetcher = new DocsFetcher(httpClient, cache, configuration, NullLogger<DocsFetcher>.Instance);
 
        _ = await fetcher.FetchDocsAsync().DefaultTimeout();
 
        var cacheFiles = Directory.GetFiles(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "cache", "docs"))
            .Select(Path.GetFileName)
            .ToArray();
 
        var expectedStem = DocsSourceConfiguration.GetContentCacheKey(DocsSourceConfiguration.DefaultLlmsTxtUrl);
        Assert.Contains($"{expectedStem}.txt", cacheFiles);
        Assert.DoesNotContain($"https___aspire.dev_{expectedStem}.txt.txt", cacheFiles);
    }
 
    [Fact]
    public async Task GetIndexAsync_ClearsLegacyUrlBackedDocsFiles_WhenModernIndexExists()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var memoryCache = new MemoryCache(new MemoryCacheOptions());
        var cache = CreateCache(workspace, memoryCache);
        var contentCache = CreateContentCache(workspace, memoryCache);
 
        await cache.SetAsync(DefaultLlmsTxtUrl, "# Legacy Content").DefaultTimeout();
        await cache.SetETagAsync(DefaultLlmsTxtUrl, "\"legacy-etag\"").DefaultTimeout();
        var legacyIndexKey = DocsSourceConfiguration.GetLegacyIndexCacheKey(DefaultLlmsTxtUrl);
        await contentCache.SetJsonAsync(
            legacyIndexKey,
            [
                new LlmsDocument
                {
                    Title = "Legacy Document",
                    Slug = "legacy-document",
                    Content = "# Legacy Document",
                    Sections = [],
                    Summary = "Legacy summary"
                }
            ],
            Aspire.Cli.JsonSourceGenerationContext.Default.LlmsDocumentArray).DefaultTimeout();
        await contentCache.SetAsync($"{legacyIndexKey}:fingerprint", "legacy-fingerprint").DefaultTimeout();
        await cache.SetIndexAsync(
            [
                new LlmsDocument
                {
                    Title = "Document",
                    Slug = "document",
                    Content = "# Document",
                    Sections = [],
                    Summary = "Summary"
                }
            ]).DefaultTimeout();
        await cache.SetIndexSourceFingerprintAsync("current-fingerprint").DefaultTimeout();
 
        _ = await cache.GetIndexAsync().DefaultTimeout();
        _ = await cache.GetIndexSourceFingerprintAsync().DefaultTimeout();
 
        var cacheFiles = Directory.GetFiles(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "cache", "docs"))
            .Select(Path.GetFileName)
            .ToArray();
 
        var legacyStem = SanitizeForFileName(DefaultLlmsTxtUrl);
        var friendlyStem = DocsSourceConfiguration.GetContentCacheKey(DefaultLlmsTxtUrl);
        Assert.DoesNotContain($"{legacyStem}.txt", cacheFiles);
        Assert.DoesNotContain($"{legacyStem}.etag.txt", cacheFiles);
        Assert.DoesNotContain($"index_{legacyStem}.json", cacheFiles);
        Assert.DoesNotContain($"index_{legacyStem}_fingerprint.txt", cacheFiles);
        Assert.Contains($"index_{friendlyStem}.json", cacheFiles);
        Assert.Contains($"index_{friendlyStem}_fingerprint.txt", cacheFiles);
    }
 
    private static string SanitizeForFileName(string value)
    {
        var result = new char[value.Length];
        for (var i = 0; i < value.Length; i++)
        {
            var c = value[i];
            result[i] = char.IsLetterOrDigit(c) || c is '.' or '-' or '_' ? c : '_';
        }
 
        return new string(result);
    }
 
    private static DocsCache CreateCache(TemporaryWorkspace workspace, IMemoryCache memoryCache)
    {
        var configuration = new ConfigurationBuilder().Build();
        return new DocsCache(memoryCache, CreateExecutionContext(workspace), configuration, NullLogger<DocsCache>.Instance);
    }
 
    private static Aspire.Cli.Documentation.FileBackedDocumentContentCache CreateContentCache(TemporaryWorkspace workspace, IMemoryCache memoryCache)
        => new(memoryCache, CreateExecutionContext(workspace), "docs", NullLogger<DocsCache>.Instance);
 
    private static CliExecutionContext CreateExecutionContext(TemporaryWorkspace workspace)
        => new(
            workspace.WorkspaceRoot,
            new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "hives")),
            new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "cache")),
            new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")),
            new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")),
            "test.log",
            identityChannel: "local");
}