109 lines
3.7 KiB
C#
109 lines
3.7 KiB
C#
using StellaOps.Scheduler.Models;
|
|
|
|
namespace StellaOps.Scheduler.Models.Tests;
|
|
|
|
public sealed class RunStateMachineTests
|
|
{
|
|
[Fact]
|
|
public void EnsureTransition_FromQueuedToRunningSetsStartedAt()
|
|
{
|
|
var run = new Run(
|
|
id: "run-queued",
|
|
tenantId: "tenant-alpha",
|
|
trigger: RunTrigger.Manual,
|
|
state: RunState.Queued,
|
|
stats: RunStats.Empty,
|
|
createdAt: DateTimeOffset.Parse("2025-10-18T03:00:00Z"));
|
|
|
|
var transitionTime = DateTimeOffset.Parse("2025-10-18T03:05:00Z");
|
|
|
|
var updated = RunStateMachine.EnsureTransition(
|
|
run,
|
|
RunState.Running,
|
|
transitionTime,
|
|
mutateStats: builder => builder.SetQueued(1));
|
|
|
|
Assert.Equal(RunState.Running, updated.State);
|
|
Assert.Equal(transitionTime.ToUniversalTime(), updated.StartedAt);
|
|
Assert.Equal(1, updated.Stats.Queued);
|
|
Assert.Null(updated.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureTransition_ToCompletedPopulatesFinishedAt()
|
|
{
|
|
var run = new Run(
|
|
id: "run-running",
|
|
tenantId: "tenant-alpha",
|
|
trigger: RunTrigger.Manual,
|
|
state: RunState.Running,
|
|
stats: RunStats.Empty,
|
|
createdAt: DateTimeOffset.Parse("2025-10-18T03:00:00Z"),
|
|
startedAt: DateTimeOffset.Parse("2025-10-18T03:05:00Z"));
|
|
|
|
var completedAt = DateTimeOffset.Parse("2025-10-18T03:10:00Z");
|
|
|
|
var updated = RunStateMachine.EnsureTransition(
|
|
run,
|
|
RunState.Completed,
|
|
completedAt,
|
|
mutateStats: builder =>
|
|
{
|
|
builder.SetQueued(1);
|
|
builder.SetCompleted(1);
|
|
});
|
|
|
|
Assert.Equal(RunState.Completed, updated.State);
|
|
Assert.Equal(completedAt.ToUniversalTime(), updated.FinishedAt);
|
|
Assert.Equal(1, updated.Stats.Completed);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnsureTransition_ErrorRequiresMessage()
|
|
{
|
|
var run = new Run(
|
|
id: "run-running",
|
|
tenantId: "tenant-alpha",
|
|
trigger: RunTrigger.Manual,
|
|
state: RunState.Running,
|
|
stats: RunStats.Empty,
|
|
createdAt: DateTimeOffset.Parse("2025-10-18T03:00:00Z"),
|
|
startedAt: DateTimeOffset.Parse("2025-10-18T03:05:00Z"));
|
|
|
|
var timestamp = DateTimeOffset.Parse("2025-10-18T03:06:00Z");
|
|
|
|
var ex = Assert.Throws<InvalidOperationException>(
|
|
() => RunStateMachine.EnsureTransition(run, RunState.Error, timestamp));
|
|
|
|
Assert.Contains("requires a non-empty error message", ex.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_ThrowsWhenTerminalWithoutFinishedAt()
|
|
{
|
|
var run = new Run(
|
|
id: "run-bad",
|
|
tenantId: "tenant-alpha",
|
|
trigger: RunTrigger.Manual,
|
|
state: RunState.Completed,
|
|
stats: RunStats.Empty,
|
|
createdAt: DateTimeOffset.Parse("2025-10-18T03:00:00Z"),
|
|
startedAt: DateTimeOffset.Parse("2025-10-18T03:05:00Z"));
|
|
|
|
Assert.Throws<InvalidOperationException>(() => RunStateMachine.Validate(run));
|
|
}
|
|
|
|
[Fact]
|
|
public void RunReasonExtension_NormalizesImpactWindow()
|
|
{
|
|
var reason = new RunReason(manualReason: "delta");
|
|
var from = DateTimeOffset.Parse("2025-10-18T01:00:00+02:00");
|
|
var to = DateTimeOffset.Parse("2025-10-18T03:30:00+02:00");
|
|
|
|
var updated = reason.WithImpactWindow(from, to);
|
|
|
|
Assert.Equal(from.ToUniversalTime().ToString("O"), updated.ImpactWindowFrom);
|
|
Assert.Equal(to.ToUniversalTime().ToString("O"), updated.ImpactWindowTo);
|
|
}
|
|
}
|