editing jobs

This commit is contained in:
Francesco Renzi
2026-01-21 22:30:19 +00:00
committed by GitHub
parent 9bc9aff86f
commit 008594a3ee
14 changed files with 6450 additions and 9 deletions

View File

@@ -0,0 +1,687 @@
using System;
using System.Collections.Generic;
using GitHub.Runner.Worker.Dap.StepCommands;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands
{
/// <summary>
/// Unit tests for StepCommandParser JSON API functionality.
/// Tests the parsing of JSON commands for browser extension integration.
/// </summary>
public sealed class StepCommandParserJsonL0 : IDisposable
{
private TestHostContext _hc;
private StepCommandParser _parser;
public StepCommandParserJsonL0()
{
_hc = new TestHostContext(this);
_parser = new StepCommandParser();
_parser.Initialize(_hc);
}
public void Dispose()
{
_hc?.Dispose();
}
#region IsStepCommand Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void IsStepCommand_DetectsJsonFormat()
{
// Arrange & Act & Assert
Assert.True(_parser.IsStepCommand("{\"cmd\":\"step.list\"}"));
Assert.True(_parser.IsStepCommand("{\"cmd\": \"step.add\", \"type\": \"run\"}"));
Assert.True(_parser.IsStepCommand(" { \"cmd\" : \"step.export\" } "));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void IsStepCommand_RejectsInvalidJson()
{
// Arrange & Act & Assert
Assert.False(_parser.IsStepCommand("{\"cmd\":\"other.command\"}"));
Assert.False(_parser.IsStepCommand("{\"action\":\"step.list\"}"));
Assert.False(_parser.IsStepCommand("{\"type\":\"step\"}"));
Assert.False(_parser.IsStepCommand("{}"));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void IsStepCommand_HandlesBothFormats()
{
// REPL format
Assert.True(_parser.IsStepCommand("!step list"));
Assert.True(_parser.IsStepCommand("!STEP ADD run \"test\""));
// JSON format
Assert.True(_parser.IsStepCommand("{\"cmd\":\"step.list\"}"));
}
#endregion
#region JSON List Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_ListCommand_Basic()
{
// Arrange
var json = "{\"cmd\":\"step.list\"}";
// Act
var command = _parser.Parse(json);
// Assert
Assert.IsType<ListCommand>(command);
var listCmd = (ListCommand)command;
Assert.False(listCmd.Verbose);
Assert.True(listCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_ListCommand_WithVerbose()
{
// Arrange
var json = "{\"cmd\":\"step.list\",\"verbose\":true}";
// Act
var command = _parser.Parse(json);
// Assert
var listCmd = Assert.IsType<ListCommand>(command);
Assert.True(listCmd.Verbose);
Assert.True(listCmd.WasJsonInput);
}
#endregion
#region JSON Add Run Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddRunCommand_Basic()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"run\",\"script\":\"npm test\"}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddRunCommand>(command);
Assert.Equal("npm test", addCmd.Script);
Assert.True(addCmd.WasJsonInput);
Assert.Equal(PositionType.Last, addCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddRunCommand_AllOptions()
{
// Arrange
var json = @"{
""cmd"": ""step.add"",
""type"": ""run"",
""script"": ""npm run build"",
""name"": ""Build App"",
""shell"": ""bash"",
""workingDirectory"": ""./src"",
""if"": ""success()"",
""env"": {""NODE_ENV"": ""production"", ""CI"": ""true""},
""continueOnError"": true,
""timeout"": 30,
""position"": {""after"": 3}
}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddRunCommand>(command);
Assert.Equal("npm run build", addCmd.Script);
Assert.Equal("Build App", addCmd.Name);
Assert.Equal("bash", addCmd.Shell);
Assert.Equal("./src", addCmd.WorkingDirectory);
Assert.Equal("success()", addCmd.Condition);
Assert.NotNull(addCmd.Env);
Assert.Equal("production", addCmd.Env["NODE_ENV"]);
Assert.Equal("true", addCmd.Env["CI"]);
Assert.True(addCmd.ContinueOnError);
Assert.Equal(30, addCmd.Timeout);
Assert.Equal(PositionType.After, addCmd.Position.Type);
Assert.Equal(3, addCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddRunCommand_MissingScript_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"run\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("script", ex.Message.ToLower());
}
#endregion
#region JSON Add Uses Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddUsesCommand_Basic()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"uses\",\"action\":\"actions/checkout@v4\"}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddUsesCommand>(command);
Assert.Equal("actions/checkout@v4", addCmd.Action);
Assert.True(addCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddUsesCommand_AllOptions()
{
// Arrange
var json = @"{
""cmd"": ""step.add"",
""type"": ""uses"",
""action"": ""actions/setup-node@v4"",
""name"": ""Setup Node.js"",
""with"": {""node-version"": ""20"", ""cache"": ""npm""},
""env"": {""NODE_OPTIONS"": ""--max-old-space-size=4096""},
""if"": ""always()"",
""continueOnError"": false,
""timeout"": 10,
""position"": {""at"": 2}
}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddUsesCommand>(command);
Assert.Equal("actions/setup-node@v4", addCmd.Action);
Assert.Equal("Setup Node.js", addCmd.Name);
Assert.NotNull(addCmd.With);
Assert.Equal("20", addCmd.With["node-version"]);
Assert.Equal("npm", addCmd.With["cache"]);
Assert.NotNull(addCmd.Env);
Assert.Equal("--max-old-space-size=4096", addCmd.Env["NODE_OPTIONS"]);
Assert.Equal("always()", addCmd.Condition);
Assert.False(addCmd.ContinueOnError);
Assert.Equal(10, addCmd.Timeout);
Assert.Equal(PositionType.At, addCmd.Position.Type);
Assert.Equal(2, addCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddUsesCommand_MissingAction_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"uses\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("action", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_AddCommand_InvalidType_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"invalid\",\"script\":\"echo test\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.InvalidType, ex.ErrorCode);
}
#endregion
#region JSON Edit Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_EditCommand_Basic()
{
// Arrange
var json = "{\"cmd\":\"step.edit\",\"index\":3,\"name\":\"New Name\"}";
// Act
var command = _parser.Parse(json);
// Assert
var editCmd = Assert.IsType<EditCommand>(command);
Assert.Equal(3, editCmd.Index);
Assert.Equal("New Name", editCmd.Name);
Assert.True(editCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_EditCommand_AllOptions()
{
// Arrange
var json = @"{
""cmd"": ""step.edit"",
""index"": 4,
""name"": ""Updated Step"",
""script"": ""npm run test:ci"",
""shell"": ""pwsh"",
""workingDirectory"": ""./tests"",
""if"": ""failure()"",
""with"": {""key1"": ""value1""},
""env"": {""DEBUG"": ""true""},
""removeWith"": [""oldKey""],
""removeEnv"": [""OBSOLETE""],
""continueOnError"": true,
""timeout"": 15
}";
// Act
var command = _parser.Parse(json);
// Assert
var editCmd = Assert.IsType<EditCommand>(command);
Assert.Equal(4, editCmd.Index);
Assert.Equal("Updated Step", editCmd.Name);
Assert.Equal("npm run test:ci", editCmd.Script);
Assert.Equal("pwsh", editCmd.Shell);
Assert.Equal("./tests", editCmd.WorkingDirectory);
Assert.Equal("failure()", editCmd.Condition);
Assert.NotNull(editCmd.With);
Assert.Equal("value1", editCmd.With["key1"]);
Assert.NotNull(editCmd.Env);
Assert.Equal("true", editCmd.Env["DEBUG"]);
Assert.NotNull(editCmd.RemoveWith);
Assert.Contains("oldKey", editCmd.RemoveWith);
Assert.NotNull(editCmd.RemoveEnv);
Assert.Contains("OBSOLETE", editCmd.RemoveEnv);
Assert.True(editCmd.ContinueOnError);
Assert.Equal(15, editCmd.Timeout);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_EditCommand_MissingIndex_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.edit\",\"name\":\"New Name\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("index", ex.Message.ToLower());
}
#endregion
#region JSON Remove Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_RemoveCommand()
{
// Arrange
var json = "{\"cmd\":\"step.remove\",\"index\":5}";
// Act
var command = _parser.Parse(json);
// Assert
var removeCmd = Assert.IsType<RemoveCommand>(command);
Assert.Equal(5, removeCmd.Index);
Assert.True(removeCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_RemoveCommand_MissingIndex_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.remove\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("index", ex.Message.ToLower());
}
#endregion
#region JSON Move Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_After()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":5,\"position\":{\"after\":2}}";
// Act
var command = _parser.Parse(json);
// Assert
var moveCmd = Assert.IsType<MoveCommand>(command);
Assert.Equal(5, moveCmd.FromIndex);
Assert.Equal(PositionType.After, moveCmd.Position.Type);
Assert.Equal(2, moveCmd.Position.Index);
Assert.True(moveCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_Before()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":3,\"position\":{\"before\":5}}";
// Act
var command = _parser.Parse(json);
// Assert
var moveCmd = Assert.IsType<MoveCommand>(command);
Assert.Equal(3, moveCmd.FromIndex);
Assert.Equal(PositionType.Before, moveCmd.Position.Type);
Assert.Equal(5, moveCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_First()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":5,\"position\":{\"first\":true}}";
// Act
var command = _parser.Parse(json);
// Assert
var moveCmd = Assert.IsType<MoveCommand>(command);
Assert.Equal(PositionType.First, moveCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_Last()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":2,\"position\":{\"last\":true}}";
// Act
var command = _parser.Parse(json);
// Assert
var moveCmd = Assert.IsType<MoveCommand>(command);
Assert.Equal(PositionType.Last, moveCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_At()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":5,\"position\":{\"at\":3}}";
// Act
var command = _parser.Parse(json);
// Assert
var moveCmd = Assert.IsType<MoveCommand>(command);
Assert.Equal(PositionType.At, moveCmd.Position.Type);
Assert.Equal(3, moveCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_MissingFrom_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"position\":{\"after\":2}}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("from", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MoveCommand_MissingPosition_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.move\",\"from\":5}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("position", ex.Message.ToLower());
}
#endregion
#region JSON Export Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_ExportCommand_Default()
{
// Arrange
var json = "{\"cmd\":\"step.export\"}";
// Act
var command = _parser.Parse(json);
// Assert
var exportCmd = Assert.IsType<ExportCommand>(command);
Assert.False(exportCmd.ChangesOnly);
Assert.False(exportCmd.WithComments);
Assert.True(exportCmd.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_ExportCommand_WithOptions()
{
// Arrange
var json = "{\"cmd\":\"step.export\",\"changesOnly\":true,\"withComments\":true}";
// Act
var command = _parser.Parse(json);
// Assert
var exportCmd = Assert.IsType<ExportCommand>(command);
Assert.True(exportCmd.ChangesOnly);
Assert.True(exportCmd.WithComments);
}
#endregion
#region JSON Error Handling Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_InvalidJson_Throws()
{
// Arrange
var json = "{invalid json}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("Invalid JSON", ex.Message);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_MissingCmd_Throws()
{
// Arrange
var json = "{\"action\":\"list\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("cmd", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_UnknownCommand_Throws()
{
// Arrange
var json = "{\"cmd\":\"step.unknown\"}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.InvalidCommand, ex.ErrorCode);
Assert.Contains("unknown", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_EmptyJson_Throws()
{
// Arrange
var json = "{}";
// Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(json));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Position Parsing Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_PositionDefaults_ToLast()
{
// Arrange - position is optional for add commands
var json = "{\"cmd\":\"step.add\",\"type\":\"run\",\"script\":\"test\"}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddRunCommand>(command);
Assert.Equal(PositionType.Last, addCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_NullPosition_DefaultsToLast()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"run\",\"script\":\"test\",\"position\":null}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddRunCommand>(command);
Assert.Equal(PositionType.Last, addCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseJson_EmptyPosition_DefaultsToLast()
{
// Arrange
var json = "{\"cmd\":\"step.add\",\"type\":\"run\",\"script\":\"test\",\"position\":{}}";
// Act
var command = _parser.Parse(json);
// Assert
var addCmd = Assert.IsType<AddRunCommand>(command);
Assert.Equal(PositionType.Last, addCmd.Position.Type);
}
#endregion
#region WasJsonInput Flag Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_JsonInput_SetsWasJsonInputTrue()
{
// Arrange
var json = "{\"cmd\":\"step.list\"}";
// Act
var command = _parser.Parse(json);
// Assert
Assert.True(command.WasJsonInput);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ReplInput_SetsWasJsonInputFalse()
{
// Arrange
var repl = "!step list";
// Act
var command = _parser.Parse(repl);
// Assert
Assert.False(command.WasJsonInput);
}
#endregion
}
}

View File

@@ -0,0 +1,725 @@
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<IExecutionContext> _ec;
private StepManipulator _manipulator;
private Queue<IStep> _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<IStep>();
for (int i = 0; i < pendingStepCount; i++)
{
var step = CreateMockStep($"Step {i + 1}");
_jobSteps.Enqueue(step);
}
_ec = new Mock<IExecutionContext>();
_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<IActionRunner>();
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<IStep>();
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<ArgumentNullException>(() => 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<StepCommandException>(() => _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<StepCommandException>(() => _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<StepCommandException>(() => _manipulator.RemoveStep(0));
Assert.Throws<StepCommandException>(() => _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<StepCommandException>(() =>
_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
}
}