This commit is contained in:
Francesco Renzi
2026-01-22 00:12:27 +00:00
committed by GitHub
parent 1bba60b475
commit d334ab3f0a
8 changed files with 1446 additions and 1183 deletions

View File

@@ -1,687 +0,0 @@
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,726 @@
using System;
using GitHub.Runner.Worker.Dap.StepCommands;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands
{
/// <summary>
/// Unit tests for StepCommandParser.
/// Tests parsing of "steps" commands with the --output flag.
/// </summary>
public sealed class StepCommandParserL0 : IDisposable
{
private TestHostContext _hc;
private StepCommandParser _parser;
public StepCommandParserL0()
{
_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_DetectsStepsPrefix()
{
// Arrange & Act & Assert
Assert.True(_parser.IsStepCommand("steps list"));
Assert.True(_parser.IsStepCommand("steps add run \"test\""));
Assert.True(_parser.IsStepCommand("STEPS LIST")); // case insensitive
Assert.True(_parser.IsStepCommand(" steps list ")); // whitespace
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void IsStepCommand_RejectsInvalid()
{
// Arrange & Act & Assert
Assert.False(_parser.IsStepCommand("step list")); // missing 's'
Assert.False(_parser.IsStepCommand("!step list")); // old format
Assert.False(_parser.IsStepCommand("stepslist")); // no space
Assert.False(_parser.IsStepCommand(""));
Assert.False(_parser.IsStepCommand(null));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void IsStepCommand_AllowsStepsAlone()
{
// "steps" alone should be detected (even if parsing will fail for lack of subcommand)
Assert.True(_parser.IsStepCommand("steps"));
Assert.True(_parser.IsStepCommand(" steps "));
}
#endregion
#region Output Format Flag Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_WithOutputJson()
{
// Arrange & Act
var cmd = _parser.Parse("steps list --output json") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_WithOutputText()
{
// Arrange & Act
var cmd = _parser.Parse("steps list --output text") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Text, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_DefaultOutputIsText()
{
// Arrange & Act
var cmd = _parser.Parse("steps list") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Text, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddCommand_WithOutputFlag()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"echo test\" --output json") as AddRunCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
Assert.Equal("echo test", cmd.Script);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_OutputFlag_ShortForm()
{
// Arrange & Act
var cmd = _parser.Parse("steps list -o json") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_OutputFlag_EqualsForm()
{
// Arrange & Act
var cmd = _parser.Parse("steps list --output=json") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_OutputFlag_TextEqualsForm()
{
// Arrange & Act
var cmd = _parser.Parse("steps list --output=text") as ListCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Text, cmd.Output);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EditCommand_WithOutputJson()
{
// Arrange & Act
var cmd = _parser.Parse("steps edit 3 --name \"New Name\" --output json") as EditCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
Assert.Equal(3, cmd.Index);
Assert.Equal("New Name", cmd.Name);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RemoveCommand_WithOutputJson()
{
// Arrange & Act
var cmd = _parser.Parse("steps remove 5 --output json") as RemoveCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
Assert.Equal(5, cmd.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_WithOutputJson()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 3 --after 5 --output json") as MoveCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
Assert.Equal(3, cmd.FromIndex);
Assert.Equal(PositionType.After, cmd.Position.Type);
Assert.Equal(5, cmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ExportCommand_WithOutputJson()
{
// Arrange & Act
var cmd = _parser.Parse("steps export --output json") as ExportCommand;
// Assert
Assert.NotNull(cmd);
Assert.Equal(OutputFormat.Json, cmd.Output);
}
#endregion
#region List Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_Basic()
{
// Arrange & Act
var cmd = _parser.Parse("steps list");
// Assert
var listCmd = Assert.IsType<ListCommand>(cmd);
Assert.False(listCmd.Verbose);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_WithVerbose()
{
// Arrange & Act
var cmd = _parser.Parse("steps list --verbose");
// Assert
var listCmd = Assert.IsType<ListCommand>(cmd);
Assert.True(listCmd.Verbose);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ListCommand_WithVerboseShort()
{
// Arrange & Act
var cmd = _parser.Parse("steps list -v");
// Assert
var listCmd = Assert.IsType<ListCommand>(cmd);
Assert.True(listCmd.Verbose);
}
#endregion
#region Add Run Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_Basic()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"npm test\"");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal("npm test", addCmd.Script);
Assert.Equal(PositionType.Last, addCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_AllOptions()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"npm run build\" --name \"Build App\" --shell bash --after 3");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal("npm run build", addCmd.Script);
Assert.Equal("Build App", addCmd.Name);
Assert.Equal("bash", addCmd.Shell);
Assert.Equal(PositionType.After, addCmd.Position.Type);
Assert.Equal(3, addCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_WithEnv()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"npm test\" --env NODE_ENV=test --env CI=true");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.NotNull(addCmd.Env);
Assert.Equal("test", addCmd.Env["NODE_ENV"]);
Assert.Equal("true", addCmd.Env["CI"]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_WithContinueOnError()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"npm test\" --continue-on-error");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.True(addCmd.ContinueOnError);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_WithTimeout()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"npm test\" --timeout 30");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal(30, addCmd.Timeout);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_PositionFirst()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"echo first\" --first");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal(PositionType.First, addCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_PositionAt()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"echo at\" --at 5");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal(PositionType.At, addCmd.Position.Type);
Assert.Equal(5, addCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_PositionBefore()
{
// Arrange & Act
var cmd = _parser.Parse("steps add run \"echo before\" --before 3");
// Assert
var addCmd = Assert.IsType<AddRunCommand>(cmd);
Assert.Equal(PositionType.Before, addCmd.Position.Type);
Assert.Equal(3, addCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddRunCommand_MissingScript_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps add run"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Add Uses Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddUsesCommand_Basic()
{
// Arrange & Act
var cmd = _parser.Parse("steps add uses actions/checkout@v4");
// Assert
var addCmd = Assert.IsType<AddUsesCommand>(cmd);
Assert.Equal("actions/checkout@v4", addCmd.Action);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddUsesCommand_AllOptions()
{
// Arrange & Act
var cmd = _parser.Parse("steps add uses actions/setup-node@v4 --name \"Setup Node\" --with node-version=20 --with cache=npm");
// Assert
var addCmd = Assert.IsType<AddUsesCommand>(cmd);
Assert.Equal("actions/setup-node@v4", addCmd.Action);
Assert.Equal("Setup Node", addCmd.Name);
Assert.NotNull(addCmd.With);
Assert.Equal("20", addCmd.With["node-version"]);
Assert.Equal("npm", addCmd.With["cache"]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddUsesCommand_MissingAction_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps add uses"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_AddCommand_InvalidType_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps add invalid \"test\""));
Assert.Equal(StepCommandErrors.InvalidType, ex.ErrorCode);
}
#endregion
#region Edit Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EditCommand_Basic()
{
// Arrange & Act
var cmd = _parser.Parse("steps edit 3 --name \"New Name\"");
// Assert
var editCmd = Assert.IsType<EditCommand>(cmd);
Assert.Equal(3, editCmd.Index);
Assert.Equal("New Name", editCmd.Name);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EditCommand_AllOptions()
{
// Arrange & Act
var cmd = _parser.Parse("steps edit 4 --name \"Updated\" --script \"npm test\" --shell pwsh --if \"failure()\"");
// Assert
var editCmd = Assert.IsType<EditCommand>(cmd);
Assert.Equal(4, editCmd.Index);
Assert.Equal("Updated", editCmd.Name);
Assert.Equal("npm test", editCmd.Script);
Assert.Equal("pwsh", editCmd.Shell);
Assert.Equal("failure()", editCmd.Condition);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EditCommand_MissingIndex_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps edit --name \"Test\""));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EditCommand_InvalidIndex_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps edit abc --name \"Test\""));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Remove Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RemoveCommand()
{
// Arrange & Act
var cmd = _parser.Parse("steps remove 5");
// Assert
var removeCmd = Assert.IsType<RemoveCommand>(cmd);
Assert.Equal(5, removeCmd.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RemoveCommand_MissingIndex_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps remove"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Move Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_After()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 5 --after 2");
// Assert
var moveCmd = Assert.IsType<MoveCommand>(cmd);
Assert.Equal(5, moveCmd.FromIndex);
Assert.Equal(PositionType.After, moveCmd.Position.Type);
Assert.Equal(2, moveCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_Before()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 3 --before 5");
// Assert
var moveCmd = Assert.IsType<MoveCommand>(cmd);
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 Parse_MoveCommand_First()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 5 --first");
// Assert
var moveCmd = Assert.IsType<MoveCommand>(cmd);
Assert.Equal(PositionType.First, moveCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_Last()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 2 --last");
// Assert
var moveCmd = Assert.IsType<MoveCommand>(cmd);
Assert.Equal(PositionType.Last, moveCmd.Position.Type);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_To()
{
// Arrange & Act
var cmd = _parser.Parse("steps move 5 --to 3");
// Assert
var moveCmd = Assert.IsType<MoveCommand>(cmd);
Assert.Equal(PositionType.At, moveCmd.Position.Type);
Assert.Equal(3, moveCmd.Position.Index);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_MissingFrom_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps move --after 2"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_MoveCommand_MissingPosition_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps move 5"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Export Command Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ExportCommand_Default()
{
// Arrange & Act
var cmd = _parser.Parse("steps export");
// Assert
var exportCmd = Assert.IsType<ExportCommand>(cmd);
Assert.False(exportCmd.ChangesOnly);
Assert.False(exportCmd.WithComments);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ExportCommand_WithOptions()
{
// Arrange & Act
var cmd = _parser.Parse("steps export --changes-only --with-comments");
// Assert
var exportCmd = Assert.IsType<ExportCommand>(cmd);
Assert.True(exportCmd.ChangesOnly);
Assert.True(exportCmd.WithComments);
}
#endregion
#region Error Handling Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_InvalidFormat_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("step list"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
Assert.Contains("steps", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_UnknownCommand_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps unknown"));
Assert.Equal(StepCommandErrors.InvalidCommand, ex.ErrorCode);
Assert.Contains("unknown", ex.Message.ToLower());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_Empty_Throws()
{
// Arrange & Act & Assert
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse(""));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_StepsAlone_Throws()
{
// "steps" without a subcommand should throw
var ex = Assert.Throws<StepCommandException>(() => _parser.Parse("steps"));
Assert.Equal(StepCommandErrors.ParseError, ex.ErrorCode);
}
#endregion
#region Case Insensitivity Tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_CaseInsensitive_StepsKeyword()
{
// Arrange & Act & Assert
Assert.IsType<ListCommand>(_parser.Parse("STEPS list"));
Assert.IsType<ListCommand>(_parser.Parse("Steps list"));
Assert.IsType<ListCommand>(_parser.Parse("sTePs list"));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_CaseInsensitive_Subcommand()
{
// Arrange & Act & Assert
Assert.IsType<ListCommand>(_parser.Parse("steps LIST"));
Assert.IsType<ListCommand>(_parser.Parse("steps List"));
Assert.IsType<AddRunCommand>(_parser.Parse("steps ADD run \"test\""));
Assert.IsType<EditCommand>(_parser.Parse("steps EDIT 1"));
}
#endregion
}
}