use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn hashmap_is_empty<K, V>(m: &HashMap<K, V>) -> bool {
m.is_empty()
}
fn vec_is_empty<T>(v: &[T]) -> bool {
v.is_empty()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ApiContentType {
#[serde(rename = "application/json")]
ApplicationJson,
#[serde(rename = "application/x-www-form-urlencoded")]
ApplicationFormUrlEncoded,
#[serde(rename = "text/plain")]
TextPlain,
#[serde(rename = "none")]
None,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ApiVariableExtraction {
#[serde(alias = "variable_name")]
pub variable_name: String,
#[serde(alias = "json_path")]
pub json_path: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "default_value"
)]
pub default_value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ApiAssertionType {
StatusCode,
JsonPath,
Header,
BodyContains,
ResponseTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ApiAssertionOperator {
Equals,
Contains,
Matches,
GreaterThan,
LessThan,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ApiAssertion {
#[serde(rename = "type", alias = "assertion_type")]
pub assertion_type: ApiAssertionType,
#[serde(alias = "expected")]
pub expected: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "json_path")]
pub json_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "header_name"
)]
pub header_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "operator")]
pub operator: Option<ApiAssertionOperator>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TestType {
Playwright,
QontinuiVision,
Python,
Repository,
CustomCommand,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PlaywrightExecutionMode {
Independent,
Chained,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CheckType {
Lint,
Format,
Typecheck,
Analyze,
Security,
CustomCommand,
HttpStatus,
AiReview,
CiCd,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VerificationCategoryKind {
Existence,
Uniqueness,
ReferentialIntegrity,
SemanticCorrectness,
RuntimeBehavior,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct RetrySpec {
#[serde(alias = "count")]
pub count: u32,
#[serde(alias = "delay_ms")]
pub delay_ms: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct BaseStepFields {
#[serde(alias = "id")]
pub id: String,
#[serde(alias = "name")]
pub name: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fail_on_console_errors"
)]
pub fail_on_console_errors: Option<bool>,
#[serde(default, skip_serializing_if = "hashmap_is_empty", alias = "inputs")]
pub inputs: HashMap<String, String>,
#[serde(default, skip_serializing_if = "hashmap_is_empty", alias = "extract")]
pub extract: HashMap<String, String>,
#[serde(default, skip_serializing_if = "vec_is_empty", alias = "depends_on")]
pub depends_on: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "required")]
pub required: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "retry")]
pub retry: Option<RetrySpec>,
#[serde(default, skip_serializing_if = "vec_is_empty", alias = "criterion_ids")]
pub criterion_ids: Vec<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "verification_category"
)]
pub verification_category: Option<VerificationCategoryKind>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "skill_origin"
)]
pub skill_origin: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CommandStepPhase {
#[default]
Setup,
Verification,
Completion,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PromptStepPhase {
#[default]
Setup,
Verification,
Agentic,
Completion,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UiBridgeStepPhase {
#[default]
Setup,
Verification,
Completion,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowStepPhase {
#[default]
Setup,
Verification,
Completion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CommandMode {
Shell,
Check,
CheckGroup,
Test,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CommandStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(alias = "phase")]
pub phase: CommandStepPhase,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "mode")]
pub mode: Option<CommandMode>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "command")]
pub command: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "working_directory"
)]
pub working_directory: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fail_on_error"
)]
pub fail_on_error: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "run_on_subsequent_iterations"
)]
pub run_on_subsequent_iterations: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "shell_command_id"
)]
pub shell_command_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "check_type")]
pub check_type: Option<CheckType>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "tool")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "check_id")]
pub check_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "config_path"
)]
pub config_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "auto_fix")]
pub auto_fix: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fail_on_warning"
)]
pub fail_on_warning: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "repository")]
pub repository: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "workflow_name"
)]
pub workflow_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "branch")]
pub branch: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "wait_for_completion"
)]
pub wait_for_completion: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "check_group_id"
)]
pub check_group_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "test_type")]
pub test_type: Option<TestType>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "test_id")]
pub test_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "code")]
pub code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "script_id")]
pub script_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "script_content"
)]
pub script_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "target_url")]
pub target_url: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fused_script_id"
)]
pub fused_script_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "execution_mode"
)]
pub execution_mode: Option<PlaywrightExecutionMode>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PromptStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(alias = "phase")]
pub phase: PromptStepPhase,
#[serde(alias = "content")]
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "prompt_id")]
pub prompt_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "provider")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "model")]
pub model: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "is_summary_step"
)]
pub is_summary_step: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UiBridgeAction {
#[default]
Navigate,
Execute,
Assert,
Snapshot,
Compare,
SnapshotAssert,
ActionPlan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UiBridgeAssertType {
Exists,
TextEquals,
Contains,
Visible,
Enabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UiBridgeComparisonMode {
Structural,
Visual,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum UiBridgeSeverity {
Critical,
Major,
Minor,
Info,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct UiBridgeStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(alias = "phase")]
pub phase: UiBridgeStepPhase,
#[serde(alias = "action")]
pub action: UiBridgeAction,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "url")]
pub url: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "instruction"
)]
pub instruction: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "target")]
pub target: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "assert_type"
)]
pub assert_type: Option<UiBridgeAssertType>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "expected")]
pub expected: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "timeout_ms")]
pub timeout_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "comparison_mode"
)]
pub comparison_mode: Option<UiBridgeComparisonMode>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "reference_snapshot_id"
)]
pub reference_snapshot_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "severity_threshold"
)]
pub severity_threshold: Option<UiBridgeSeverity>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "ui_bridge_snapshot_target"
)]
pub ui_bridge_snapshot_target: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "action_plan"
)]
pub action_plan: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(alias = "phase")]
pub phase: WorkflowStepPhase,
#[serde(alias = "workflow_id")]
pub workflow_id: String,
#[serde(alias = "workflow_name")]
pub workflow_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum CanonicalStep {
Command(CommandStep),
Prompt(PromptStep),
UiBridge(UiBridgeStep),
Workflow(WorkflowStep),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum UnifiedStep {
Canonical(CanonicalStep),
Other(serde_json::Value),
}
impl UnifiedStep {
pub fn command(step: CommandStep) -> Self {
Self::Canonical(CanonicalStep::Command(step))
}
pub fn prompt(step: PromptStep) -> Self {
Self::Canonical(CanonicalStep::Prompt(step))
}
pub fn ui_bridge(step: UiBridgeStep) -> Self {
Self::Canonical(CanonicalStep::UiBridge(step))
}
pub fn workflow(step: WorkflowStep) -> Self {
Self::Canonical(CanonicalStep::Workflow(step))
}
pub fn as_canonical(&self) -> Option<&CanonicalStep> {
match self {
Self::Canonical(c) => Some(c),
Self::Other(_) => None,
}
}
pub fn step_type(&self) -> Option<&str> {
match self {
Self::Canonical(CanonicalStep::Command(_)) => Some("command"),
Self::Canonical(CanonicalStep::Prompt(_)) => Some("prompt"),
Self::Canonical(CanonicalStep::UiBridge(_)) => Some("ui_bridge"),
Self::Canonical(CanonicalStep::Workflow(_)) => Some("workflow"),
Self::Other(v) => v.get("type").and_then(|t| t.as_str()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CodeExecutionStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "code")]
pub code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "code_file")]
pub code_file: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "sandbox_mode"
)]
pub sandbox_mode: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExecutePlaybookStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "content")]
pub content: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "playbook_path"
)]
pub playbook_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum A11yAction {
#[default]
Capture,
Click,
Type,
Focus,
Query,
AiContext,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct NativeAccessibilityStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(default, alias = "action")]
pub action: A11yAction,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "target")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "ref_id")]
pub ref_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "text")]
pub text: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "clear_first"
)]
pub clear_first: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "query_role")]
pub query_role: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "query_label"
)]
pub query_label: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "interactive_only"
)]
pub interactive_only: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "max_elements"
)]
pub max_elements: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "include_hidden"
)]
pub include_hidden: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "max_depth")]
pub max_depth: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RestartProcessStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "restart_process_id"
)]
pub restart_process_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "restart_process_name"
)]
pub restart_process_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "restart_wait_for_health"
)]
pub restart_wait_for_health: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SaveWorkflowArtifactStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "artifact_input_path"
)]
pub artifact_input_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "artifact_capture_prompts"
)]
pub artifact_capture_prompts: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowFixupMode {
#[default]
Autofix,
Harden,
ValidateCriteria,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowFixupStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fixup_input_path"
)]
pub fixup_input_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "fixup_mode")]
pub fixup_mode: Option<WorkflowFixupMode>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "fixup_criteria_path"
)]
pub fixup_criteria_path: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct UiBridgeDesignAuditStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VgaActionKind {
#[default]
Click,
Type,
WaitFor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VgaAction {
Click {
#[serde(rename = "elementId", alias = "element_id")]
element_id: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "timeoutMs",
alias = "timeout_ms"
)]
timeout_ms: Option<u64>,
},
Type {
#[serde(alias = "text")]
text: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "elementId",
alias = "element_id"
)]
element_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "timeoutMs",
alias = "timeout_ms"
)]
timeout_ms: Option<u64>,
},
WaitFor {
#[serde(rename = "elementId", alias = "element_id")]
element_id: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "timeoutMs",
alias = "timeout_ms"
)]
timeout_ms: Option<u64>,
},
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VgaAutomateStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(alias = "state_machine_id")]
pub state_machine_id: String,
#[serde(alias = "target_process")]
pub target_process: String,
#[serde(
default,
skip_serializing_if = "vec_is_empty",
alias = "action_sequence"
)]
pub action_sequence: Vec<VgaAction>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "timeout_ms")]
pub timeout_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "async")]
pub r#async: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VisualAssertionType {
#[default]
Text,
Screenshot,
Highlight,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct UiBridgeVisualAssertionStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "visual_assertion_type"
)]
pub visual_assertion_type: Option<VisualAssertionType>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "visual_assertion_query"
)]
pub visual_assertion_query: Option<serde_json::Value>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "visual_assertion_expected"
)]
pub visual_assertion_expected: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "visual_assertion_options"
)]
pub visual_assertion_options: Option<serde_json::Value>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowRefStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "workflow_id"
)]
pub workflow_id: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "ref_workflow_name"
)]
pub ref_workflow_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "ref_workflow_inputs"
)]
pub ref_workflow_inputs: Option<HashMap<String, String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "ref_inherit_model_overrides"
)]
pub ref_inherit_model_overrides: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DagCancelStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "cancel_reason"
)]
pub cancel_reason: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DagApprovalStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "approval_prompt"
)]
pub approval_prompt: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "timeout_seconds"
)]
pub timeout_seconds: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DagLoopStep {
#[serde(flatten)]
pub base: BaseStepFields,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "max_iterations"
)]
pub max_iterations: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "loop_condition"
)]
pub loop_condition: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum FullRunnerStep {
Command(CommandStep),
Prompt(PromptStep),
UiBridge(UiBridgeStep),
Workflow(WorkflowStep),
CodeExecution(CodeExecutionStep),
ExecutePlaybook(ExecutePlaybookStep),
NativeAccessibility(NativeAccessibilityStep),
RestartProcess(RestartProcessStep),
SaveWorkflowArtifact(SaveWorkflowArtifactStep),
WorkflowFixup(WorkflowFixupStep),
UiBridgeDesignAudit(UiBridgeDesignAuditStep),
UiBridgeVisualAssertion(UiBridgeVisualAssertionStep),
VgaAutomate(VgaAutomateStep),
WorkflowRef(WorkflowRefStep),
DagCancel(DagCancelStep),
DagApproval(DagApprovalStep),
DagLoop(DagLoopStep),
}
impl FullRunnerStep {
pub fn step_type(&self) -> &'static str {
match self {
Self::Command(_) => "command",
Self::Prompt(_) => "prompt",
Self::UiBridge(_) => "ui_bridge",
Self::Workflow(_) => "workflow",
Self::CodeExecution(_) => "code_execution",
Self::ExecutePlaybook(_) => "execute_playbook",
Self::NativeAccessibility(_) => "native_accessibility",
Self::RestartProcess(_) => "restart_process",
Self::SaveWorkflowArtifact(_) => "save_workflow_artifact",
Self::WorkflowFixup(_) => "workflow_fixup",
Self::UiBridgeDesignAudit(_) => "ui_bridge_design_audit",
Self::UiBridgeVisualAssertion(_) => "ui_bridge_visual_assertion",
Self::VgaAutomate(_) => "vga_automate",
Self::WorkflowRef(_) => "workflow_ref",
Self::DagCancel(_) => "dag_cancel",
Self::DagApproval(_) => "dag_approval",
Self::DagLoop(_) => "dag_loop",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, Value};
fn base(id: &str, name: &str) -> BaseStepFields {
BaseStepFields {
id: id.to_string(),
name: name.to_string(),
..Default::default()
}
}
fn roundtrip<T>(value: &T) -> T
where
T: Serialize + for<'de> Deserialize<'de>,
{
let json = serde_json::to_string(value).expect("serialize");
serde_json::from_str(&json).expect("deserialize")
}
fn assert_type_tag(json: &Value, expected: &str) {
assert_eq!(
json.get("type").and_then(|t| t.as_str()),
Some(expected),
"expected type tag {:?}, got: {}",
expected,
json
);
}
#[test]
fn command_step_round_trip() {
let step = FullRunnerStep::Command(CommandStep {
base: base("s1", "Build"),
phase: CommandStepPhase::Setup,
mode: Some(CommandMode::Shell),
command: Some("cargo build".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "command");
assert_eq!(json["mode"], "shell");
assert_eq!(json["command"], "cargo build");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn prompt_step_round_trip() {
let step = FullRunnerStep::Prompt(PromptStep {
base: base("p1", "Agentic Task"),
phase: PromptStepPhase::Agentic,
content: "Fix the failing tests.".into(),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "prompt");
assert_eq!(json["phase"], "agentic");
assert_eq!(json["content"], "Fix the failing tests.");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn ui_bridge_step_round_trip() {
let step = FullRunnerStep::UiBridge(UiBridgeStep {
base: base("u1", "Navigate"),
phase: UiBridgeStepPhase::Setup,
action: UiBridgeAction::Navigate,
url: Some("http://localhost:3000".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "ui_bridge");
assert_eq!(json["action"], "navigate");
assert_eq!(json["url"], "http://localhost:3000");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn workflow_step_round_trip() {
let step = FullRunnerStep::Workflow(WorkflowStep {
base: base("w1", "Inner Workflow"),
phase: WorkflowStepPhase::Setup,
workflow_id: "wf-uuid-123".into(),
workflow_name: "My Workflow".into(),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "workflow");
assert_eq!(json["workflowId"], "wf-uuid-123");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn code_execution_step_round_trip() {
let step = FullRunnerStep::CodeExecution(CodeExecutionStep {
base: base("ce1", "Run Script"),
code: Some("print('hello')".into()),
sandbox_mode: Some("enforce".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "code_execution");
assert_eq!(json["code"], "print('hello')");
assert_eq!(json["sandboxMode"], "enforce");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn execute_playbook_step_round_trip() {
let step = FullRunnerStep::ExecutePlaybook(ExecutePlaybookStep {
base: base("ep1", "Drive State Machine"),
playbook_path: Some("/artifacts/playbook.md".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "execute_playbook");
assert_eq!(json["playbookPath"], "/artifacts/playbook.md");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn native_accessibility_step_round_trip() {
let step = FullRunnerStep::NativeAccessibility(NativeAccessibilityStep {
base: base("na1", "Click Button"),
action: A11yAction::Click,
target: Some("Desktop".into()),
ref_id: Some("@e5".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "native_accessibility");
assert_eq!(json["action"], "click");
assert_eq!(json["refId"], "@e5");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn restart_process_step_round_trip() {
let step = FullRunnerStep::RestartProcess(RestartProcessStep {
base: base("rp1", "Restart Backend"),
restart_process_name: Some("backend".into()),
restart_wait_for_health: Some(true),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "restart_process");
assert_eq!(json["restartProcessName"], "backend");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn save_workflow_artifact_step_round_trip() {
let step = FullRunnerStep::SaveWorkflowArtifact(SaveWorkflowArtifactStep {
base: base("swa1", "Save Generated Workflow"),
artifact_input_path: Some("{{artifact_dir}}/workflow.json".into()),
artifact_capture_prompts: Some(true),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "save_workflow_artifact");
assert_eq!(json["artifactInputPath"], "{{artifact_dir}}/workflow.json");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn workflow_fixup_step_round_trip() {
let step = FullRunnerStep::WorkflowFixup(WorkflowFixupStep {
base: base("wf1", "Auto-fix Workflow"),
fixup_input_path: Some("{{artifact_dir}}/workflow.json".into()),
fixup_mode: Some(WorkflowFixupMode::Harden),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "workflow_fixup");
assert_eq!(json["fixupMode"], "harden");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn ui_bridge_design_audit_step_round_trip() {
let step = FullRunnerStep::UiBridgeDesignAudit(UiBridgeDesignAuditStep {
base: base("da1", "Design Audit"),
timeout_seconds: Some(30),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "ui_bridge_design_audit");
assert_eq!(json["timeoutSeconds"], 30);
assert_eq!(roundtrip(&step), step);
}
#[test]
fn vga_automate_step_round_trip() {
let step = FullRunnerStep::VgaAutomate(VgaAutomateStep {
base: base("vga1", "Drive Notepad++"),
state_machine_id: "11111111-1111-1111-1111-111111111111".into(),
target_process: "notepad++.exe".into(),
action_sequence: vec![
VgaAction::WaitFor {
element_id: "22222222-2222-2222-2222-222222222222".into(),
timeout_ms: Some(30000),
},
VgaAction::Click {
element_id: "22222222-2222-2222-2222-222222222222".into(),
timeout_ms: None,
},
VgaAction::Type {
text: "hello world".into(),
element_id: None,
timeout_ms: Some(10000),
},
],
timeout_ms: Some(300000),
r#async: Some(false),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "vga_automate");
assert_eq!(
json["stateMachineId"],
"11111111-1111-1111-1111-111111111111"
);
assert_eq!(json["targetProcess"], "notepad++.exe");
assert_eq!(json["actionSequence"][0]["kind"], "wait_for");
assert_eq!(json["actionSequence"][1]["kind"], "click");
assert_eq!(json["actionSequence"][2]["kind"], "type");
assert_eq!(json["actionSequence"][2]["text"], "hello world");
assert_eq!(json["timeoutMs"], 300000);
assert_eq!(json["async"], false);
assert_eq!(roundtrip(&step), step);
}
#[test]
fn ui_bridge_visual_assertion_step_round_trip() {
let step = FullRunnerStep::UiBridgeVisualAssertion(UiBridgeVisualAssertionStep {
base: base("va1", "Assert Button Text"),
visual_assertion_type: Some(VisualAssertionType::Text),
visual_assertion_expected: Some("Submit".into()),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "ui_bridge_visual_assertion");
assert_eq!(json["visualAssertionType"], "text");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn workflow_ref_step_round_trip() {
let step = FullRunnerStep::WorkflowRef(WorkflowRefStep {
base: base("wr1", "Call Sub-Workflow"),
workflow_id: Some("child-wf-uuid".into()),
ref_inherit_model_overrides: Some(true),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "workflow_ref");
assert_eq!(json["workflowId"], "child-wf-uuid");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn dag_cancel_step_round_trip() {
let step = FullRunnerStep::DagCancel(DagCancelStep {
base: base("dc1", "Cancel on Failure"),
cancel_reason: Some("Build failed".into()),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "dag_cancel");
assert_eq!(json["cancelReason"], "Build failed");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn dag_approval_step_round_trip() {
let step = FullRunnerStep::DagApproval(DagApprovalStep {
base: base("da1", "Approval Gate"),
approval_prompt: Some("Approve deployment?".into()),
timeout_seconds: Some(3600),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "dag_approval");
assert_eq!(json["approvalPrompt"], "Approve deployment?");
assert_eq!(roundtrip(&step), step);
}
#[test]
fn dag_loop_step_round_trip() {
let step = FullRunnerStep::DagLoop(DagLoopStep {
base: base("dl1", "Retry Loop"),
max_iterations: Some(5),
loop_condition: Some("not_passing".into()),
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "dag_loop");
assert_eq!(json["maxIterations"], 5);
assert_eq!(roundtrip(&step), step);
}
#[test]
fn value_bridge_command_shell() {
let raw = json!({
"type": "command",
"id": "abc",
"name": "Build",
"phase": "setup",
"mode": "shell",
"command": "npm install"
});
let step: FullRunnerStep = serde_json::from_value(raw.clone()).expect("deserialize");
assert_eq!(step.step_type(), "command");
let back = serde_json::to_value(&step).unwrap();
assert_eq!(back["type"], "command");
assert_eq!(back["command"], "npm install");
}
#[test]
fn value_bridge_prompt_agentic() {
let raw = json!({
"type": "prompt",
"id": "p1",
"name": "Fix Tests",
"phase": "agentic",
"content": "Please fix the failing tests."
});
let step: FullRunnerStep = serde_json::from_value(raw).expect("deserialize");
assert_eq!(step.step_type(), "prompt");
}
#[test]
fn value_bridge_ui_bridge_navigate() {
let raw = json!({
"type": "ui_bridge",
"id": "u1",
"name": "Open Dashboard",
"phase": "setup",
"action": "navigate",
"url": "http://localhost:3000/dashboard"
});
let step: FullRunnerStep = serde_json::from_value(raw).expect("deserialize");
assert_eq!(step.step_type(), "ui_bridge");
}
#[test]
fn value_bridge_code_execution() {
let raw = json!({
"type": "code_execution",
"id": "ce1",
"name": "Setup Data",
"code": "import os; print(os.getcwd())"
});
let step: FullRunnerStep = serde_json::from_value(raw).expect("deserialize");
assert_eq!(step.step_type(), "code_execution");
}
#[test]
fn value_bridge_dag_approval() {
let raw = json!({
"type": "dag_approval",
"id": "appr1",
"name": "Deploy Approval",
"approval_prompt": "Ready to deploy to production?"
});
let step: FullRunnerStep = serde_json::from_value(raw).expect("deserialize");
assert_eq!(step.step_type(), "dag_approval");
}
#[test]
fn unknown_type_tag_returns_error() {
let raw = json!({
"type": "totally_unknown_step_xyz",
"id": "x1",
"name": "Mystery Step"
});
let result: Result<FullRunnerStep, _> = serde_json::from_value(raw);
assert!(
result.is_err(),
"Expected error for unknown type tag, got: {:?}",
result
);
}
#[test]
fn canonical_step_non_regression() {
let step = CanonicalStep::Command(CommandStep {
base: base("c1", "Check"),
phase: CommandStepPhase::Verification,
mode: Some(CommandMode::Check),
check_type: Some(CheckType::Lint),
..Default::default()
});
let json = serde_json::to_value(&step).unwrap();
assert_type_tag(&json, "command");
assert_eq!(json["mode"], "check");
}
#[test]
fn unified_step_other_round_trips_verbatim() {
let raw = json!({
"type": "log_watch",
"id": "lw1",
"name": "Watch Logs",
"custom_field": 42
});
let step: UnifiedStep = serde_json::from_value(raw.clone()).expect("deserialize");
assert!(matches!(&step, UnifiedStep::Other(_)));
assert_eq!(step.step_type(), Some("log_watch"));
let back = serde_json::to_value(&step).unwrap();
assert_eq!(back, raw);
}
}