mirror of
https://github.com/actions/runner.git
synced 2026-01-23 04:51:23 +08:00
simplify
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Plan: Simplify Step Commands to Use REPL Format
|
# Plan: Simplify Step Commands to Use REPL Format
|
||||||
|
|
||||||
**Status:** Ready for Implementation
|
**Status:** Complete
|
||||||
**Date:** January 2026
|
**Date:** January 2026
|
||||||
**Prerequisites:** dap-step-manipulation.md (Chunks 1-9 completed)
|
**Prerequisites:** dap-step-manipulation.md (Chunks 1-9 completed)
|
||||||
|
|
||||||
@@ -30,12 +30,12 @@ This causes issues:
|
|||||||
|
|
||||||
## Progress Checklist
|
## Progress Checklist
|
||||||
|
|
||||||
- [ ] **Chunk 1:** Update StepCommandParser - `steps` prefix, `--output` flag, remove JSON parsing
|
- [x] **Chunk 1:** Update StepCommandParser - `steps` prefix, `--output` flag, remove JSON parsing
|
||||||
- [ ] **Chunk 2:** Update StepCommandHandler - format responses based on OutputFormat
|
- [x] **Chunk 2:** Update StepCommandHandler - format responses based on OutputFormat
|
||||||
- [ ] **Chunk 3:** Update Browser Extension - build REPL command strings
|
- [x] **Chunk 3:** Update Browser Extension - build REPL command strings
|
||||||
- [ ] **Chunk 4:** Update REPL context detection in browser extension
|
- [x] **Chunk 4:** Update REPL context detection in browser extension
|
||||||
- [ ] **Chunk 5:** Update/remove tests
|
- [x] **Chunk 5:** Update/remove tests
|
||||||
- [ ] **Chunk 6:** Update plan documentation
|
- [x] **Chunk 6:** Update plan documentation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
- [x] **Chunk 2:** Step Serializer (ActionStep → YAML)
|
- [x] **Chunk 2:** Step Serializer (ActionStep → YAML)
|
||||||
- [x] **Chunk 3:** Step Factory (Create new steps)
|
- [x] **Chunk 3:** Step Factory (Create new steps)
|
||||||
- [x] **Chunk 4:** Step Manipulator (Queue operations)
|
- [x] **Chunk 4:** Step Manipulator (Queue operations)
|
||||||
- [x] **Chunk 5:** REPL Commands (!step list, !step add run, !step edit, !step remove, !step move)
|
- [x] **Chunk 5:** REPL Commands (steps list, steps add run, steps edit, steps remove, steps move)
|
||||||
- [x] **Chunk 6:** Action Download Integration (!step add uses)
|
- [x] **Chunk 6:** Action Download Integration (steps add uses)
|
||||||
- [x] **Chunk 7:** Export Command (!step export)
|
- [x] **Chunk 7:** Export Command (steps export)
|
||||||
- [x] **Chunk 8:** JSON API for Browser Extension
|
- [x] **Chunk 8:** Output Format Flag (--output text|json for programmatic use)
|
||||||
- [x] **Chunk 9:** Browser Extension UI
|
- [x] **Chunk 9:** Browser Extension UI
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -28,7 +28,7 @@ This transforms the debugger from a "read-only inspection tool" into an **intera
|
|||||||
- **Primary:** Enable add/edit/move/delete of job steps during debug session
|
- **Primary:** Enable add/edit/move/delete of job steps during debug session
|
||||||
- **Primary:** Support both `run` and `uses` step types
|
- **Primary:** Support both `run` and `uses` step types
|
||||||
- **Primary:** Export modified steps as valid YAML
|
- **Primary:** Export modified steps as valid YAML
|
||||||
- **Secondary:** Provide both REPL commands and JSON API for different clients
|
- **Secondary:** Provide `--output` flag for text/JSON response format
|
||||||
- **Non-goal:** Full workflow file reconstruction (steps section only)
|
- **Non-goal:** Full workflow file reconstruction (steps section only)
|
||||||
- **Non-goal:** Production action restriction enforcement (noted for later)
|
- **Non-goal:** Production action restriction enforcement (noted for later)
|
||||||
|
|
||||||
@@ -37,9 +37,17 @@ This transforms the debugger from a "read-only inspection tool" into an **intera
|
|||||||
### Grammar
|
### Grammar
|
||||||
|
|
||||||
```
|
```
|
||||||
!step <command> [target] [options]
|
steps <command> [target] [options] [--output text|json]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
|
||||||
|
All commands support the `--output` flag to control response format:
|
||||||
|
- `--output text` (default) - Human-readable text output
|
||||||
|
- `--output json` - JSON output for programmatic use
|
||||||
|
- Short form: `-o json`, `-o text`
|
||||||
|
- Equals form: `--output=json`, `--output=text`
|
||||||
|
|
||||||
### Index Reference
|
### Index Reference
|
||||||
|
|
||||||
- **1-based indexing** for user-friendliness
|
- **1-based indexing** for user-friendliness
|
||||||
@@ -50,16 +58,16 @@ This transforms the debugger from a "read-only inspection tool" into an **intera
|
|||||||
|
|
||||||
| Command | Purpose | Example |
|
| Command | Purpose | Example |
|
||||||
|---------|---------|---------|
|
|---------|---------|---------|
|
||||||
| `!step list` | Show all steps | `!step list --verbose` |
|
| `steps list` | Show all steps | `steps list --verbose` |
|
||||||
| `!step add` | Add new step | `!step add run "npm test" --after 3` |
|
| `steps add` | Add new step | `steps add run "npm test" --after 3` |
|
||||||
| `!step edit` | Modify step | `!step edit 4 --script "npm run test:ci"` |
|
| `steps edit` | Modify step | `steps edit 4 --script "npm run test:ci"` |
|
||||||
| `!step remove` | Delete step | `!step remove 5` |
|
| `steps remove` | Delete step | `steps remove 5` |
|
||||||
| `!step move` | Reorder step | `!step move 5 --after 2` |
|
| `steps move` | Reorder step | `steps move 5 --after 2` |
|
||||||
| `!step export` | Generate YAML | `!step export --with-comments` |
|
| `steps export` | Generate YAML | `steps export --with-comments` |
|
||||||
|
|
||||||
### Position Modifiers
|
### Position Modifiers
|
||||||
|
|
||||||
For `!step add` and `!step move`:
|
For `steps add` and `steps move`:
|
||||||
- `--at <index>` — Insert at specific position
|
- `--at <index>` — Insert at specific position
|
||||||
- `--after <index>` — Insert after step
|
- `--after <index>` — Insert after step
|
||||||
- `--before <index>` — Insert before step
|
- `--before <index>` — Insert before step
|
||||||
@@ -91,11 +99,16 @@ See "Command API Full Reference" section at end of document.
|
|||||||
```csharp
|
```csharp
|
||||||
public interface IStepCommandParser
|
public interface IStepCommandParser
|
||||||
{
|
{
|
||||||
StepCommand Parse(string input); // "!step add run \"echo hello\" --after 3"
|
StepCommand Parse(string input); // "steps add run \"echo hello\" --after 3"
|
||||||
bool IsStepCommand(string input); // Starts with "!step" or is JSON with cmd:"step.*"
|
bool IsStepCommand(string input); // Starts with "steps " or equals "steps"
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class StepCommand { }
|
public enum OutputFormat { Text, Json }
|
||||||
|
|
||||||
|
public abstract class StepCommand
|
||||||
|
{
|
||||||
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
||||||
|
}
|
||||||
public class ListCommand : StepCommand { public bool Verbose; }
|
public class ListCommand : StepCommand { public bool Verbose; }
|
||||||
public class AddRunCommand : StepCommand {
|
public class AddRunCommand : StepCommand {
|
||||||
public string Script;
|
public string Script;
|
||||||
@@ -450,7 +463,7 @@ See "Command API Full Reference" section at end of document.
|
|||||||
|
|
||||||
### Chunk 5: REPL Commands (run steps)
|
### Chunk 5: REPL Commands (run steps)
|
||||||
|
|
||||||
**Goal:** Implement `!step list`, `!step add run`, `!step edit`, `!step remove`, `!step move`.
|
**Goal:** Implement `steps list`, `steps add run`, `steps edit`, `steps remove`, `steps move`.
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add `HandleStepCommandAsync()`
|
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add `HandleStepCommandAsync()`
|
||||||
@@ -557,9 +570,9 @@ See "Command API Full Reference" section at end of document.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Chunk 6: Action Download Integration (!step add uses)
|
### Chunk 6: Action Download Integration (steps add uses)
|
||||||
|
|
||||||
**Goal:** Support `!step add uses` with full action download.
|
**Goal:** Support `steps add uses` with full action download.
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
||||||
@@ -628,7 +641,7 @@ See "Command API Full Reference" section at end of document.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Chunk 7: Export Command (!step export)
|
### Chunk 7: Export Command (steps export)
|
||||||
|
|
||||||
**Goal:** Generate YAML output for modified steps.
|
**Goal:** Generate YAML output for modified steps.
|
||||||
|
|
||||||
@@ -701,77 +714,93 @@ See "Command API Full Reference" section at end of document.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Chunk 8: JSON API for Browser Extension
|
### Chunk 8: Output Format Flag for Browser Extension
|
||||||
|
|
||||||
**Goal:** Add JSON command support for programmatic access.
|
**Goal:** Add `--output` flag support for programmatic access (replaces separate JSON API).
|
||||||
|
|
||||||
**Files to modify:**
|
**Files to modify:**
|
||||||
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
|
||||||
- `src/Runner.Worker/Dap/DapDebugSession.cs`
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
||||||
|
|
||||||
**Details:**
|
**Details:**
|
||||||
|
|
||||||
1. **Detect JSON input:**
|
1. **Add OutputFormat enum and property to base StepCommand:**
|
||||||
```csharp
|
```csharp
|
||||||
public bool IsStepCommand(string input)
|
public enum OutputFormat { Text, Json }
|
||||||
|
|
||||||
|
public abstract class StepCommand
|
||||||
{
|
{
|
||||||
var trimmed = input.Trim();
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
||||||
return trimmed.StartsWith("!step") ||
|
|
||||||
(trimmed.StartsWith("{") && trimmed.Contains("\"cmd\"") && trimmed.Contains("\"step."));
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Parse JSON commands:**
|
2. **Parse `--output` flag in command parser:**
|
||||||
```csharp
|
```csharp
|
||||||
public StepCommand Parse(string input)
|
// Recognize --output json, --output text, -o json, -o text, --output=json
|
||||||
|
private OutputFormat ParseOutputFlag(List<string> tokens)
|
||||||
{
|
{
|
||||||
var trimmed = input.Trim();
|
for (int i = 0; i < tokens.Count; i++)
|
||||||
if (trimmed.StartsWith("{"))
|
|
||||||
return ParseJsonCommand(trimmed);
|
|
||||||
else
|
|
||||||
return ParseReplCommand(trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private StepCommand ParseJsonCommand(string json)
|
|
||||||
{
|
|
||||||
var obj = JObject.Parse(json);
|
|
||||||
var cmd = obj["cmd"]?.ToString();
|
|
||||||
|
|
||||||
return cmd switch
|
|
||||||
{
|
{
|
||||||
"step.list" => new ListCommand { Verbose = obj["verbose"]?.Value<bool>() ?? false },
|
var token = tokens[i].ToLower();
|
||||||
"step.add" => ParseJsonAddCommand(obj),
|
if (token == "--output" || token == "-o")
|
||||||
"step.edit" => ParseJsonEditCommand(obj),
|
{
|
||||||
"step.remove" => new RemoveCommand { Index = obj["index"].Value<int>() },
|
if (i + 1 < tokens.Count)
|
||||||
"step.move" => ParseJsonMoveCommand(obj),
|
{
|
||||||
"step.export" => new ExportCommand {
|
var format = tokens[i + 1].ToLower();
|
||||||
ChangesOnly = obj["changesOnly"]?.Value<bool>() ?? false,
|
tokens.RemoveAt(i); tokens.RemoveAt(i);
|
||||||
WithComments = obj["withComments"]?.Value<bool>() ?? false
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
||||||
},
|
}
|
||||||
_ => throw new StepCommandException($"Unknown command: {cmd}")
|
}
|
||||||
|
else if (token.StartsWith("--output="))
|
||||||
|
{
|
||||||
|
var format = token.Substring("--output=".Length);
|
||||||
|
tokens.RemoveAt(i);
|
||||||
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OutputFormat.Text;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Format responses based on Output property in handler:**
|
||||||
|
```csharp
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Result = data })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = FormatAsText(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **JSON response format:**
|
4. **Browser extension sends REPL commands with `--output json`:**
|
||||||
```csharp
|
```javascript
|
||||||
// For JSON input, return structured JSON response
|
// Browser extension builds command strings like:
|
||||||
if (wasJsonInput)
|
// "steps list --output json"
|
||||||
{
|
// "steps add run \"echo test\" --after 3 --output json"
|
||||||
return CreateSuccessResponse(new EvaluateResponseBody
|
|
||||||
{
|
|
||||||
Result = JsonConvert.SerializeObject(result),
|
|
||||||
Type = "json"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Testing:**
|
**Benefits over separate JSON API:**
|
||||||
- All commands via JSON
|
- Single code path for parsing all commands
|
||||||
- Verify JSON responses are parseable
|
- Easier debugging (UI sends same commands humans would type)
|
||||||
- Test error responses
|
- Commands can be copy-pasted from UI to console for testing
|
||||||
|
- Less code to maintain
|
||||||
|
|
||||||
**Estimated effort:** Small-medium
|
**Testing:**
|
||||||
|
- All commands with `--output json` return valid JSON
|
||||||
|
- All commands with `--output text` (or default) return human-readable text
|
||||||
|
- Short form `-o json` works correctly
|
||||||
|
|
||||||
|
**Estimated effort:** Small
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -845,13 +874,18 @@ See "Command API Full Reference" section at end of document.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Send commands via JSON API:**
|
6. **Send commands via REPL format with `--output json`:**
|
||||||
```javascript
|
```javascript
|
||||||
async function addStep(type, options) {
|
async function addStep(type, options) {
|
||||||
const cmd = { cmd: 'step.add', type, ...options };
|
const cmd = buildStepCommand('step.add', { type, ...options });
|
||||||
const response = await sendEvaluate(JSON.stringify(cmd));
|
const response = await sendEvaluate(cmd); // e.g., "steps add run \"echo test\" --output json"
|
||||||
refreshStepList();
|
refreshStepList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSteps() {
|
||||||
|
const response = await sendEvaluate('steps list --output json');
|
||||||
|
// Parse JSON response
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Testing:**
|
**Testing:**
|
||||||
@@ -883,7 +917,9 @@ See "Command API Full Reference" section at end of document.
|
|||||||
| File | Chunk | Changes |
|
| File | Chunk | Changes |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| `DapDebugSession.cs` | 1, 5 | Add command dispatch, wire up services |
|
| `DapDebugSession.cs` | 1, 5 | Add command dispatch, wire up services |
|
||||||
| `content.js` | 9 | Steps panel, dialogs, export modal |
|
| `StepCommandParser.cs` | 8 | Add `--output` flag parsing |
|
||||||
|
| `StepCommandHandler.cs` | 8 | Format responses based on OutputFormat |
|
||||||
|
| `content.js` | 9 | Steps panel, dialogs, export modal, build REPL commands |
|
||||||
| `content.css` | 9 | Styling for new UI elements |
|
| `content.css` | 9 | Styling for new UI elements |
|
||||||
| `background.js` | 9 | Helper functions if needed |
|
| `background.js` | 9 | Helper functions if needed |
|
||||||
|
|
||||||
@@ -891,10 +927,23 @@ See "Command API Full Reference" section at end of document.
|
|||||||
|
|
||||||
## Command API Full Reference
|
## Command API Full Reference
|
||||||
|
|
||||||
### !step list
|
### Output Format Flag
|
||||||
|
|
||||||
|
All commands support the `--output` flag:
|
||||||
|
|
||||||
```
|
```
|
||||||
!step list [--verbose]
|
steps <command> ... --output text # Human-readable output (default)
|
||||||
|
steps <command> ... --output json # JSON output for programmatic use
|
||||||
|
steps <command> ... -o json # Short form
|
||||||
|
steps <command> ... --output=json # Equals form
|
||||||
|
```
|
||||||
|
|
||||||
|
The browser extension uses `--output json` for all commands to receive structured responses.
|
||||||
|
|
||||||
|
### steps list
|
||||||
|
|
||||||
|
```
|
||||||
|
steps list [--verbose] [--output text|json]
|
||||||
```
|
```
|
||||||
|
|
||||||
Show all steps with their indices, status, and modification state.
|
Show all steps with their indices, status, and modification state.
|
||||||
@@ -912,16 +961,25 @@ Steps:
|
|||||||
Legend: ✓ = completed, ▶ = current/paused, [ADDED] = new, [MODIFIED] = edited
|
Legend: ✓ = completed, ▶ = current/paused, [ADDED] = new, [MODIFIED] = edited
|
||||||
```
|
```
|
||||||
|
|
||||||
**JSON:**
|
**JSON output (`steps list --output json`):**
|
||||||
```json
|
```json
|
||||||
{"cmd": "step.list", "verbose": false}
|
{
|
||||||
|
"Success": true,
|
||||||
|
"Result": [
|
||||||
|
{"index": 1, "name": "Checkout", "type": "uses", "typeDetail": "actions/checkout@v4", "status": "completed"},
|
||||||
|
{"index": 2, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "completed"},
|
||||||
|
{"index": 3, "name": "Install deps", "type": "run", "typeDetail": "npm ci", "status": "current"},
|
||||||
|
{"index": 4, "name": "Run tests", "type": "run", "typeDetail": "npm test", "status": "pending", "change": "MODIFIED"},
|
||||||
|
{"index": 5, "name": "Build", "type": "run", "typeDetail": "npm run build", "status": "pending", "change": "ADDED"}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### !step add
|
### steps add
|
||||||
|
|
||||||
**Run step:**
|
**Run step:**
|
||||||
```
|
```
|
||||||
!step add run "<script>" [options]
|
steps add run "<script>" [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name "<name>" Display name
|
--name "<name>" Display name
|
||||||
@@ -936,11 +994,12 @@ Options:
|
|||||||
--before <index> Insert before step
|
--before <index> Insert before step
|
||||||
--first Insert at first pending position
|
--first Insert at first pending position
|
||||||
--last Insert at end (default)
|
--last Insert at end (default)
|
||||||
|
--output text|json Response format (default: text)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Uses step:**
|
**Uses step:**
|
||||||
```
|
```
|
||||||
!step add uses <action> [options]
|
steps add uses <action> [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name "<name>" Display name
|
--name "<name>" Display name
|
||||||
@@ -950,50 +1009,138 @@ Options:
|
|||||||
--continue-on-error Don't fail job on step failure
|
--continue-on-error Don't fail job on step failure
|
||||||
--timeout <minutes> Step timeout
|
--timeout <minutes> Step timeout
|
||||||
[position options same as run]
|
[position options same as run]
|
||||||
|
--output text|json Response format (default: text)
|
||||||
```
|
```
|
||||||
|
|
||||||
**JSON (run):**
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Human usage (text output)
|
||||||
|
steps add run "npm test" --name "Run Tests" --after 3
|
||||||
|
|
||||||
|
# Browser extension (JSON output)
|
||||||
|
steps add uses actions/setup-node@v4 --with node-version=20 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON response (`--output json`):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"cmd": "step.add",
|
"Success": true,
|
||||||
"type": "run",
|
"Message": "Step added at position 4",
|
||||||
"script": "npm test",
|
"Result": {"index": 4, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "pending", "change": "ADDED"}
|
||||||
"name": "Run Tests",
|
|
||||||
"shell": "bash",
|
|
||||||
"workingDirectory": "src",
|
|
||||||
"if": "success()",
|
|
||||||
"env": {"NODE_ENV": "test"},
|
|
||||||
"continueOnError": false,
|
|
||||||
"timeout": 10,
|
|
||||||
"position": {"after": 3}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**JSON (uses):**
|
**Position options:**
|
||||||
|
- `--at 3` — Insert at position 3
|
||||||
|
- `--after 2` — Insert after step 2
|
||||||
|
- `--before 4` — Insert before step 4
|
||||||
|
- `--first` — Insert at first pending position
|
||||||
|
- `--last` — Insert at end (default if omitted)
|
||||||
|
|
||||||
|
### steps edit
|
||||||
|
|
||||||
|
```
|
||||||
|
steps edit <index> [modifications]
|
||||||
|
|
||||||
|
Modifications:
|
||||||
|
--name "<name>" Change display name
|
||||||
|
--script "<script>" Change script (run only)
|
||||||
|
--action "<action>" Change action (uses only)
|
||||||
|
--shell <shell> Change shell (run only)
|
||||||
|
--working-directory <path> Change working directory
|
||||||
|
--if "<condition>" Change condition
|
||||||
|
--with key=value Set/update input (uses only)
|
||||||
|
--env KEY=value Set/update env var
|
||||||
|
--remove-with <key> Remove input
|
||||||
|
--remove-env <KEY> Remove env var
|
||||||
|
--continue-on-error Enable continue-on-error
|
||||||
|
--no-continue-on-error Disable continue-on-error
|
||||||
|
--timeout <minutes> Change timeout
|
||||||
|
--output text|json Response format (default: text)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Human usage
|
||||||
|
steps edit 4 --script "npm run test:ci" --name "CI Tests"
|
||||||
|
|
||||||
|
# Browser extension
|
||||||
|
steps edit 4 --script "npm run test:ci" --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON response (`--output json`):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"cmd": "step.add",
|
"Success": true,
|
||||||
"type": "uses",
|
"Message": "Step 4 updated",
|
||||||
"action": "actions/setup-node@v4",
|
"Result": {"index": 4, "name": "CI Tests", "type": "run", "typeDetail": "npm run test:ci", "status": "pending", "change": "MODIFIED"}
|
||||||
"name": "Setup Node",
|
|
||||||
"with": {"node-version": "20"},
|
|
||||||
"env": {},
|
|
||||||
"if": "success()",
|
|
||||||
"position": {"at": 2}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Position object options:**
|
### steps remove
|
||||||
```json
|
|
||||||
{"at": 3} // Insert at position 3
|
```
|
||||||
{"after": 2} // Insert after step 2
|
steps remove <index> [--output text|json]
|
||||||
{"before": 4} // Insert before step 4
|
|
||||||
{"first": true} // Insert at first pending position
|
|
||||||
{"last": true} // Insert at end (default if omitted)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### !step edit
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
steps remove 5
|
||||||
|
steps remove 5 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON response (`--output json`):**
|
||||||
|
```json
|
||||||
|
{"Success": true, "Message": "Step 5 removed"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### steps move
|
||||||
|
|
||||||
|
```
|
||||||
|
steps move <from> <position> [--output text|json]
|
||||||
|
|
||||||
|
Position (one required):
|
||||||
|
--to <index> Move to position
|
||||||
|
--after <index> Move after step
|
||||||
|
--before <index> Move before step
|
||||||
|
--first Move to first pending position
|
||||||
|
--last Move to end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
steps move 5 --after 2
|
||||||
|
steps move 5 --after 2 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON response (`--output json`):**
|
||||||
|
```json
|
||||||
|
{"Success": true, "Message": "Step moved to position 3"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### steps export
|
||||||
|
|
||||||
|
```
|
||||||
|
steps export [--changes-only] [--with-comments] [--output text|json]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
steps export --with-comments
|
||||||
|
steps export --changes-only --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON response (`--output json`):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Success": true,
|
||||||
|
"Result": {
|
||||||
|
"yaml": "steps:\n - name: Checkout\n uses: actions/checkout@v4\n...",
|
||||||
|
"totalSteps": 5,
|
||||||
|
"addedCount": 1,
|
||||||
|
"modifiedCount": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
!step edit <index> [modifications]
|
!step edit <index> [modifications]
|
||||||
|
|
||||||
@@ -1098,12 +1245,12 @@ Position (one required):
|
|||||||
These command names are reserved for future implementation:
|
These command names are reserved for future implementation:
|
||||||
|
|
||||||
```
|
```
|
||||||
!step duplicate <index> # Clone a step
|
steps duplicate <index> # Clone a step
|
||||||
!step enable <index> # Re-enable a disabled step
|
steps enable <index> # Re-enable a disabled step
|
||||||
!step disable <index> # Skip step without removing
|
steps disable <index> # Skip step without removing
|
||||||
!step inspect <index> # Show detailed step info
|
steps inspect <index> # Show detailed step info
|
||||||
!step reset <index> # Revert modifications
|
steps reset <index> # Revert modifications
|
||||||
!step import # Add steps from YAML
|
steps import # Add steps from YAML
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote a string for use in command, escaping as needed
|
||||||
|
*/
|
||||||
|
function quoteString(str) {
|
||||||
|
// Escape backslashes and quotes, wrap in quotes
|
||||||
|
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip result indicator suffix from step name
|
* Strip result indicator suffix from step name
|
||||||
* e.g., "Run tests [running]" -> "Run tests"
|
* e.g., "Run tests [running]" -> "Run tests"
|
||||||
@@ -510,7 +518,7 @@ function renderStepList(steps) {
|
|||||||
async function loadSteps() {
|
async function loadSteps() {
|
||||||
try {
|
try {
|
||||||
const response = await sendDapRequest('evaluate', {
|
const response = await sendDapRequest('evaluate', {
|
||||||
expression: JSON.stringify({ cmd: 'step.list', verbose: false }),
|
expression: 'steps list --output json',
|
||||||
frameId: currentFrameId,
|
frameId: currentFrameId,
|
||||||
context: 'repl',
|
context: 'repl',
|
||||||
});
|
});
|
||||||
@@ -553,13 +561,122 @@ function enableStepControls(enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send step command via JSON API
|
* Build REPL command string from action and options
|
||||||
*/
|
*/
|
||||||
async function sendStepCommand(cmd, options = {}) {
|
function buildStepCommand(action, options) {
|
||||||
const command = { cmd, ...options };
|
let cmd;
|
||||||
|
switch (action) {
|
||||||
|
case 'step.list':
|
||||||
|
cmd = options.verbose ? 'steps list --verbose' : 'steps list';
|
||||||
|
break;
|
||||||
|
case 'step.add':
|
||||||
|
cmd = buildAddStepCommand(options);
|
||||||
|
break;
|
||||||
|
case 'step.edit':
|
||||||
|
cmd = buildEditStepCommand(options);
|
||||||
|
break;
|
||||||
|
case 'step.remove':
|
||||||
|
cmd = `steps remove ${options.index}`;
|
||||||
|
break;
|
||||||
|
case 'step.move':
|
||||||
|
cmd = buildMoveStepCommand(options);
|
||||||
|
break;
|
||||||
|
case 'step.export':
|
||||||
|
cmd = buildExportCommand(options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown step command: ${action}`);
|
||||||
|
}
|
||||||
|
// Always request JSON output for programmatic use
|
||||||
|
return cmd + ' --output json';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build add step command string
|
||||||
|
*/
|
||||||
|
function buildAddStepCommand(options) {
|
||||||
|
let cmd = 'steps add';
|
||||||
|
|
||||||
|
if (options.type === 'run') {
|
||||||
|
cmd += ` run ${quoteString(options.script)}`;
|
||||||
|
if (options.shell) cmd += ` --shell ${options.shell}`;
|
||||||
|
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
|
||||||
|
} else if (options.type === 'uses') {
|
||||||
|
cmd += ` uses ${options.action}`;
|
||||||
|
if (options.with) {
|
||||||
|
for (const [key, value] of Object.entries(options.with)) {
|
||||||
|
cmd += ` --with ${key}=${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
||||||
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
||||||
|
if (options.env) {
|
||||||
|
for (const [key, value] of Object.entries(options.env)) {
|
||||||
|
cmd += ` --env ${key}=${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.continueOnError) cmd += ' --continue-on-error';
|
||||||
|
if (options.timeout) cmd += ` --timeout ${options.timeout}`;
|
||||||
|
|
||||||
|
// Position
|
||||||
|
if (options.position) {
|
||||||
|
if (options.position.after !== undefined) cmd += ` --after ${options.position.after}`;
|
||||||
|
else if (options.position.before !== undefined) cmd += ` --before ${options.position.before}`;
|
||||||
|
else if (options.position.at !== undefined) cmd += ` --at ${options.position.at}`;
|
||||||
|
else if (options.position.first) cmd += ' --first';
|
||||||
|
// --last is default, no need to specify
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build edit step command string
|
||||||
|
*/
|
||||||
|
function buildEditStepCommand(options) {
|
||||||
|
let cmd = `steps edit ${options.index}`;
|
||||||
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
||||||
|
if (options.script) cmd += ` --script ${quoteString(options.script)}`;
|
||||||
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
||||||
|
if (options.shell) cmd += ` --shell ${options.shell}`;
|
||||||
|
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build move step command string
|
||||||
|
*/
|
||||||
|
function buildMoveStepCommand(options) {
|
||||||
|
let cmd = `steps move ${options.from}`;
|
||||||
|
const pos = options.position;
|
||||||
|
if (pos.after !== undefined) cmd += ` --after ${pos.after}`;
|
||||||
|
else if (pos.before !== undefined) cmd += ` --before ${pos.before}`;
|
||||||
|
else if (pos.at !== undefined) cmd += ` --to ${pos.at}`;
|
||||||
|
else if (pos.first) cmd += ' --first';
|
||||||
|
else if (pos.last) cmd += ' --last';
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build export command string
|
||||||
|
*/
|
||||||
|
function buildExportCommand(options) {
|
||||||
|
let cmd = 'steps export';
|
||||||
|
if (options.changesOnly) cmd += ' --changes-only';
|
||||||
|
if (options.withComments) cmd += ' --with-comments';
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send step command via REPL format
|
||||||
|
*/
|
||||||
|
async function sendStepCommand(action, options = {}) {
|
||||||
|
const expression = buildStepCommand(action, options);
|
||||||
try {
|
try {
|
||||||
const response = await sendDapRequest('evaluate', {
|
const response = await sendDapRequest('evaluate', {
|
||||||
expression: JSON.stringify(command),
|
expression,
|
||||||
frameId: currentFrameId,
|
frameId: currentFrameId,
|
||||||
context: 'repl',
|
context: 'repl',
|
||||||
});
|
});
|
||||||
@@ -568,7 +685,8 @@ async function sendStepCommand(cmd, options = {}) {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(response.result);
|
return JSON.parse(response.result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { Success: false, Error: 'PARSE_ERROR', Message: response.result };
|
// Response might be plain text for non-JSON output
|
||||||
|
return { Success: true, Message: response.result };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { Success: false, Error: 'NO_RESPONSE', Message: 'No response from server' };
|
return { Success: false, Error: 'NO_RESPONSE', Message: 'No response from server' };
|
||||||
@@ -1141,7 +1259,8 @@ async function handleReplKeydown(e) {
|
|||||||
const response = await sendDapRequest('evaluate', {
|
const response = await sendDapRequest('evaluate', {
|
||||||
expression: command,
|
expression: command,
|
||||||
frameId: currentFrameId,
|
frameId: currentFrameId,
|
||||||
context: command.startsWith('!') ? 'repl' : 'watch',
|
// Use 'repl' context for shell commands (!) and step commands
|
||||||
|
context: (command.startsWith('!') || command.startsWith('steps')) ? 'repl' : 'watch',
|
||||||
});
|
});
|
||||||
// Only show result if it's NOT an exit code summary
|
// Only show result if it's NOT an exit code summary
|
||||||
// (shell command output is already streamed via output events)
|
// (shell command output is already streamed via output events)
|
||||||
|
|||||||
@@ -1138,8 +1138,8 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
// Return appropriate response format based on input type
|
// Return appropriate response format based on output type
|
||||||
if (command.WasJsonInput)
|
if (command.Output == OutputFormat.Json)
|
||||||
{
|
{
|
||||||
return CreateSuccessResponse(new EvaluateResponseBody
|
return CreateSuccessResponse(new EvaluateResponseBody
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,45 +80,78 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
catch (StepCommandException ex)
|
catch (StepCommandException ex)
|
||||||
{
|
{
|
||||||
return ex.ToResult();
|
return FormatErrorResult(ex.ErrorCode, ex.Message, command.Output);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Error($"Step command failed: {ex}");
|
Trace.Error($"Step command failed: {ex}");
|
||||||
return StepCommandResult.Fail(StepCommandErrors.InvalidCommand, ex.Message);
|
return FormatErrorResult(StepCommandErrors.InvalidCommand, ex.Message, command.Output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats an error result based on the requested output format.
|
||||||
|
/// </summary>
|
||||||
|
private StepCommandResult FormatErrorResult(string errorCode, string message, OutputFormat outputFormat)
|
||||||
|
{
|
||||||
|
if (outputFormat == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Error = errorCode,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = false, Error = errorCode, Message = message })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return StepCommandResult.Fail(errorCode, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Command Handlers
|
#region Command Handlers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step list command.
|
/// Handles the steps list command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleList(ListCommand command)
|
private StepCommandResult HandleList(ListCommand command)
|
||||||
{
|
{
|
||||||
var steps = _manipulator.GetAllSteps();
|
var steps = _manipulator.GetAllSteps();
|
||||||
var output = FormatStepList(steps, command.Verbose);
|
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
steps = steps.Select(s => new
|
||||||
Message = output,
|
|
||||||
Result = new
|
|
||||||
{
|
{
|
||||||
steps = steps.Select(s => new
|
index = s.Index,
|
||||||
{
|
name = s.Name,
|
||||||
index = s.Index,
|
type = s.Type,
|
||||||
name = s.Name,
|
typeDetail = s.TypeDetail,
|
||||||
type = s.Type,
|
status = s.Status.ToString().ToLower(),
|
||||||
typeDetail = s.TypeDetail,
|
change = s.Change?.ToString().ToUpper()
|
||||||
status = s.Status.ToString().ToLower(),
|
}).ToList(),
|
||||||
change = s.Change?.ToString().ToUpper()
|
totalCount = steps.Count,
|
||||||
}).ToList(),
|
completedCount = steps.Count(s => s.Status == StepStatus.Completed),
|
||||||
totalCount = steps.Count,
|
pendingCount = steps.Count(s => s.Status == StepStatus.Pending)
|
||||||
completedCount = steps.Count(s => s.Status == StepStatus.Completed),
|
|
||||||
pendingCount = steps.Count(s => s.Status == StepStatus.Pending)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = FormatStepList(steps, command.Verbose),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -190,7 +223,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step add run command.
|
/// Handles the steps add run command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleAddRun(AddRunCommand command, IExecutionContext jobContext)
|
private StepCommandResult HandleAddRun(AddRunCommand command, IExecutionContext jobContext)
|
||||||
{
|
{
|
||||||
@@ -216,26 +249,43 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
Trace.Info($"Added run step '{actionStep.DisplayName}' at position {index}");
|
Trace.Info($"Added run step '{actionStep.DisplayName}' at position {index}");
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
index,
|
||||||
Message = $"Step added at position {index}: {actionStep.DisplayName}",
|
step = new
|
||||||
Result = new
|
|
||||||
{
|
{
|
||||||
index,
|
name = stepInfo.Name,
|
||||||
step = new
|
type = stepInfo.Type,
|
||||||
{
|
typeDetail = stepInfo.TypeDetail,
|
||||||
name = stepInfo.Name,
|
status = stepInfo.Status.ToString().ToLower(),
|
||||||
type = stepInfo.Type,
|
change = stepInfo.Change?.ToString().ToUpper()
|
||||||
typeDetail = stepInfo.TypeDetail,
|
|
||||||
status = stepInfo.Status.ToString().ToLower()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var textMessage = $"Step added at position {index}: {actionStep.DisplayName}";
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Message = textMessage, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = textMessage,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step add uses command with action download integration.
|
/// Handles the steps add uses command with action download integration.
|
||||||
/// Downloads the action via IActionManager and handles pre/post steps.
|
/// Downloads the action via IActionManager and handles pre/post steps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<StepCommandResult> HandleAddUsesAsync(AddUsesCommand command, IExecutionContext jobContext)
|
private async Task<StepCommandResult> HandleAddUsesAsync(AddUsesCommand command, IExecutionContext jobContext)
|
||||||
@@ -380,29 +430,46 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
// - Enforce enterprise policies
|
// - Enforce enterprise policies
|
||||||
// For now, allow all actions in prototype
|
// For now, allow all actions in prototype
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
index = mainStepIndex,
|
||||||
Message = messageBuilder.ToString(),
|
preStepIndex,
|
||||||
Result = new
|
hasPreStep = preStepIndex.HasValue,
|
||||||
|
step = new
|
||||||
{
|
{
|
||||||
index = mainStepIndex,
|
name = stepInfo.Name,
|
||||||
preStepIndex,
|
type = stepInfo.Type,
|
||||||
hasPreStep = preStepIndex.HasValue,
|
typeDetail = stepInfo.TypeDetail,
|
||||||
step = new
|
status = stepInfo.Status.ToString().ToLower(),
|
||||||
{
|
change = stepInfo.Change?.ToString().ToUpper()
|
||||||
name = stepInfo.Name,
|
},
|
||||||
type = stepInfo.Type,
|
actionDownloaded = !isLocalOrDocker
|
||||||
typeDetail = stepInfo.TypeDetail,
|
|
||||||
status = stepInfo.Status.ToString().ToLower()
|
|
||||||
},
|
|
||||||
actionDownloaded = !isLocalOrDocker
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var textMessage = messageBuilder.ToString();
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Message = textMessage, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = textMessage,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step edit command.
|
/// Handles the steps edit command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleEdit(EditCommand command)
|
private StepCommandResult HandleEdit(EditCommand command)
|
||||||
{
|
{
|
||||||
@@ -529,21 +596,36 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
});
|
});
|
||||||
|
|
||||||
var changesStr = changes.Count > 0 ? string.Join(", ", changes) : "no changes";
|
var changesStr = changes.Count > 0 ? string.Join(", ", changes) : "no changes";
|
||||||
|
var resultData = new
|
||||||
return new StepCommandResult
|
|
||||||
{
|
{
|
||||||
Success = true,
|
index = command.Index,
|
||||||
Message = $"Step {command.Index} updated ({changesStr})",
|
changes
|
||||||
Result = new
|
|
||||||
{
|
|
||||||
index = command.Index,
|
|
||||||
changes
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var textMessage = $"Step {command.Index} updated ({changesStr})";
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Message = textMessage, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = textMessage,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step remove command.
|
/// Handles the steps remove command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleRemove(RemoveCommand command)
|
private StepCommandResult HandleRemove(RemoveCommand command)
|
||||||
{
|
{
|
||||||
@@ -556,20 +638,36 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
Trace.Info($"Removed step '{stepName}' from position {command.Index}");
|
Trace.Info($"Removed step '{stepName}' from position {command.Index}");
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
index = command.Index,
|
||||||
Message = $"Step {command.Index} removed: {stepName}",
|
removedStep = stepName
|
||||||
Result = new
|
|
||||||
{
|
|
||||||
index = command.Index,
|
|
||||||
removedStep = stepName
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var textMessage = $"Step {command.Index} removed: {stepName}";
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Message = textMessage, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = textMessage,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step move command.
|
/// Handles the steps move command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleMove(MoveCommand command)
|
private StepCommandResult HandleMove(MoveCommand command)
|
||||||
{
|
{
|
||||||
@@ -582,21 +680,37 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
Trace.Info($"Moved step '{stepName}' from position {command.FromIndex} to {newIndex}");
|
Trace.Info($"Moved step '{stepName}' from position {command.FromIndex} to {newIndex}");
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
fromIndex = command.FromIndex,
|
||||||
Message = $"Step moved from position {command.FromIndex} to {newIndex}: {stepName}",
|
toIndex = newIndex,
|
||||||
Result = new
|
stepName
|
||||||
{
|
|
||||||
fromIndex = command.FromIndex,
|
|
||||||
toIndex = newIndex,
|
|
||||||
stepName
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var textMessage = $"Step moved from position {command.FromIndex} to {newIndex}: {stepName}";
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Message = textMessage, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = textMessage,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the !step export command.
|
/// Handles the steps export command.
|
||||||
/// Generates YAML output for modified steps with optional change comments.
|
/// Generates YAML output for modified steps with optional change comments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleExport(ExportCommand command)
|
private StepCommandResult HandleExport(ExportCommand command)
|
||||||
@@ -616,21 +730,35 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
var yaml = _serializer.ToYaml(toExport, command.WithComments);
|
var yaml = _serializer.ToYaml(toExport, command.WithComments);
|
||||||
|
|
||||||
return new StepCommandResult
|
var resultData = new
|
||||||
{
|
{
|
||||||
Success = true,
|
yaml,
|
||||||
Message = yaml,
|
totalSteps = steps.Count,
|
||||||
Result = new
|
exportedSteps = toExport.Count(),
|
||||||
{
|
addedCount = changes.Count(c => c.Type == ChangeType.Added),
|
||||||
yaml,
|
modifiedCount = changes.Count(c => c.Type == ChangeType.Modified),
|
||||||
totalSteps = steps.Count,
|
movedCount = changes.Count(c => c.Type == ChangeType.Moved),
|
||||||
exportedSteps = toExport.Count(),
|
removedCount = changes.Count(c => c.Type == ChangeType.Removed)
|
||||||
addedCount = changes.Count(c => c.Type == ChangeType.Added),
|
|
||||||
modifiedCount = changes.Count(c => c.Type == ChangeType.Modified),
|
|
||||||
movedCount = changes.Count(c => c.Type == ChangeType.Moved),
|
|
||||||
removedCount = changes.Count(c => c.Type == ChangeType.Removed)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (command.Output == OutputFormat.Json)
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = JsonConvert.SerializeObject(new { Success = true, Result = resultData }),
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = yaml,
|
||||||
|
Result = resultData
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -2,20 +2,30 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap.StepCommands
|
namespace GitHub.Runner.Worker.Dap.StepCommands
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for parsing step commands from REPL strings or JSON.
|
/// Output format for step command responses.
|
||||||
|
/// </summary>
|
||||||
|
public enum OutputFormat
|
||||||
|
{
|
||||||
|
/// <summary>Human-readable text output (default)</summary>
|
||||||
|
Text,
|
||||||
|
/// <summary>JSON output for programmatic use</summary>
|
||||||
|
Json
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for parsing step commands from REPL strings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ServiceLocator(Default = typeof(StepCommandParser))]
|
[ServiceLocator(Default = typeof(StepCommandParser))]
|
||||||
public interface IStepCommandParser : IRunnerService
|
public interface IStepCommandParser : IRunnerService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a command string (REPL or JSON) into a structured StepCommand.
|
/// Parses a command string into a structured StepCommand.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input string (e.g., "!step list --verbose" or JSON)</param>
|
/// <param name="input">The input string (e.g., "steps list --verbose")</param>
|
||||||
/// <returns>Parsed StepCommand</returns>
|
/// <returns>Parsed StepCommand</returns>
|
||||||
/// <exception cref="StepCommandException">If parsing fails</exception>
|
/// <exception cref="StepCommandException">If parsing fails</exception>
|
||||||
StepCommand Parse(string input);
|
StepCommand Parse(string input);
|
||||||
@@ -24,7 +34,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// Checks if the input is a step command.
|
/// Checks if the input is a step command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input string to check</param>
|
/// <param name="input">The input string to check</param>
|
||||||
/// <returns>True if this is a step command (REPL or JSON format)</returns>
|
/// <returns>True if this is a step command (starts with "steps")</returns>
|
||||||
bool IsStepCommand(string input);
|
bool IsStepCommand(string input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,13 +46,13 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public abstract class StepCommand
|
public abstract class StepCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the original input was JSON (affects response format).
|
/// Output format for the command response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool WasJsonInput { get; set; }
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step list [--verbose]
|
/// steps list [--verbose]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ListCommand : StepCommand
|
public class ListCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -50,7 +60,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step add run "script" [options]
|
/// steps add run "script" [options]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddRunCommand : StepCommand
|
public class AddRunCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -66,7 +76,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step add uses "action@ref" [options]
|
/// steps add uses "action@ref" [options]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AddUsesCommand : StepCommand
|
public class AddUsesCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -81,7 +91,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step edit <index> [modifications]
|
/// steps edit <index> [modifications]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EditCommand : StepCommand
|
public class EditCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -101,7 +111,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step remove <index>
|
/// steps remove <index>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RemoveCommand : StepCommand
|
public class RemoveCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -109,7 +119,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step move <from> [position options]
|
/// steps move <from> [position options]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MoveCommand : StepCommand
|
public class MoveCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -118,7 +128,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// !step export [--changes-only] [--with-comments]
|
/// steps export [--changes-only] [--with-comments]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExportCommand : StepCommand
|
public class ExportCommand : StepCommand
|
||||||
{
|
{
|
||||||
@@ -178,7 +188,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parser implementation for step commands (REPL and JSON formats).
|
/// Parser implementation for step commands.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class StepCommandParser : RunnerService, IStepCommandParser
|
public sealed class StepCommandParser : RunnerService, IStepCommandParser
|
||||||
{
|
{
|
||||||
@@ -194,12 +204,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
var trimmed = input.Trim();
|
var trimmed = input.Trim();
|
||||||
|
|
||||||
// REPL command format: !step ...
|
// Command format: steps ...
|
||||||
if (trimmed.StartsWith("!step", StringComparison.OrdinalIgnoreCase))
|
if (trimmed.StartsWith("steps ", StringComparison.OrdinalIgnoreCase) ||
|
||||||
return true;
|
trimmed.Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
||||||
// JSON format: {"cmd": "step.*", ...}
|
|
||||||
if (trimmed.StartsWith("{") && trimmed.Contains("\"cmd\"") && trimmed.Contains("\"step."))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -208,239 +215,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public StepCommand Parse(string input)
|
public StepCommand Parse(string input)
|
||||||
{
|
{
|
||||||
var trimmed = input?.Trim() ?? "";
|
var trimmed = input?.Trim() ?? "";
|
||||||
|
return ParseReplCommand(trimmed);
|
||||||
if (trimmed.StartsWith("{"))
|
|
||||||
{
|
|
||||||
return ParseJsonCommand(trimmed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ParseReplCommand(trimmed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region JSON Parsing
|
|
||||||
|
|
||||||
private StepCommand ParseJsonCommand(string json)
|
|
||||||
{
|
|
||||||
JObject obj;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
obj = JObject.Parse(json);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, $"Invalid JSON: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd = obj["cmd"]?.ToString();
|
|
||||||
if (string.IsNullOrEmpty(cmd))
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'cmd' field in JSON");
|
|
||||||
}
|
|
||||||
|
|
||||||
StepCommand result = cmd switch
|
|
||||||
{
|
|
||||||
"step.list" => ParseJsonListCommand(obj),
|
|
||||||
"step.add" => ParseJsonAddCommand(obj),
|
|
||||||
"step.edit" => ParseJsonEditCommand(obj),
|
|
||||||
"step.remove" => ParseJsonRemoveCommand(obj),
|
|
||||||
"step.move" => ParseJsonMoveCommand(obj),
|
|
||||||
"step.export" => ParseJsonExportCommand(obj),
|
|
||||||
_ => throw new StepCommandException(StepCommandErrors.InvalidCommand, $"Unknown command: {cmd}")
|
|
||||||
};
|
|
||||||
|
|
||||||
result.WasJsonInput = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListCommand ParseJsonListCommand(JObject obj)
|
|
||||||
{
|
|
||||||
return new ListCommand
|
|
||||||
{
|
|
||||||
Verbose = obj["verbose"]?.Value<bool>() ?? false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private StepCommand ParseJsonAddCommand(JObject obj)
|
|
||||||
{
|
|
||||||
var type = obj["type"]?.ToString()?.ToLower();
|
|
||||||
|
|
||||||
if (type == "run")
|
|
||||||
{
|
|
||||||
var script = obj["script"]?.ToString();
|
|
||||||
if (string.IsNullOrEmpty(script))
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'script' field for run step");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AddRunCommand
|
|
||||||
{
|
|
||||||
Script = script,
|
|
||||||
Name = obj["name"]?.ToString(),
|
|
||||||
Shell = obj["shell"]?.ToString(),
|
|
||||||
WorkingDirectory = obj["workingDirectory"]?.ToString(),
|
|
||||||
Env = ParseJsonDictionary(obj["env"]),
|
|
||||||
Condition = obj["if"]?.ToString(),
|
|
||||||
ContinueOnError = obj["continueOnError"]?.Value<bool>() ?? false,
|
|
||||||
Timeout = obj["timeout"]?.Value<int>(),
|
|
||||||
Position = ParseJsonPosition(obj["position"])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (type == "uses")
|
|
||||||
{
|
|
||||||
var action = obj["action"]?.ToString();
|
|
||||||
if (string.IsNullOrEmpty(action))
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'action' field for uses step");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AddUsesCommand
|
|
||||||
{
|
|
||||||
Action = action,
|
|
||||||
Name = obj["name"]?.ToString(),
|
|
||||||
With = ParseJsonDictionary(obj["with"]),
|
|
||||||
Env = ParseJsonDictionary(obj["env"]),
|
|
||||||
Condition = obj["if"]?.ToString(),
|
|
||||||
ContinueOnError = obj["continueOnError"]?.Value<bool>() ?? false,
|
|
||||||
Timeout = obj["timeout"]?.Value<int>(),
|
|
||||||
Position = ParseJsonPosition(obj["position"])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.InvalidType,
|
|
||||||
$"Invalid step type: '{type}'. Must be 'run' or 'uses'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private EditCommand ParseJsonEditCommand(JObject obj)
|
|
||||||
{
|
|
||||||
var index = obj["index"]?.Value<int>();
|
|
||||||
if (!index.HasValue)
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'index' field for edit command");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EditCommand
|
|
||||||
{
|
|
||||||
Index = index.Value,
|
|
||||||
Name = obj["name"]?.ToString(),
|
|
||||||
Script = obj["script"]?.ToString(),
|
|
||||||
Action = obj["action"]?.ToString(),
|
|
||||||
Shell = obj["shell"]?.ToString(),
|
|
||||||
WorkingDirectory = obj["workingDirectory"]?.ToString(),
|
|
||||||
Condition = obj["if"]?.ToString(),
|
|
||||||
With = ParseJsonDictionary(obj["with"]),
|
|
||||||
Env = ParseJsonDictionary(obj["env"]),
|
|
||||||
RemoveWith = ParseJsonStringList(obj["removeWith"]),
|
|
||||||
RemoveEnv = ParseJsonStringList(obj["removeEnv"]),
|
|
||||||
ContinueOnError = obj["continueOnError"]?.Value<bool>(),
|
|
||||||
Timeout = obj["timeout"]?.Value<int>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private RemoveCommand ParseJsonRemoveCommand(JObject obj)
|
|
||||||
{
|
|
||||||
var index = obj["index"]?.Value<int>();
|
|
||||||
if (!index.HasValue)
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'index' field for remove command");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RemoveCommand { Index = index.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
private MoveCommand ParseJsonMoveCommand(JObject obj)
|
|
||||||
{
|
|
||||||
var from = obj["from"]?.Value<int>();
|
|
||||||
if (!from.HasValue)
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'from' field for move command");
|
|
||||||
}
|
|
||||||
|
|
||||||
var position = ParseJsonPosition(obj["position"]);
|
|
||||||
if (position.Type == PositionType.Last)
|
|
||||||
{
|
|
||||||
// Default 'last' is fine for add, but move needs explicit position
|
|
||||||
// unless explicitly set
|
|
||||||
var posObj = obj["position"];
|
|
||||||
if (posObj == null)
|
|
||||||
{
|
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError, "Missing 'position' field for move command");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MoveCommand
|
|
||||||
{
|
|
||||||
FromIndex = from.Value,
|
|
||||||
Position = position
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExportCommand ParseJsonExportCommand(JObject obj)
|
|
||||||
{
|
|
||||||
return new ExportCommand
|
|
||||||
{
|
|
||||||
ChangesOnly = obj["changesOnly"]?.Value<bool>() ?? false,
|
|
||||||
WithComments = obj["withComments"]?.Value<bool>() ?? false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private StepPosition ParseJsonPosition(JToken token)
|
|
||||||
{
|
|
||||||
if (token == null || token.Type == JTokenType.Null)
|
|
||||||
return StepPosition.Last();
|
|
||||||
|
|
||||||
if (token.Type == JTokenType.Object)
|
|
||||||
{
|
|
||||||
var obj = (JObject)token;
|
|
||||||
|
|
||||||
if (obj["at"] != null)
|
|
||||||
return StepPosition.At(obj["at"].Value<int>());
|
|
||||||
if (obj["after"] != null)
|
|
||||||
return StepPosition.After(obj["after"].Value<int>());
|
|
||||||
if (obj["before"] != null)
|
|
||||||
return StepPosition.Before(obj["before"].Value<int>());
|
|
||||||
if (obj["first"]?.Value<bool>() == true)
|
|
||||||
return StepPosition.First();
|
|
||||||
if (obj["last"]?.Value<bool>() == true)
|
|
||||||
return StepPosition.Last();
|
|
||||||
}
|
|
||||||
|
|
||||||
return StepPosition.Last();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> ParseJsonDictionary(JToken token)
|
|
||||||
{
|
|
||||||
if (token == null || token.Type != JTokenType.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>();
|
|
||||||
foreach (var prop in ((JObject)token).Properties())
|
|
||||||
{
|
|
||||||
result[prop.Name] = prop.Value?.ToString() ?? "";
|
|
||||||
}
|
|
||||||
return result.Count > 0 ? result : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> ParseJsonStringList(JToken token)
|
|
||||||
{
|
|
||||||
if (token == null || token.Type != JTokenType.Array)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var result = new List<string>();
|
|
||||||
foreach (var item in (JArray)token)
|
|
||||||
{
|
|
||||||
var str = item?.ToString();
|
|
||||||
if (!string.IsNullOrEmpty(str))
|
|
||||||
result.Add(str);
|
|
||||||
}
|
|
||||||
return result.Count > 0 ? result : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region REPL Parsing
|
#region REPL Parsing
|
||||||
|
|
||||||
private StepCommand ParseReplCommand(string input)
|
private StepCommand ParseReplCommand(string input)
|
||||||
@@ -448,10 +225,10 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
// Tokenize the input, respecting quoted strings
|
// Tokenize the input, respecting quoted strings
|
||||||
var tokens = Tokenize(input);
|
var tokens = Tokenize(input);
|
||||||
|
|
||||||
if (tokens.Count < 2 || !tokens[0].Equals("!step", StringComparison.OrdinalIgnoreCase))
|
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Invalid command format. Expected: !step <command> [args...]");
|
"Invalid command format. Expected: steps <command> [args...]");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subCommand = tokens[1].ToLower();
|
var subCommand = tokens[1].ToLower();
|
||||||
@@ -510,7 +287,10 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private ListCommand ParseReplListCommand(List<string> tokens)
|
private ListCommand ParseReplListCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
var cmd = new ListCommand();
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
|
var cmd = new ListCommand { Output = outputFormat };
|
||||||
|
|
||||||
for (int i = 2; i < tokens.Count; i++)
|
for (int i = 2; i < tokens.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -531,36 +311,43 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private StepCommand ParseReplAddCommand(List<string> tokens)
|
private StepCommand ParseReplAddCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
if (tokens.Count < 3)
|
if (tokens.Count < 3)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step add <run|uses> <script|action> [options]");
|
"Usage: steps add <run|uses> <script|action> [options]");
|
||||||
}
|
}
|
||||||
|
|
||||||
var type = tokens[2].ToLower();
|
var type = tokens[2].ToLower();
|
||||||
|
|
||||||
|
StepCommand cmd;
|
||||||
if (type == "run")
|
if (type == "run")
|
||||||
{
|
{
|
||||||
return ParseReplAddRunCommand(tokens);
|
cmd = ParseReplAddRunCommand(tokens);
|
||||||
}
|
}
|
||||||
else if (type == "uses")
|
else if (type == "uses")
|
||||||
{
|
{
|
||||||
return ParseReplAddUsesCommand(tokens);
|
cmd = ParseReplAddUsesCommand(tokens);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.InvalidType,
|
throw new StepCommandException(StepCommandErrors.InvalidType,
|
||||||
$"Invalid step type: '{type}'. Must be 'run' or 'uses'.");
|
$"Invalid step type: '{type}'. Must be 'run' or 'uses'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Output = outputFormat;
|
||||||
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AddRunCommand ParseReplAddRunCommand(List<string> tokens)
|
private AddRunCommand ParseReplAddRunCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
// !step add run "script" [options]
|
// steps add run "script" [options]
|
||||||
if (tokens.Count < 4)
|
if (tokens.Count < 4)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step add run \"<script>\" [--name \"...\"] [--shell <shell>] [--at|--after|--before <n>]");
|
"Usage: steps add run \"<script>\" [--name \"...\"] [--shell <shell>] [--at|--after|--before <n>]");
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd = new AddRunCommand
|
var cmd = new AddRunCommand
|
||||||
@@ -626,11 +413,11 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private AddUsesCommand ParseReplAddUsesCommand(List<string> tokens)
|
private AddUsesCommand ParseReplAddUsesCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
// !step add uses "action@ref" [options]
|
// steps add uses "action@ref" [options]
|
||||||
if (tokens.Count < 4)
|
if (tokens.Count < 4)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step add uses <action@ref> [--name \"...\"] [--with key=value] [--at|--after|--before <n>]");
|
"Usage: steps add uses <action@ref> [--name \"...\"] [--with key=value] [--at|--after|--before <n>]");
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd = new AddUsesCommand
|
var cmd = new AddUsesCommand
|
||||||
@@ -696,11 +483,14 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private EditCommand ParseReplEditCommand(List<string> tokens)
|
private EditCommand ParseReplEditCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
// !step edit <index> [modifications]
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
|
// steps edit <index> [modifications]
|
||||||
if (tokens.Count < 3)
|
if (tokens.Count < 3)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step edit <index> [--name \"...\"] [--script \"...\"] [--if \"...\"]");
|
"Usage: steps edit <index> [--name \"...\"] [--script \"...\"] [--if \"...\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!int.TryParse(tokens[2], out var index))
|
if (!int.TryParse(tokens[2], out var index))
|
||||||
@@ -712,6 +502,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
var cmd = new EditCommand
|
var cmd = new EditCommand
|
||||||
{
|
{
|
||||||
Index = index,
|
Index = index,
|
||||||
|
Output = outputFormat,
|
||||||
With = new Dictionary<string, string>(),
|
With = new Dictionary<string, string>(),
|
||||||
Env = new Dictionary<string, string>(),
|
Env = new Dictionary<string, string>(),
|
||||||
RemoveWith = new List<string>(),
|
RemoveWith = new List<string>(),
|
||||||
@@ -785,11 +576,14 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private RemoveCommand ParseReplRemoveCommand(List<string> tokens)
|
private RemoveCommand ParseReplRemoveCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
// !step remove <index>
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
|
// steps remove <index>
|
||||||
if (tokens.Count < 3)
|
if (tokens.Count < 3)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step remove <index>");
|
"Usage: steps remove <index>");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!int.TryParse(tokens[2], out var index))
|
if (!int.TryParse(tokens[2], out var index))
|
||||||
@@ -798,16 +592,19 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
$"Invalid index: {tokens[2]}. Must be a number.");
|
$"Invalid index: {tokens[2]}. Must be a number.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RemoveCommand { Index = index };
|
return new RemoveCommand { Index = index, Output = outputFormat };
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoveCommand ParseReplMoveCommand(List<string> tokens)
|
private MoveCommand ParseReplMoveCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
// !step move <from> --to|--after|--before <index>|--first|--last
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
|
// steps move <from> --to|--after|--before <index>|--first|--last
|
||||||
if (tokens.Count < 3)
|
if (tokens.Count < 3)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Usage: !step move <from> --to|--after|--before <index>|--first|--last");
|
"Usage: steps move <from> --to|--after|--before <index>|--first|--last");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!int.TryParse(tokens[2], out var fromIndex))
|
if (!int.TryParse(tokens[2], out var fromIndex))
|
||||||
@@ -816,7 +613,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
$"Invalid from index: {tokens[2]}. Must be a number.");
|
$"Invalid from index: {tokens[2]}. Must be a number.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd = new MoveCommand { FromIndex = fromIndex };
|
var cmd = new MoveCommand { FromIndex = fromIndex, Output = outputFormat };
|
||||||
|
|
||||||
// Parse position
|
// Parse position
|
||||||
for (int i = 3; i < tokens.Count; i++)
|
for (int i = 3; i < tokens.Count; i++)
|
||||||
@@ -858,7 +655,10 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
private ExportCommand ParseReplExportCommand(List<string> tokens)
|
private ExportCommand ParseReplExportCommand(List<string> tokens)
|
||||||
{
|
{
|
||||||
var cmd = new ExportCommand();
|
// Extract --output flag before processing other options
|
||||||
|
var outputFormat = ExtractOutputFlag(tokens);
|
||||||
|
|
||||||
|
var cmd = new ExportCommand { Output = outputFormat };
|
||||||
|
|
||||||
for (int i = 2; i < tokens.Count; i++)
|
for (int i = 2; i < tokens.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -883,6 +683,36 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
#region Argument Helpers
|
#region Argument Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts and removes the --output flag from tokens, returning the output format.
|
||||||
|
/// Supports: --output json, --output text, -o json, -o text, --output=json, --output=text
|
||||||
|
/// </summary>
|
||||||
|
private OutputFormat ExtractOutputFlag(List<string> tokens)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < tokens.Count; i++)
|
||||||
|
{
|
||||||
|
var token = tokens[i].ToLower();
|
||||||
|
|
||||||
|
if (token == "--output" || token == "-o")
|
||||||
|
{
|
||||||
|
if (i + 1 < tokens.Count)
|
||||||
|
{
|
||||||
|
var format = tokens[i + 1].ToLower();
|
||||||
|
tokens.RemoveAt(i); // Remove flag
|
||||||
|
tokens.RemoveAt(i); // Remove value (now at same index)
|
||||||
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token.StartsWith("--output="))
|
||||||
|
{
|
||||||
|
var format = token.Substring("--output=".Length);
|
||||||
|
tokens.RemoveAt(i);
|
||||||
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OutputFormat.Text;
|
||||||
|
}
|
||||||
|
|
||||||
private string GetNextArg(List<string> tokens, ref int index, string optName)
|
private string GetNextArg(List<string> tokens, ref int index, string optName)
|
||||||
{
|
{
|
||||||
if (index + 1 >= tokens.Count)
|
if (index + 1 >= tokens.Count)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
726
src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs
Normal file
726
src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user