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
6 changes: 5 additions & 1 deletion src/RockBot.Tools.Web/Brave/BraveSearchProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ public async Task<IReadOnlyList<WebSearchResult>> SearchAsync(string query, int
if (string.IsNullOrEmpty(apiKey))
{
logger.LogWarning("Brave API key not set. Configure WebTools:ApiKey in user secrets or set the {EnvVar} environment variable", options.ApiKeyEnvVar);
return [];
// Signal a configuration gap rather than returning an empty result set, which the
// caller can't distinguish from a search that genuinely found nothing.
throw new WebSearchNotConfiguredException(
$"Web search is unavailable because the Brave Search API key is not configured. " +
$"Set WebTools:ApiKey (config / user secrets) or the {options.ApiKeyEnvVar} environment variable.");
}

using var client = httpClientFactory.CreateClient("RockBot.Tools.Web.Brave");
Expand Down
8 changes: 8 additions & 0 deletions src/RockBot.Tools.Web/WebSearchNotConfiguredException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace RockBot.Tools.Web;

/// <summary>
/// Thrown by an <see cref="IWebSearchProvider"/> when web search cannot run because its API
/// credentials are not configured. Lets callers distinguish a configuration gap — which should
/// be surfaced to the user as an actionable error — from a search that ran and found nothing.
/// </summary>
public sealed class WebSearchNotConfiguredException(string message) : InvalidOperationException(message);
7 changes: 7 additions & 0 deletions src/RockBot.Tools.Web/WebSearchToolExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ public async Task<ToolInvokeResponse> ExecuteAsync(ToolInvokeRequest request, Ca
IsError = false
};
}
catch (WebSearchNotConfiguredException ex)
{
// Configuration gap (e.g. missing API key). Surface the actionable message
// directly so the agent can tell the user web search isn't set up, rather than
// reporting a generic search failure or a misleading "no results found".
return Error(request, ex.Message);
}
catch (Exception ex)
{
return Error(request, $"Search failed: {ex.Message}");
Expand Down
10 changes: 6 additions & 4 deletions tests/RockBot.Tools.Web.Tests/BraveSearchProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task SearchAsync_DeserializesResultsCorrectly()
}

[TestMethod]
public async Task SearchAsync_ReturnsEmpty_WhenApiKeyMissing()
public async Task SearchAsync_Throws_WhenApiKeyMissing()
{
var handler = new CaptureHandler(new HttpResponseMessage(HttpStatusCode.OK)
{
Expand All @@ -91,9 +91,11 @@ public async Task SearchAsync_ReturnsEmpty_WhenApiKeyMissing()
var options = new WebToolOptions { ApiKeyEnvVar = "ROCKBOT_TEST_BRAVE_KEY_NONEXISTENT_12345" };
var provider = new BraveSearchProvider(new StubFactory(handler), options, NullLogger<BraveSearchProvider>.Instance);

var results = await provider.SearchAsync("test", 10, CancellationToken.None);

Assert.AreEqual(0, results.Count);
// A missing key is a configuration gap, not a search that returned nothing — the
// provider throws so the caller can surface an actionable error to the user.
var ex = await Assert.ThrowsExactlyAsync<WebSearchNotConfiguredException>(
() => provider.SearchAsync("test", 10, CancellationToken.None));
StringAssert.Contains(ex.Message, "not configured");
}

[TestMethod]
Expand Down
16 changes: 16 additions & 0 deletions tests/RockBot.Tools.Web.Tests/WebSearchToolExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ public async Task ExecuteAsync_ReturnsError_WhenProviderThrows()
StringAssert.Contains(response.Content, "Search failed");
}

[TestMethod]
public async Task ExecuteAsync_ReturnsError_WhenSearchNotConfigured()
{
var executor = new WebSearchToolExecutor(
new ThrowingSearchProvider(new WebSearchNotConfiguredException(
"Web search is unavailable because the Brave Search API key is not configured.")),
new WebToolOptions());

var response = await executor.ExecuteAsync(MakeRequest("""{"query": "test"}"""), CancellationToken.None);

Assert.IsTrue(response.IsError);
StringAssert.Contains(response.Content, "not configured");
// Distinct from the generic "Search failed" path so the agent can act on it.
StringAssert.Contains(response.Content, "Brave Search API key");
}

[TestMethod]
public async Task ExecuteAsync_ReturnsError_WhenQueryMissing()
{
Expand Down
Loading