forked from admin/VolcanicAgenticCore
feat(telegram): trigger options and Jev client for OpenRouter systemone
This commit is contained in:
143
tests/Volcanic.AgenticCore.Telegram.Tests/JevClientTests.cs
Normal file
143
tests/Volcanic.AgenticCore.Telegram.Tests/JevClientTests.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Volcanic.AgenticCore.Features.Processes;
|
||||
using Volcanic.AgenticCore.Telegram.Triggers;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram.Tests;
|
||||
|
||||
public sealed class JevClientTests
|
||||
{
|
||||
private const string Key = "sk-or-test-secret-123";
|
||||
|
||||
private sealed class ScriptedHandler(params Func<HttpResponseMessage>[] responses) : HttpMessageHandler
|
||||
{
|
||||
public List<(HttpRequestMessage Request, string Body)> Calls { get; } = [];
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls.Add((request, await request.Content!.ReadAsStringAsync(cancellationToken)));
|
||||
return responses[Math.Min(Calls.Count - 1, responses.Length - 1)]();
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpResponseMessage Json(HttpStatusCode status, string body) => new(status) { Content = new StringContent(body, Encoding.UTF8, "application/json") };
|
||||
|
||||
private static Func<HttpResponseMessage> Ok(string answers) => () => Json(HttpStatusCode.OK, $"{{\"model\":\"typesafe/jev-1.13-20260917\",\"answers\":{answers}}}");
|
||||
|
||||
private static (JevClient Client, ScriptedHandler Handler) Create(params Func<HttpResponseMessage>[] responses)
|
||||
{
|
||||
var handler = new ScriptedHandler(responses);
|
||||
var options = new TriggerOptions { ApiKey = Key, BaseUrl = "https://or.example/api/v1/", RetryDelay = TimeSpan.Zero };
|
||||
return (new JevClient(new HttpClient(handler), options), handler);
|
||||
}
|
||||
|
||||
private static readonly JevQuestion Deploys = new("q0", "Is this about deploys?", "Mentions deploys", "Small talk");
|
||||
private static readonly JevQuestion Lunch = new("q1", "Is this about lunch?");
|
||||
|
||||
[Fact]
|
||||
public async Task Posts_state_model_and_noul_questions_and_reads_the_answers()
|
||||
{
|
||||
var (client, handler) = Create(Ok("{\"q0\":{\"type\":\"noul\",\"noul\":0.93},\"q1\":{\"type\":\"noul\",\"noul\":0.01}}"));
|
||||
|
||||
var answers = await client.AskAsync(new JsonObject { ["text"] = "prod is down" }, [Deploys, Lunch], CancellationToken.None);
|
||||
|
||||
Assert.Equal(0.93, answers["q0"]);
|
||||
Assert.Equal(0.01, answers["q1"]);
|
||||
var (request, body) = Assert.Single(handler.Calls);
|
||||
Assert.Equal("https://or.example/api/v1/systemone", request.RequestUri!.ToString());
|
||||
Assert.Equal(("Bearer", Key), (request.Headers.Authorization!.Scheme, request.Headers.Authorization.Parameter));
|
||||
var json = JsonNode.Parse(body)!;
|
||||
Assert.Equal("prod is down", (string?)json["state"]!["text"]);
|
||||
Assert.Equal(TriggerOptions.DefaultModel, (string?)json["model"]);
|
||||
Assert.Equal("noul", (string?)json["questions"]!["q0"]!["type"]);
|
||||
Assert.Equal("Mentions deploys", (string?)json["questions"]!["q0"]!["criteria"]!["true"]);
|
||||
Assert.Equal("Small talk", (string?)json["questions"]!["q0"]!["criteria"]!["false"]);
|
||||
Assert.Null(json["questions"]!["q1"]!["criteria"]); // only when both sides are set
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_questions_makes_no_call()
|
||||
{
|
||||
var (client, handler) = Create(Ok("{}"));
|
||||
Assert.Empty(await client.AskAsync(JsonValue.Create("x"), [], CancellationToken.None));
|
||||
Assert.Empty(handler.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Transient_failures_are_retried_three_times_then_fail_with_four_attempts()
|
||||
{
|
||||
var (client, handler) = Create(() => Json(HttpStatusCode.ServiceUnavailable, "{\"error\":\"busy\"}"));
|
||||
|
||||
var ex = await Assert.ThrowsAsync<JevException>(() => client.AskAsync(JsonValue.Create("x"), [Lunch], CancellationToken.None));
|
||||
|
||||
Assert.Equal(4, handler.Calls.Count);
|
||||
Assert.Equal(4, ex.Attempts);
|
||||
Assert.Contains("HTTP 503", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_retry_that_succeeds_returns_the_answers()
|
||||
{
|
||||
var (client, handler) = Create(
|
||||
() => throw new HttpRequestException("connection reset"),
|
||||
() => { var r = Json(HttpStatusCode.TooManyRequests, "slow down"); r.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.Zero); return r; },
|
||||
Ok("{\"q1\":{\"type\":\"noul\",\"noul\":0.5}}"));
|
||||
|
||||
var answers = await client.AskAsync(JsonValue.Create("x"), [Lunch], CancellationToken.None);
|
||||
|
||||
Assert.Equal(0.5, answers["q1"]);
|
||||
Assert.Equal(3, handler.Calls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_client_error_is_not_retried_and_never_shows_the_key()
|
||||
{
|
||||
var (client, handler) = Create(() => Json(HttpStatusCode.BadRequest, $"{{\"error\":\"bad key {Key}\"}}"));
|
||||
|
||||
var ex = await Assert.ThrowsAsync<JevException>(() => client.AskAsync(JsonValue.Create("x"), [Lunch], CancellationToken.None));
|
||||
|
||||
Assert.Single(handler.Calls);
|
||||
Assert.Equal(1, ex.Attempts);
|
||||
Assert.DoesNotContain(Key, ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("[REDACTED]", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_missing_answer_is_a_failure()
|
||||
{
|
||||
var (client, _) = Create(Ok("{\"q0\":{\"type\":\"noul\",\"noul\":0.9}}"));
|
||||
var ex = await Assert.ThrowsAsync<JevException>(() => client.AskAsync(JsonValue.Create("x"), [Deploys, Lunch], CancellationToken.None));
|
||||
Assert.Contains("q1", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Options_read_the_environment_only_when_empty_and_validate()
|
||||
{
|
||||
var env = new Dictionary<string, string> { ["OPENROUTER_API_KEY"] = " key ", ["TRIGGER_MODEL"] = "typesafe/jev-2" };
|
||||
var options = new TelegramOptions { BotToken = "t", AllowedUserIds = [1] };
|
||||
options.ApplyEnvironment(name => env.GetValueOrDefault(name));
|
||||
Assert.True(options.Triggers.IsEnabled);
|
||||
Assert.Equal("key", options.Triggers.ApiKey);
|
||||
Assert.Equal("typesafe/jev-2", options.Triggers.ResolvedModel);
|
||||
|
||||
var configured = new TelegramOptions { Triggers = new TriggerOptions { ApiKey = "mine" } };
|
||||
configured.ApplyEnvironment(name => env.GetValueOrDefault(name));
|
||||
Assert.Equal("mine", configured.Triggers.ApiKey);
|
||||
|
||||
var bad = new TelegramOptions { BotToken = "t", AllowedUserIds = [1], Triggers = new TriggerOptions { BaseUrl = "/v1", DefaultThreshold = 1.5, MaxConcurrency = 0 } };
|
||||
var errors = bad.Validate();
|
||||
Assert.Contains(errors, e => e.Contains("Triggers:BaseUrl", StringComparison.Ordinal));
|
||||
Assert.Contains(errors, e => e.Contains("DefaultThreshold", StringComparison.Ordinal));
|
||||
Assert.Contains(errors, e => e.Contains("MaxConcurrency", StringComparison.Ordinal));
|
||||
Assert.Empty(new TelegramOptions { BotToken = "t", AllowedUserIds = [1] }.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_key_is_a_secret_value_for_agent_processes()
|
||||
{
|
||||
var target = new ProcessEnvironmentOptions();
|
||||
TelegramProcessEnvironment.Configure(target, new TelegramOptions { Triggers = new TriggerOptions { ApiKey = Key } }, new ProcessEnvironmentSection(), _ => null);
|
||||
Assert.Contains(Key, target.SecretValues);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user