diff --git a/src/Elastic.Codex/Elastic.Codex.csproj b/src/Elastic.Codex/Elastic.Codex.csproj
index bb2e313e3..f6fe523b0 100644
--- a/src/Elastic.Codex/Elastic.Codex.csproj
+++ b/src/Elastic.Codex/Elastic.Codex.csproj
@@ -12,6 +12,10 @@
true
+
+
+
+
diff --git a/src/Elastic.Codex/Sourcing/CodexCloneService.cs b/src/Elastic.Codex/Sourcing/CodexCloneService.cs
index 531b0fd20..b39c1581d 100644
--- a/src/Elastic.Codex/Sourcing/CodexCloneService.cs
+++ b/src/Elastic.Codex/Sourcing/CodexCloneService.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
+using System.Security;
using Elastic.Documentation.Configuration.Codex;
using Elastic.Documentation.Configuration.Toc;
using Elastic.Documentation.Diagnostics;
@@ -268,9 +269,9 @@ await context.WriteFileSystem.File.WriteAllTextAsync(
return found;
}
}
- catch (UnauthorizedAccessException)
+ catch (Exception ex) when (ex is UnauthorizedAccessException or SecurityException)
{
- // Skip directories we can't access
+ // Skip directories we can't access (including ScopedFileSystem-blocked hidden dirs)
}
return null;
diff --git a/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs
new file mode 100644
index 000000000..91961955f
--- /dev/null
+++ b/tests/Navigation.Tests/Codex/FindDocsetFileTests.cs
@@ -0,0 +1,89 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// 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.IO.Abstractions.TestingHelpers;
+using AwesomeAssertions;
+using Elastic.Codex.Sourcing;
+using Elastic.Documentation.Configuration;
+using Nullean.ScopedFileSystem;
+
+namespace Elastic.Documentation.Navigation.Tests.Codex;
+
+public class FindDocsetFileTests
+{
+ private static readonly string RepoRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, "repo");
+
+ private static ScopedFileSystem CreateScopedFs(MockFileSystem mockFs) =>
+ FileSystemFactory.ScopeCurrentWorkingDirectory(mockFs);
+
+ [Fact]
+ public void StandardPath_Found()
+ {
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(RepoRoot, "docs/docset.yml"), new MockFileData("project: test") }
+ });
+
+ var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot));
+
+ result.Should().NotBeNull();
+ result.Name.Should().Be("docset.yml");
+ }
+
+ [Fact]
+ public void NonStandardPath_FoundViaRecursion()
+ {
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(RepoRoot, "docs-codex/docset.yml"), new MockFileData("project: test") }
+ });
+
+ var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot));
+
+ result.Should().NotBeNull();
+ result.Name.Should().Be("docset.yml");
+ }
+
+ [Fact]
+ public void HiddenDirectory_SkippedByScopedFileSystem()
+ {
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(RepoRoot, ".github/workflows/ci.yml"), new MockFileData("name: CI") },
+ { Path.Join(RepoRoot, "docs-codex/docset.yml"), new MockFileData("project: test") }
+ });
+ var scopedFs = CreateScopedFs(mockFs);
+
+ var result = CodexCloneService.FindDocsetFile(scopedFs, scopedFs.DirectoryInfo.New(RepoRoot));
+
+ result.Should().NotBeNull();
+ result.Name.Should().Be("docset.yml");
+ }
+
+ [Fact]
+ public void NoDocset_ReturnsNull()
+ {
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(RepoRoot, "src/main.py"), new MockFileData("print('hello')") }
+ });
+
+ var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot));
+
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void NodeModules_Skipped()
+ {
+ var mockFs = new MockFileSystem(new Dictionary
+ {
+ { Path.Join(RepoRoot, "node_modules/some-pkg/docset.yml"), new MockFileData("project: fake") }
+ });
+
+ var result = CodexCloneService.FindDocsetFile(mockFs, mockFs.DirectoryInfo.New(RepoRoot));
+
+ result.Should().BeNull();
+ }
+}