forked from admin/VolcanicAgenticCore
Jobs are prompts or watchers. A reminder is a prompt the agent passes on, so
it lands in the transcript. Stored reminder jobs load as prompts
("Remind me: <text>"). Also records the Coolify deploy target in CLAUDE.md.
114 lines
6.0 KiB
C#
114 lines
6.0 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Volcanic.AgenticCore.Abstractions.Json;
|
|
using Volcanic.AgenticCore.Abstractions.Messages;
|
|
using Volcanic.AgenticCore.Abstractions.Tools;
|
|
using Volcanic.AgenticCore.Extensions.Scheduler;
|
|
using Volcanic.AgenticCore.Extensions.Tests.Scheduler.Support;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Tests.Scheduler;
|
|
|
|
public sealed class ScheduleToolTests
|
|
{
|
|
private static async Task<SchedulerHarness> StartedAsync()
|
|
{
|
|
var h = new SchedulerHarness(); // Sat 2026-09-26 12:03Z
|
|
h.Delivery.Zones["111"] = SchedulerHarness.Berlin;
|
|
h.Delivery.Zones["222"] = SchedulerHarness.Tokyo;
|
|
await h.StartAsync();
|
|
return h;
|
|
}
|
|
|
|
private static Task<ToolResult> Run(SchedulerHarness h, string target, object args) =>
|
|
new ScheduleTool(h.Scheduler, target).ExecuteAsync(
|
|
new ToolCallContent("call-1", ScheduleTool.ToolName, VolcanicJson.ToElement(args)),
|
|
new ToolInvocation("call-1", new SchedulerStubContext(), null), CancellationToken.None);
|
|
|
|
[Fact]
|
|
public async Task Create_list_and_cancel_through_the_tool()
|
|
{
|
|
await using var h = await StartedAsync();
|
|
|
|
var created = await Run(h, "111", new { action = "create", name = "Call the dentist", kind = "prompt", at = "2026-09-27T09:00", text = "Call the dentist" });
|
|
var id = Assert.Single(h.Scheduler.List("111")).Job.Id;
|
|
Assert.Equal(
|
|
$"Created: `{id}` 💬 Call the dentist · once 27 Sep 09:00\nNext run: Sun 2026-09-27 09:00 (Europe/Berlin)\nNow: Sat 2026-09-26 14:03 (Europe/Berlin)",
|
|
created.TextContent);
|
|
|
|
var listed = await Run(h, "111", new { action = "list" });
|
|
Assert.Equal($"`{id}` 💬 Call the dentist · once 27 Sep 09:00\nNow: Sat 2026-09-26 14:03 (Europe/Berlin)", listed.TextContent);
|
|
|
|
var cancelled = await Run(h, "111", new { action = "cancel", id });
|
|
Assert.Equal($"Cancelled {id} (Call the dentist).", cancelled.TextContent);
|
|
Assert.Equal("No scheduled jobs.\nNow: Sat 2026-09-26 14:03 (Europe/Berlin)", (await Run(h, "111", new { action = "list" })).TextContent);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("""{"action":"create","name":"x","kind":"prompt","cron":"0 0 9 * * *","text":"t"}""", "seconds aren't supported")]
|
|
[InlineData("""{"action":"create","name":"x","kind":"prompt","at":"2026-09-26T13:00","text":"t"}""", "is in the past")]
|
|
[InlineData("""{"action":"create","name":"x","kind":"prompt","cron":"@daily"}""", "A prompt job needs text")]
|
|
[InlineData("""{"action":"create","name":"x","kind":"reminder","cron":"@daily","text":"t"}""", "kind must be prompt or watcher.")]
|
|
[InlineData("""{"action":"create","name":"x","kind":"alarm","cron":"@daily","text":"t"}""", "kind must be prompt or watcher.")]
|
|
[InlineData("""{"action":"create","name":"x","cron":"@daily","text":"t"}""", "kind must be prompt or watcher.")]
|
|
[InlineData("""{"action":"snooze"}""", "action must be create, list or cancel.")]
|
|
[InlineData("""{"action":"cancel"}""", "cancel needs id (see action=list).")]
|
|
[InlineData("""{"action":"cancel","id":"zzzzz"}""", "No job zzzzz in this chat.")]
|
|
public async Task Validation_errors_become_error_results(string json, string fragment)
|
|
{
|
|
await using var h = await StartedAsync();
|
|
var tool = new ScheduleTool(h.Scheduler, "111");
|
|
|
|
var ex = await Assert.ThrowsAsync<ToolExecutionException>(() => tool.ExecuteAsync(
|
|
new ToolCallContent("c", ScheduleTool.ToolName, VolcanicJson.Parse(json)), new ToolInvocation("c", new SchedulerStubContext(), null), CancellationToken.None));
|
|
|
|
Assert.Contains(fragment, ex.Message);
|
|
Assert.Empty(h.Scheduler.List("111"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task List_and_cancel_never_see_another_targets_jobs()
|
|
{
|
|
await using var h = await StartedAsync();
|
|
await Run(h, "222", new { action = "create", name = "Theirs", kind = "prompt", cron = "@daily", text = "t" });
|
|
var theirs = Assert.Single(h.Scheduler.List("222")).Job.Id;
|
|
|
|
Assert.StartsWith("No scheduled jobs.", (await Run(h, "111", new { action = "list" })).TextContent);
|
|
var ex = await Assert.ThrowsAsync<ToolExecutionException>(() => Run(h, "111", new { action = "cancel", id = theirs }));
|
|
Assert.Equal($"No job {theirs} in this chat.", ex.Message);
|
|
Assert.Single(h.Scheduler.List("222"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_now_line_uses_the_targets_zone()
|
|
{
|
|
await using var h = await StartedAsync();
|
|
Assert.EndsWith("Now: Sat 2026-09-26 21:03 (Asia/Tokyo)", (await Run(h, "222", new { action = "list" })).TextContent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_offset_less_at_is_read_in_the_targets_zone()
|
|
{
|
|
await using var h = await StartedAsync();
|
|
await Run(h, "222", new { action = "create", name = "Tokyo", kind = "prompt", at = "2026-09-27T09:00", text = "Good morning" });
|
|
Assert.Equal(new DateTimeOffset(2026, 9, 27, 0, 0, 0, TimeSpan.Zero), Assert.Single(h.Scheduler.List("222")).Job.Schedule.At);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Snippet_and_guidelines_match_the_spec()
|
|
{
|
|
await using var h = await StartedAsync();
|
|
var tool = new ScheduleTool(h.Scheduler, "111");
|
|
Assert.Equal("schedule", tool.Name);
|
|
Assert.Equal("schedule: one-off and recurring prompts and watcher scripts that message this chat", tool.PromptSnippet);
|
|
Assert.Equal(4, tool.PromptGuidelines.Count);
|
|
Assert.DoesNotContain(tool.PromptGuidelines, g => g.Contains("Europe/", StringComparison.Ordinal)); // zone-free, so the prompt text is stable
|
|
Assert.Contains(tool.PromptGuidelines, g => g.Contains("scripts/", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Discovery_without_a_host_scheduler_cannot_construct_the_extension()
|
|
{
|
|
using var provider = new ServiceCollection().BuildServiceProvider();
|
|
Assert.Throws<InvalidOperationException>(() => ActivatorUtilities.CreateInstance(provider, typeof(SchedulerExtension)));
|
|
}
|
|
}
|