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.
69 lines
4.0 KiB
C#
69 lines
4.0 KiB
C#
using Volcanic.AgenticCore.Extensions.Scheduler;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Tests.Scheduler;
|
|
|
|
public sealed class JobValidationTests
|
|
{
|
|
private static readonly DateTimeOffset Now = new(2026, 9, 26, 12, 3, 0, TimeSpan.Zero); // 14:03 Berlin
|
|
private static readonly TimeZoneInfo Berlin = ScheduleTests.Berlin;
|
|
|
|
private static string Error(NewJob job) =>
|
|
Assert.Throws<JobValidationException>(() => JobValidation.ValidateNew(job, Now, Berlin)).Message;
|
|
|
|
[Fact]
|
|
public void Past_at_is_rejected_beyond_the_grace()
|
|
{
|
|
Assert.Equal("at 2026-09-26 13:00 (Europe/Berlin) is in the past; now is 2026-09-26 14:03. Pick a future time.",
|
|
Error(new NewJob("x", JobKind.Prompt, At: "2026-09-26T13:00", Text: "hi")));
|
|
var schedule = JobValidation.ValidateNew(new NewJob("x", JobKind.Prompt, At: "2026-09-26T12:02:30Z", Text: "hi"), Now, Berlin);
|
|
Assert.Equal(new DateTimeOffset(2026, 9, 26, 12, 2, 30, TimeSpan.Zero), schedule.At); // within 60 s of grace
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null, null)]
|
|
[InlineData("2026-09-27T09:00", "0 9 * * *")]
|
|
public void Exactly_one_of_at_and_cron(string? at, string? cron) =>
|
|
Assert.Equal("Set exactly one of at (a one-off time) or cron (a recurring schedule).",
|
|
Error(new NewJob("x", JobKind.Prompt, at, cron, Text: "hi")));
|
|
|
|
[Fact]
|
|
public void Kind_specific_fields()
|
|
{
|
|
const string cron = "0 9 * * *";
|
|
Assert.Equal("A prompt job needs text (what to ask the agent).", Error(new NewJob("x", JobKind.Prompt, Cron: cron)));
|
|
Assert.Equal("command is only for watchers.", Error(new NewJob("x", JobKind.Prompt, Cron: cron, Text: "t", Command: "ls")));
|
|
Assert.Equal("A watcher needs command (the shell command to run).", Error(new NewJob("x", JobKind.Watcher, Cron: cron)));
|
|
Assert.Equal("A watcher takes no text: its script's output becomes the prompt.", Error(new NewJob("x", JobKind.Watcher, Cron: cron, Text: "t", Command: "ls")));
|
|
Assert.Equal("timeoutSeconds is only for watchers.", Error(new NewJob("x", JobKind.Prompt, Cron: cron, Text: "t", TimeoutSeconds: 10)));
|
|
Assert.Equal("timeoutSeconds must be between 1 and 3600.", Error(new NewJob("x", JobKind.Watcher, Cron: cron, Command: "ls", TimeoutSeconds: 0)));
|
|
Assert.Equal("timeoutSeconds must be between 1 and 3600.", Error(new NewJob("x", JobKind.Watcher, Cron: cron, Command: "ls", TimeoutSeconds: 3601)));
|
|
Assert.Equal(new JobSchedule(null, cron), JobValidation.ValidateNew(new NewJob("x", JobKind.Watcher, Cron: cron, Command: "ls", TimeoutSeconds: 3600), Now, Berlin));
|
|
}
|
|
|
|
[Fact]
|
|
public void Name_is_required_and_capped()
|
|
{
|
|
Assert.Equal("name is required (a short label such as \"Call the dentist\").", Error(new NewJob(" ", JobKind.Prompt, Cron: "@daily", Text: "t")));
|
|
Assert.Equal("name is 81 characters; the limit is 80.", Error(new NewJob(new string('n', 81), JobKind.Prompt, Cron: "@daily", Text: "t")));
|
|
}
|
|
|
|
[Fact]
|
|
public void Text_is_capped_at_4000() =>
|
|
Assert.Equal("text is 4001 characters; the limit is 4000.", Error(new NewJob("x", JobKind.Prompt, Cron: "@daily", Text: new string('t', 4001))));
|
|
|
|
[Fact]
|
|
public void Stored_jobs_are_checked_without_the_future_rule()
|
|
{
|
|
var job = new ScheduledJob
|
|
{
|
|
Id = "k7m2q", Target = "111", Name = "Old", Kind = JobKind.Prompt, Text = "hi",
|
|
Schedule = new JobSchedule(Now.AddDays(-3), null), CreatedAt = Now.AddDays(-4),
|
|
};
|
|
Assert.Null(JobValidation.CheckStored(job)); // an overdue one-off is fine on load: it fires late
|
|
Assert.NotNull(JobValidation.CheckStored(job with { Schedule = new JobSchedule(Now, "0 9 * * *") }));
|
|
Assert.NotNull(JobValidation.CheckStored(job with { Schedule = new JobSchedule(null, "0 0 9 * * *") }));
|
|
Assert.NotNull(JobValidation.CheckStored(job with { Kind = JobKind.Watcher }));
|
|
Assert.NotNull(JobValidation.CheckStored(job with { Id = "" }));
|
|
}
|
|
}
|