using FluentAssertions; using Xunit; namespace OECS.Tests; public class InterruptTests { // ── Test types ── private record struct TestInterrupt : IInterrupt { public string Message; } [MessagePack.MessagePackObject] private struct TestHandler : IInterruptHandlerCommand { [MessagePack.Key(0)] public bool ShouldResolve; public bool TryResolve(TestInterrupt interrupt) { return ShouldResolve; } } private record struct AnotherInterrupt : IInterrupt { public int Value; } [MessagePack.MessagePackObject] private struct AnotherHandler : IInterruptHandlerCommand { [MessagePack.Key(0)] public bool ShouldResolve; public bool TryResolve(AnotherInterrupt interrupt) { return ShouldResolve; } } // ── Issue and resolve ── [Fact] public void Interrupt_blocks_next_tick() { var world = new World(); var group = new SystemGroup(world); var systemRan = false; group.Add(new TestSystem(() => systemRan = true)); // Issue an interrupt. world.Interrupt(new TestInterrupt { Message = "hello" }); // Run a tick — systems should be skipped. group.RunLogical(); systemRan.Should().BeFalse(); } [Fact] public void Interrupt_resolved_by_handler_unblocks_world() { var world = new World(); var group = new SystemGroup(world); var systemRan = false; group.Add(new TestSystem(() => systemRan = true)); // Issue an interrupt. world.Interrupt(new TestInterrupt { Message = "hello" }); group.RunLogical(); systemRan.Should().BeFalse(); // Enqueue a handler that resolves it. world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); group.RunLogical(); systemRan.Should().BeTrue(); } [Fact] public void Interrupt_handler_rejects_leaves_interrupt_pending() { var world = new World(); var group = new SystemGroup(world); var systemRan = false; group.Add(new TestSystem(() => systemRan = true)); world.Interrupt(new TestInterrupt { Message = "hello" }); group.RunLogical(); systemRan.Should().BeFalse(); // Handler rejects — interrupt stays pending. world.Commands.Enqueue(new TestHandler { ShouldResolve = false }); group.RunLogical(); systemRan.Should().BeFalse(); // Second handler accepts. world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); group.RunLogical(); systemRan.Should().BeTrue(); } [Fact] public void HasInterrupt_returns_true_for_pending_interrupt() { var world = new World(); var group = new SystemGroup(world); world.HasInterrupt().Should().BeFalse(); world.Interrupt(new TestInterrupt { Message = "hello" }); world.HasInterrupt().Should().BeTrue(); world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); group.RunLogical(); world.HasInterrupt().Should().BeFalse(); } [Fact] public void Duplicate_interrupt_type_throws() { var world = new World(); var group = new SystemGroup(world); world.Interrupt(new TestInterrupt { Message = "first" }); var act = () => world.Interrupt(new TestInterrupt { Message = "second" }); act.Should().Throw() .WithMessage("*TestInterrupt*"); } [Fact] public void Multiple_interrupt_types_independent() { var world = new World(); var group = new SystemGroup(world); world.Interrupt(new TestInterrupt { Message = "a" }); world.Interrupt(new AnotherInterrupt { Value = 42 }); // Resolve only one. world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); group.RunLogical(); // TestInterrupt resolved, AnotherInterrupt still pending. world.HasInterrupt().Should().BeFalse(); world.HasInterrupt().Should().BeTrue(); // Resolve the other. world.Commands.Enqueue(new AnotherHandler { ShouldResolve = true }); group.RunLogical(); world.HasInterrupt().Should().BeFalse(); } [Fact] public void Handler_receives_correct_interrupt_data() { var world = new World(); var group = new SystemGroup(world); world.Interrupt(new TestInterrupt { Message = "hello" }); // Enqueue handler and run — the default Execute calls TryResolve // with the interrupt data. world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); group.RunLogical(); // Interrupt was resolved — the TryResolve received the correct data. world.HasInterrupt().Should().BeFalse(); } [Fact] public void Interrupt_without_system_group_is_noop() { var world = new World(); // No SystemGroup — interrupt is silently ignored. world.Interrupt(new TestInterrupt { Message = "hello" }); world.HasInterrupt().Should().BeFalse(); } [Fact] public void Commands_still_execute_when_blocked() { var world = new World(); var group = new SystemGroup(world); var commandExecuted = false; var handler = new TestHandler { ShouldResolve = true }; world.Interrupt(new TestInterrupt { Message = "hello" }); // Enqueue both a handler and a regular command. world.Commands.Enqueue(new TestHandler { ShouldResolve = true }); world.Commands.Enqueue(new SetFlagCommand(() => commandExecuted = true)); group.RunLogical(); // Both should have executed. world.HasInterrupt().Should().BeFalse(); commandExecuted.Should().BeTrue(); } // ── Helpers ── private class TestSystem : ISystem { private readonly Action _action; public TestSystem(Action action) => _action = action; public void RunImpl(World world) => _action(); } [MessagePack.MessagePackObject] private struct SetFlagCommand : ICommand { private Action? _action; [MessagePack.IgnoreMember] public Action Action { get => _action!; set => _action = value; } public SetFlagCommand(Action action) => _action = action; public void Execute(World world) => _action?.Invoke(); } }