Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/Elastic.Codex/Building/CodexBuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using System.IO.Abstractions;
using System.Text.Json;
using Elastic.Codex.Navigation;
Expand Down Expand Up @@ -195,21 +196,24 @@ public async Task<CodexBuildResult> BuildAll(
};

ICrossLinkResolver crossLinkResolver;
var codexRepos = new HashSet<string> { repoName };

if (buildContext.Configuration.CrossLinkEntries.Length > 0)
{
var fetcher = new DocSetConfigurationCrossLinkFetcher(
logFactory,
buildContext.Configuration,
codexLinkIndexReader: buildContext.Configuration.Registry != DocSetRegistry.Public ? codexLinkIndexReader : null);
var crossLinks = await fetcher.FetchCrossLinks(ctx);
IUriEnvironmentResolver? uriResolver = crossLinks.CodexRepositories is not null
? new CodexAwareUriResolver(crossLinks.CodexRepositories, useRelativePaths: true)
: null;
if (crossLinks.CodexRepositories is not null)
codexRepos.UnionWith(crossLinks.CodexRepositories);
var uriResolver = new CodexAwareUriResolver(codexRepos.ToFrozenSet(), useRelativePaths: true);
crossLinkResolver = new CrossLinkResolver(crossLinks, uriResolver);
}
else
{
crossLinkResolver = NoopCrossLinkResolver.Instance;
var uriResolver = new CodexAwareUriResolver(codexRepos.ToFrozenSet(), useRelativePaths: true);
crossLinkResolver = new CrossLinkResolver(FetchedCrossLinks.Empty, uriResolver);
}

// Create documentation set
Expand Down Expand Up @@ -295,7 +299,9 @@ string Resolve(string path)
CrossLinkResolver.ToTargetUrlPath(path));
}

return uri?.AbsolutePath ?? string.Empty;
return uri is null
? string.Empty
: uri.IsAbsoluteUri ? uri.AbsolutePath : uri.OriginalString;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@

namespace Elastic.Markdown.Tests.CrossLinks;

/// <summary>Mirrors the path extraction logic in CodexBuildService.CollectRedirects.</summary>
internal static class RedirectPathExtractor
{
public static string GetPath(Uri? uri) =>
uri is null
? string.Empty
: uri.IsAbsoluteUri ? uri.AbsolutePath : uri.OriginalString;
}

public class CodexAwareUriResolverTests
{
private static readonly FrozenSet<string> CodexRepos =
Expand Down Expand Up @@ -85,6 +94,34 @@ public void DefaultMode_IsAbsolute()
result.IsAbsoluteUri.Should().BeTrue();
result.ToString().Should().Be("https://codex.elastic.dev/r/observability-robots/page");
}

[Fact]
public void SameRepoRedirect_CurrentRepoInCodexSet_ProducesCodexPath()
{
var codexRepos = new HashSet<string> { "ai-guild" }.ToFrozenSet();
var resolver = new CodexAwareUriResolver(codexRepos, useRelativePaths: true);
var crossLinkUri = new Uri("ai-guild://best-practices/tools", UriKind.Absolute);
var targetPath = CrossLinkResolver.ToTargetUrlPath("best-practices/tools");

var result = resolver.Resolve(crossLinkUri, targetPath);

result.IsAbsoluteUri.Should().BeFalse();
result.ToString().Should().Be("/r/ai-guild/best-practices/tools");
}

[Fact]
public void SameRepoRedirect_WithIndexNormalization_StripsTrailingIndex()
{
var codexRepos = new HashSet<string> { "ai-guild" }.ToFrozenSet();
var resolver = new CodexAwareUriResolver(codexRepos, useRelativePaths: true);
var crossLinkUri = new Uri("ai-guild://best-practices/tools/index.md", UriKind.Absolute);
var targetPath = CrossLinkResolver.ToTargetUrlPath("best-practices/tools/index.md");

var result = resolver.Resolve(crossLinkUri, targetPath);

result.IsAbsoluteUri.Should().BeFalse();
result.ToString().Should().Be("/r/ai-guild/best-practices/tools");
}
}

public class IsolatedBuildEnvironmentUriResolverTests
Expand Down Expand Up @@ -123,3 +160,66 @@ public void NonCloudRepo_UsesMainBranch()
result.ToString().Should().Contain("/tree/main/");
}
}

public class CodexRedirectPathExtractionTests
{
[Fact]
public void RelativeUri_FromCodexAwareResolver_ExtractsPathCorrectly()
{
var codexRepos = new HashSet<string> { "ai-guild" }.ToFrozenSet();
var resolver = new CodexAwareUriResolver(codexRepos, useRelativePaths: true);
var uri = resolver.Resolve(new Uri("ai-guild://tools", UriKind.Absolute), "tools");

var path = RedirectPathExtractor.GetPath(uri);

path.Should().Be("/r/ai-guild/tools");
}

[Fact]
public void AbsoluteUri_FromCodexAwareResolver_ExtractsPathCorrectly()
{
var codexRepos = new HashSet<string> { "ai-guild" }.ToFrozenSet();
var resolver = new CodexAwareUriResolver(codexRepos, useRelativePaths: false);
var uri = resolver.Resolve(new Uri("ai-guild://tools", UriKind.Absolute), "tools");

var path = RedirectPathExtractor.GetPath(uri);

path.Should().Be("/r/ai-guild/tools");
}

[Fact]
public void AbsoluteUri_FromIsolatedBuildResolver_ExtractsPathCorrectly()
{
var resolver = new IsolatedBuildEnvironmentUriResolver();
var uri = resolver.Resolve(new Uri("docs-content://get-started", UriKind.Absolute), "get-started");

var path = RedirectPathExtractor.GetPath(uri);

path.Should().Be("/elastic/docs-content/tree/main/get-started");
}

[Fact]
public void NullUri_ReturnsEmptyString()
{
var path = RedirectPathExtractor.GetPath(null);

path.Should().BeEmpty();
}
}

public class CodexCrossRepoRedirectTests
{
[Fact]
public void CrossRepoRedirect_TargetInCodexRepo_ResolvesToCodexPath()
{
var resolver = new Elastic.Markdown.Tests.TestCodexCrossLinkResolver(useRelativePaths: true);
var crossRepoUri = new Uri("kibana://get-started/index.md", UriKind.Absolute);

var success = resolver.TryResolve(_ => { }, crossRepoUri, out var resolvedUri);

success.Should().BeTrue();
resolvedUri.Should().NotBeNull();
resolvedUri!.IsAbsoluteUri.Should().BeFalse();
resolvedUri.ToString().Should().Be("/r/kibana/get-started");
}
}
Loading