using System; using System.Collections.Generic; using System.Linq; using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.Pipelines; using GitHub.Runner.Worker; using GitHub.Runner.Worker.Dap.StepCommands; using Moq; using Xunit; namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands { public sealed class StepManipulatorL0 : IDisposable { private TestHostContext _hc; private Mock _ec; private StepManipulator _manipulator; private Queue _jobSteps; public StepManipulatorL0() { _hc = new TestHostContext(this); _manipulator = new StepManipulator(); _manipulator.Initialize(_hc); } public void Dispose() { _hc?.Dispose(); } private void SetupJobContext(int pendingStepCount = 3) { _jobSteps = new Queue(); for (int i = 0; i < pendingStepCount; i++) { var step = CreateMockStep($"Step {i + 1}"); _jobSteps.Enqueue(step); } _ec = new Mock(); _ec.Setup(x => x.JobSteps).Returns(_jobSteps); _manipulator.Initialize(_ec.Object, 0); } private IStep CreateMockStep(string displayName, bool isActionRunner = true) { if (isActionRunner) { var actionRunner = new Mock(); var actionStep = new ActionStep { Id = Guid.NewGuid(), Name = $"_step_{Guid.NewGuid():N}", DisplayName = displayName, Reference = new ScriptReference(), Inputs = CreateScriptInputs("echo hello"), Condition = "success()" }; actionRunner.Setup(x => x.DisplayName).Returns(displayName); actionRunner.Setup(x => x.Action).Returns(actionStep); actionRunner.Setup(x => x.Condition).Returns("success()"); return actionRunner.Object; } else { var step = new Mock(); step.Setup(x => x.DisplayName).Returns(displayName); step.Setup(x => x.Condition).Returns("success()"); return step.Object; } } private MappingToken CreateScriptInputs(string script) { var inputs = new MappingToken(null, null, null); inputs.Add( new StringToken(null, null, null, "script"), new StringToken(null, null, null, script)); return inputs; } #region Initialization Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Initialize_SetsJobContext() { // Arrange SetupJobContext(); // Act & Assert - no exception means success var steps = _manipulator.GetAllSteps(); Assert.Equal(3, steps.Count); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Initialize_ThrowsOnNullContext() { // Arrange var manipulator = new StepManipulator(); manipulator.Initialize(_hc); // Act & Assert Assert.Throws(() => manipulator.Initialize(null, 0)); } #endregion #region GetAllSteps Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetAllSteps_ReturnsPendingSteps() { // Arrange SetupJobContext(3); // Act var steps = _manipulator.GetAllSteps(); // Assert Assert.Equal(3, steps.Count); Assert.All(steps, s => Assert.Equal(StepStatus.Pending, s.Status)); Assert.Equal(1, steps[0].Index); Assert.Equal(2, steps[1].Index); Assert.Equal(3, steps[2].Index); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetAllSteps_IncludesCompletedSteps() { // Arrange SetupJobContext(2); var completedStep = CreateMockStep("Completed Step"); _manipulator.AddCompletedStep(completedStep); // Act var steps = _manipulator.GetAllSteps(); // Assert Assert.Equal(3, steps.Count); Assert.Equal(StepStatus.Completed, steps[0].Status); Assert.Equal("Completed Step", steps[0].Name); Assert.Equal(StepStatus.Pending, steps[1].Status); Assert.Equal(StepStatus.Pending, steps[2].Status); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetAllSteps_IncludesCurrentStep() { // Arrange SetupJobContext(2); var currentStep = CreateMockStep("Current Step"); _manipulator.SetCurrentStep(currentStep); // Act var steps = _manipulator.GetAllSteps(); // Assert Assert.Equal(3, steps.Count); Assert.Equal(StepStatus.Current, steps[0].Status); Assert.Equal("Current Step", steps[0].Name); } #endregion #region GetStep Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetStep_ReturnsCorrectStep() { // Arrange SetupJobContext(3); // Act var step = _manipulator.GetStep(2); // Assert Assert.NotNull(step); Assert.Equal(2, step.Index); Assert.Equal("Step 2", step.Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetStep_ReturnsNullForInvalidIndex() { // Arrange SetupJobContext(3); // Act & Assert Assert.Null(_manipulator.GetStep(0)); Assert.Null(_manipulator.GetStep(4)); Assert.Null(_manipulator.GetStep(-1)); } #endregion #region GetPendingCount Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetPendingCount_ReturnsCorrectCount() { // Arrange SetupJobContext(5); // Act var count = _manipulator.GetPendingCount(); // Assert Assert.Equal(5, count); } #endregion #region GetFirstPendingIndex Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetFirstPendingIndex_WithNoPriorSteps_ReturnsOne() { // Arrange SetupJobContext(3); // Act var index = _manipulator.GetFirstPendingIndex(); // Assert Assert.Equal(1, index); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetFirstPendingIndex_WithCompletedSteps_ReturnsCorrectIndex() { // Arrange SetupJobContext(2); _manipulator.AddCompletedStep(CreateMockStep("Completed 1")); _manipulator.AddCompletedStep(CreateMockStep("Completed 2")); // Act var index = _manipulator.GetFirstPendingIndex(); // Assert Assert.Equal(3, index); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void GetFirstPendingIndex_WithNoSteps_ReturnsNegativeOne() { // Arrange SetupJobContext(0); // Act var index = _manipulator.GetFirstPendingIndex(); // Assert Assert.Equal(-1, index); } #endregion #region InsertStep Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_AtLast_AppendsToQueue() { // Arrange SetupJobContext(2); var newStep = CreateMockStep("New Step"); // Act var index = _manipulator.InsertStep(newStep, StepPosition.Last()); // Assert Assert.Equal(3, index); Assert.Equal(3, _jobSteps.Count); var steps = _manipulator.GetAllSteps(); Assert.Equal("New Step", steps[2].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_AtFirst_PrependsToQueue() { // Arrange SetupJobContext(2); var newStep = CreateMockStep("New Step"); // Act var index = _manipulator.InsertStep(newStep, StepPosition.First()); // Assert Assert.Equal(1, index); var steps = _manipulator.GetAllSteps(); Assert.Equal("New Step", steps[0].Name); Assert.Equal("Step 1", steps[1].Name); Assert.Equal("Step 2", steps[2].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_AtPosition_InsertsCorrectly() { // Arrange SetupJobContext(3); var newStep = CreateMockStep("New Step"); // Act var index = _manipulator.InsertStep(newStep, StepPosition.At(2)); // Assert Assert.Equal(2, index); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 1", steps[0].Name); Assert.Equal("New Step", steps[1].Name); Assert.Equal("Step 2", steps[2].Name); Assert.Equal("Step 3", steps[3].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_AfterPosition_InsertsCorrectly() { // Arrange SetupJobContext(3); var newStep = CreateMockStep("New Step"); // Act var index = _manipulator.InsertStep(newStep, StepPosition.After(1)); // Assert Assert.Equal(2, index); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 1", steps[0].Name); Assert.Equal("New Step", steps[1].Name); Assert.Equal("Step 2", steps[2].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_BeforePosition_InsertsCorrectly() { // Arrange SetupJobContext(3); var newStep = CreateMockStep("New Step"); // Act var index = _manipulator.InsertStep(newStep, StepPosition.Before(3)); // Assert Assert.Equal(3, index); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 1", steps[0].Name); Assert.Equal("Step 2", steps[1].Name); Assert.Equal("New Step", steps[2].Name); Assert.Equal("Step 3", steps[3].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InsertStep_TracksChange() { // Arrange SetupJobContext(2); var newStep = CreateMockStep("New Step"); // Act _manipulator.InsertStep(newStep, StepPosition.Last()); // Assert var changes = _manipulator.GetChanges(); Assert.Single(changes); Assert.Equal(ChangeType.Added, changes[0].Type); Assert.Equal(3, changes[0].CurrentIndex); } #endregion #region RemoveStep Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveStep_RemovesFromQueue() { // Arrange SetupJobContext(3); // Act _manipulator.RemoveStep(2); // Assert Assert.Equal(2, _jobSteps.Count); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 1", steps[0].Name); Assert.Equal("Step 3", steps[1].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveStep_TracksChange() { // Arrange SetupJobContext(3); // Act _manipulator.RemoveStep(2); // Assert var changes = _manipulator.GetChanges(); Assert.Single(changes); Assert.Equal(ChangeType.Removed, changes[0].Type); Assert.Equal(2, changes[0].OriginalIndex); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveStep_ThrowsForCompletedStep() { // Arrange SetupJobContext(2); _manipulator.AddCompletedStep(CreateMockStep("Completed Step")); // Act & Assert var ex = Assert.Throws(() => _manipulator.RemoveStep(1)); Assert.Equal(StepCommandErrors.InvalidIndex, ex.ErrorCode); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveStep_ThrowsForCurrentStep() { // Arrange SetupJobContext(2); _manipulator.SetCurrentStep(CreateMockStep("Current Step")); // Act & Assert var ex = Assert.Throws(() => _manipulator.RemoveStep(1)); Assert.Equal(StepCommandErrors.InvalidIndex, ex.ErrorCode); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveStep_ThrowsForInvalidIndex() { // Arrange SetupJobContext(3); // Act & Assert Assert.Throws(() => _manipulator.RemoveStep(0)); Assert.Throws(() => _manipulator.RemoveStep(4)); } #endregion #region MoveStep Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MoveStep_ToLast_MovesCorrectly() { // Arrange SetupJobContext(3); // Act var newIndex = _manipulator.MoveStep(1, StepPosition.Last()); // Assert Assert.Equal(3, newIndex); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 2", steps[0].Name); Assert.Equal("Step 3", steps[1].Name); Assert.Equal("Step 1", steps[2].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MoveStep_ToFirst_MovesCorrectly() { // Arrange SetupJobContext(3); // Act var newIndex = _manipulator.MoveStep(3, StepPosition.First()); // Assert Assert.Equal(1, newIndex); var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 3", steps[0].Name); Assert.Equal("Step 1", steps[1].Name); Assert.Equal("Step 2", steps[2].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MoveStep_ToMiddle_MovesCorrectly() { // Arrange SetupJobContext(4); // Act - move step 1 to after step 2 (which becomes position 2) var newIndex = _manipulator.MoveStep(1, StepPosition.After(2)); // Assert var steps = _manipulator.GetAllSteps(); Assert.Equal("Step 2", steps[0].Name); Assert.Equal("Step 1", steps[1].Name); Assert.Equal("Step 3", steps[2].Name); Assert.Equal("Step 4", steps[3].Name); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MoveStep_TracksChange() { // Arrange SetupJobContext(3); // Act _manipulator.MoveStep(1, StepPosition.Last()); // Assert var changes = _manipulator.GetChanges(); Assert.Single(changes); Assert.Equal(ChangeType.Moved, changes[0].Type); Assert.Equal(1, changes[0].OriginalIndex); Assert.Equal(3, changes[0].CurrentIndex); } #endregion #region EditStep Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void EditStep_ModifiesActionStep() { // Arrange SetupJobContext(3); // Act _manipulator.EditStep(2, step => { step.DisplayName = "Modified Step"; }); // Assert var steps = _manipulator.GetAllSteps(); var actionRunner = steps[1].Step as IActionRunner; Assert.NotNull(actionRunner); Assert.Equal("Modified Step", actionRunner.Action.DisplayName); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void EditStep_TracksChange() { // Arrange SetupJobContext(3); // Act _manipulator.EditStep(2, step => { step.DisplayName = "Modified Step"; }); // Assert var changes = _manipulator.GetChanges(); Assert.Single(changes); Assert.Equal(ChangeType.Modified, changes[0].Type); Assert.Equal(2, changes[0].CurrentIndex); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void EditStep_ThrowsForCompletedStep() { // Arrange SetupJobContext(2); _manipulator.AddCompletedStep(CreateMockStep("Completed Step")); // Act & Assert var ex = Assert.Throws(() => _manipulator.EditStep(1, step => { })); Assert.Equal(StepCommandErrors.InvalidIndex, ex.ErrorCode); } #endregion #region Change Tracking Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RecordOriginalState_CapturesSteps() { // Arrange SetupJobContext(3); // Act _manipulator.RecordOriginalState(); _manipulator.InsertStep(CreateMockStep("New Step"), StepPosition.Last()); // Assert - changes should be tracked var changes = _manipulator.GetChanges(); Assert.Single(changes); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ClearChanges_RemovesAllChanges() { // Arrange SetupJobContext(3); _manipulator.InsertStep(CreateMockStep("New Step"), StepPosition.Last()); // Act _manipulator.ClearChanges(); // Assert Assert.Empty(_manipulator.GetChanges()); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MultipleOperations_TrackAllChanges() { // Arrange SetupJobContext(3); // Act _manipulator.InsertStep(CreateMockStep("New Step"), StepPosition.Last()); _manipulator.RemoveStep(1); _manipulator.MoveStep(2, StepPosition.First()); // Assert var changes = _manipulator.GetChanges(); Assert.Equal(3, changes.Count); Assert.Equal(ChangeType.Added, changes[0].Type); Assert.Equal(ChangeType.Removed, changes[1].Type); Assert.Equal(ChangeType.Moved, changes[2].Type); } #endregion #region StepInfo Factory Tests [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void StepInfo_FromStep_ExtractsRunStepInfo() { // Arrange var step = CreateMockStep("Test Run Step"); // Act var info = StepInfo.FromStep(step, 1, StepStatus.Pending); // Assert Assert.Equal("Test Run Step", info.Name); Assert.Equal("run", info.Type); Assert.Equal(StepStatus.Pending, info.Status); Assert.NotNull(info.Action); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void StepInfo_FromStep_HandlesNonActionRunner() { // Arrange var step = CreateMockStep("Extension Step", isActionRunner: false); // Act var info = StepInfo.FromStep(step, 1, StepStatus.Pending); // Assert Assert.Equal("Extension Step", info.Name); Assert.Equal("extension", info.Type); Assert.Null(info.Action); } #endregion } }