Files
Playa/tests/Volcanic.AgenticCore.Extensions.Tests/Scheduler/JobValidationTests.cs
EugeneTes 8014a0e770 refactor(scheduler): drop the reminder job kind
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.
2026-09-29 09:30:40 +00:00

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 = "" }));
}
}