diff --git a/README.md b/README.md index 67d6a2d7..43bf0da2 100644 --- a/README.md +++ b/README.md @@ -87,11 +87,14 @@ The demo shows: - building a deterministic plan from a workflow definition (`.psd1`) - executing the plan using a host-provided step registry +The execution result buffers all emitted events in `result.Events`. Hosts can optionally stream events live +by providing `-EventSink` as an object implementing `WriteEvent(event)`. + Next steps: -- Usage & examples: `docs/02-examples.md` -- Architecture: `docs/01-architecture.md` +- Documentation entry point: `docs/index.md` - Workflow samples: `examples/workflows/` +- Repository demo: `examples/run-demo.ps1` - Pester tests: `tests/` --- @@ -100,9 +103,10 @@ Next steps: Start here: -- `docs/00-index.md` – documentation map -- `docs/01-architecture.md` – architecture and principles -- `docs/02-examples.md` – runnable examples + workflow snippets +- `docs/index.md` – documentation map +- `docs/getting-started/quickstart.md` – plan → execute walkthrough +- `docs/advanced/architecture.md` – architecture and principles +- `docs/usage/workflows.md` – workflow schema and validation Project docs: diff --git a/docs/_config.yml b/docs/_config.yml index 69932bc0..afe82f28 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -5,6 +5,7 @@ markdown: kramdown kramdown: input: GFM hard_wrap: false +logo: "/assets/idle_logo_flat_white.png" plugins: [] header_pages: - overview/concept.md diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index ee62786d..6aea307e 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -42,6 +42,20 @@ This enables previews, approvals, and repeatable audits. Conditions are data-only objects. They are validated early and evaluated deterministically. +## Eventing + +IdLE emits **structured events** during execution. + +- The engine always creates an `EventSink` and exposes it as `Context.EventSink`. +- Steps and the engine use a single contract: `Context.EventSink.WriteEvent(Type, Message, StepName, Data)`. +- All events are buffered in the execution result (`result.Events`). + +Hosts may optionally provide an external sink to stream events live: + +- `Invoke-IdlePlan -EventSink ` +- The sink must implement `WriteEvent(event)` +- ScriptBlock sinks are rejected (secure default) + ## State ownership Steps may only write to `State.*` and only to declared output paths. diff --git a/docs/advanced/extensibility.md b/docs/advanced/extensibility.md index 5562d6fc..2a13c99b 100644 --- a/docs/advanced/extensibility.md +++ b/docs/advanced/extensibility.md @@ -11,6 +11,12 @@ A new step typically involves: 3. An execution function (invoke) that performs actions via providers 4. Unit tests (Pester) +Steps can emit structured events using the execution context contract: + +- `Context.EventSink.WriteEvent(Type, Message, StepName, Data)` + +Keep steps host-agnostic: do not call UI APIs directly. + ## Add a new provider A new provider typically involves: diff --git a/docs/assets/idle_logo_flat_white.png b/docs/assets/idle_logo_flat_white.png new file mode 100644 index 00000000..71e3d9ae Binary files /dev/null and b/docs/assets/idle_logo_flat_white.png differ diff --git a/docs/assets/idle_logo_flat_white_text.png b/docs/assets/idle_logo_flat_white_text.png new file mode 100644 index 00000000..c4cba2e8 Binary files /dev/null and b/docs/assets/idle_logo_flat_white_text.png differ diff --git a/docs/assets/idle_logo_text.png b/docs/assets/idle_logo_text.png new file mode 100644 index 00000000..c5f3e56d Binary files /dev/null and b/docs/assets/idle_logo_text.png differ diff --git a/docs/assets/idle_logo_tranparent.png b/docs/assets/idle_logo_tranparent.png new file mode 100644 index 00000000..466275bb Binary files /dev/null and b/docs/assets/idle_logo_tranparent.png differ diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index fc474542..18a6a997 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -17,7 +17,7 @@ pwsh -File .\examples\run-demo.ps1 ## Minimal plan and execute ```powershell -$workflowPath = Join-Path $PSScriptRoot 'workflows\joiner-with-when.psd1' +$workflowPath = Join-Path (Get-Location) 'examples\workflows\joiner-with-when.psd1' $request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user' $plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request @@ -26,6 +26,38 @@ $providers = @{} $result = Invoke-IdlePlan -Plan $plan -Providers $providers ``` +## Inspect results and events + +Execution returns a result object containing step results and buffered events: + +```powershell +$result.Status +$result.Steps +$result.Events | Select-Object Type, StepName, Message +``` + +## Optional: stream events with -EventSink + +If a host wants live progress, it can provide an **object** event sink. +The sink must implement `WriteEvent(event)`. + +> Security note: ScriptBlock sinks are not supported. + +Example: + +```powershell +$streamed = [System.Collections.Generic.List[object]]::new() + +$sink = [pscustomobject]@{} +$null = Add-Member -InputObject $sink -MemberType ScriptMethod -Name 'WriteEvent' -Value { + param($e) + [void]$streamed.Add($e) + Write-Host ("[{0}] {1}" -f $e.Type, $e.Message) +} + +$result = Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sink +``` + ## Next steps - [Workflows](../usage/workflows.md) diff --git a/docs/usage/steps.md b/docs/usage/steps.md index 599c7f65..df095e58 100644 --- a/docs/usage/steps.md +++ b/docs/usage/steps.md @@ -31,6 +31,39 @@ Avoid executing code from configuration. Keep inputs data-only. Steps may write to `State.*` only, and only to declared output paths. This prevents hidden coupling between steps. +## Eventing + +Steps may emit **structured events** for progress and audit. + +The engine provides a stable, object-based contract on the execution context: + +- `Context.EventSink.WriteEvent(Type, Message, StepName, Data)` + +Notes: + +- `Type` is a short string (for example: `Custom`, `Debug`). +- `Message` is a human-readable message. +- `StepName` should be the current step name (if available). +- `Data` is an optional hashtable for structured details. + +Example: + +```powershell +$Context.EventSink.WriteEvent( + 'Custom', + 'Ensured Department attribute.', + $Step.Name, + @{ Provider = 'Identity'; Attribute = 'Department' } +) +``` + +Security and portability: + +- Steps must never execute code from configuration. +- Steps must not assume a specific host UI. +- Hosts can optionally stream events via `Invoke-IdlePlan -EventSink `, + but **ScriptBlock sinks are not supported**. + ## Error behavior IdLE uses a fail-fast execution model in V1: diff --git a/docs/usage/workflows.md b/docs/usage/workflows.md index fa0c9039..4df3a317 100644 --- a/docs/usage/workflows.md +++ b/docs/usage/workflows.md @@ -4,17 +4,22 @@ Workflows are **data-only** configuration files (PSD1) describing which steps sh ## Format -A workflow is a PowerShell hashtable stored as `.psd1`: +A workflow is a PowerShell hashtable stored as `.psd1`. + +Workflow definitions are **data-only**. Do not embed executable code. + +Example: ```powershell @{ Name = 'Joiner - Standard' LifecycleEvent = 'Joiner' + Steps = @( @{ - Name = 'Emit:Start' - Step = 'EmitEvent' - Inputs = @{ Message = 'Starting Joiner' } + Name = 'Emit start' + Type = 'IdLE.Step.EmitEvent' + With = @{ Message = 'Starting Joiner' } } ) } @@ -31,6 +36,13 @@ Typical validation rules: - condition schemas must be valid - `*From` paths must reference allowed roots +## Step identifiers + +Step types are treated as **contracts**. Prefer fully-qualified ids (module + step name), +for example: `IdLE.Step.EmitEvent`. + +The host maps step types to step implementations via a step registry. + ## Conditional steps Steps can be skipped using declarative `When` conditions. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..a33952b3 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,51 @@ +# Examples + +This folder contains runnable examples for IdLE. + +## Run the demo + +From the repository root: + +```powershell +pwsh -File .\examples\run-demo.ps1 +``` + +The demo: + +- builds a plan from a workflow (`.psd1`) +- executes the plan using mock providers +- prints step results and buffered events + +## Workflow samples + +Workflow samples are located in: + +- `examples/workflows/` + +Workflows are **data-only** PSD1 files. A minimal workflow looks like: + +```powershell +@{ + Name = 'Joiner - Minimal Demo' + LifecycleEvent = 'Joiner' + Steps = @( + @{ + Name = 'EmitHello' + Type = 'IdLE.Step.EmitEvent' + With = @{ Message = 'Hello from workflow.' } + } + ) +} +``` + +For details, see `docs/usage/workflows.md`. + +## Events + +IdLE buffers all emitted events in the execution result: + +```powershell +$result.Events | Select-Object Type, StepName, Message +``` + +Hosts can optionally stream events live by providing `-EventSink` as an object implementing `WriteEvent(event)`. diff --git a/src/IdLE.Core/Private/New-IdleEventSink.ps1 b/src/IdLE.Core/Private/New-IdleEventSink.ps1 new file mode 100644 index 00000000..dd7adb62 --- /dev/null +++ b/src/IdLE.Core/Private/New-IdleEventSink.ps1 @@ -0,0 +1,78 @@ +function New-IdleEventSink { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $CorrelationId, + + [Parameter()] + [AllowNull()] + [string] $Actor, + + [Parameter()] + [AllowNull()] + [object] $ExternalEventSink, + + [Parameter()] + [AllowNull()] + [System.Collections.Generic.List[object]] $EventBuffer + ) + + # External sinks are host-provided extension points. + # We validate strictly to keep the engine deterministic and to avoid code execution. + if ($null -ne $ExternalEventSink) { + if ($ExternalEventSink -is [scriptblock]) { + throw [System.ArgumentException]::new( + 'ExternalEventSink must not be a ScriptBlock. Provide an object with a WriteEvent(event) method.', + 'ExternalEventSink' + ) + } + + if (-not ($ExternalEventSink.PSObject.Methods.Name -contains 'WriteEvent')) { + throw [System.ArgumentException]::new( + 'ExternalEventSink must provide a WriteEvent(event) method.', + 'ExternalEventSink' + ) + } + } + + # Capture command references once to avoid scope/name resolution issues inside script methods. + $newIdleEventCmd = Get-Command -Name 'New-IdleEvent' -CommandType Function -ErrorAction Stop + $writeIdleEventCmd = Get-Command -Name 'Write-IdleEvent' -CommandType Function -ErrorAction Stop + + $sink = [pscustomobject]@{ + PSTypeName = 'IdLE.EventSink' + CorrelationId = $CorrelationId + Actor = $Actor + } + + # Provide a stable, object-based contract to steps. + # Steps call: $Context.EventSink.WriteEvent(Type, Message, StepName, Data) + # The engine stays in control of event shape and buffering. + $writeMethod = { + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Type, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Message, + + [Parameter()] + [AllowNull()] + [string] $StepName, + + [Parameter()] + [AllowNull()] + [hashtable] $Data + ) + + $evt = & $newIdleEventCmd -Type $Type -Message $Message -CorrelationId $CorrelationId -Actor $Actor -StepName $StepName -Data $Data + & $writeIdleEventCmd -Event $evt -EventSink $ExternalEventSink -EventBuffer $EventBuffer + }.GetNewClosure() + + $null = Add-Member -InputObject $sink -MemberType ScriptMethod -Name 'WriteEvent' -Value $writeMethod -Force + + return $sink +} diff --git a/src/IdLE.Core/Private/Write-IdleEvent.ps1 b/src/IdLE.Core/Private/Write-IdleEvent.ps1 index 0ad9bdf7..57ca6ec9 100644 --- a/src/IdLE.Core/Private/Write-IdleEvent.ps1 +++ b/src/IdLE.Core/Private/Write-IdleEvent.ps1 @@ -14,18 +14,20 @@ function Write-IdleEvent { [System.Collections.Generic.List[object]] $EventBuffer ) - # If an event sink is provided, try to emit events immediately. - # Supported shapes: - # - ScriptBlock: invoked with the event as the only argument + # If an external event sink is provided, emit events immediately. + # Security note: we do NOT support ScriptBlock sinks to avoid arbitrary code execution. + # Supported shape: # - Object with method "WriteEvent": called as $EventSink.WriteEvent($Event) - # - If nothing is provided: do nothing (events can still be buffered) if ($null -ne $EventSink) { if ($EventSink -is [scriptblock]) { - & $EventSink $Event + throw [System.ArgumentException]::new('EventSink must not be a ScriptBlock. Provide an object with a WriteEvent(event) method.', 'EventSink') } - elseif ($EventSink.PSObject.Methods.Name -contains 'WriteEvent') { - $EventSink.WriteEvent($Event) + + if (-not ($EventSink.PSObject.Methods.Name -contains 'WriteEvent')) { + throw [System.ArgumentException]::new('EventSink must provide a WriteEvent(event) method.', 'EventSink') } + + $EventSink.WriteEvent($Event) } # Buffer events for return value / tests if requested. diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index c4c0438f..e01fa670 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -14,7 +14,7 @@ function Invoke-IdlePlanObject { Provider registry/collection (used for StepRegistry in this increment; passed through for future steps). .PARAMETER EventSink - Optional sink for event streaming. Can be a ScriptBlock or an object with a WriteEvent() method. + Optional external event sink for streaming. Must be an object with a WriteEvent(event) method. .OUTPUTS PSCustomObject (PSTypeName: IdLE.ExecutionResult) @@ -47,9 +47,9 @@ function Invoke-IdlePlanObject { $corr = [string]$Plan.CorrelationId $actor = if ($planProps -contains 'Actor') { [string]$Plan.Actor } else { $null } - # Capture command references once to avoid scope/name resolution issues inside closures. - $newIdleEventCmd = Get-Command -Name 'New-IdleEvent' -CommandType Function -ErrorAction Stop - $writeIdleEventCmd = Get-Command -Name 'Write-IdleEvent' -CommandType Function -ErrorAction Stop + # Create the engine-managed event sink object used by both the engine and steps. + # This keeps event shape deterministic and isolates host-provided sinks behind a single contract. + $engineEventSink = New-IdleEventSink -CorrelationId $corr -Actor $actor -ExternalEventSink $EventSink -EventBuffer $events # Resolve step types to PowerShell functions via a registry. # This decouples workflow "Type" strings from actual implementation functions. @@ -64,24 +64,13 @@ function Invoke-IdlePlanObject { Plan = $Plan Providers = $Providers - # Expose a single event writer for steps. - # The engine stays in control of event shape, sinks and buffering. - WriteEvent = { - param( - [Parameter(Mandatory)][string] $Type, - [Parameter(Mandatory)][string] $Message, - [Parameter()][AllowNull()][string] $StepName, - [Parameter()][AllowNull()][hashtable] $Data - ) - - # Use captured command references to avoid scope/name resolution issues in step handlers. - $evt = & $newIdleEventCmd -Type $Type -Message $Message -CorrelationId $corr -Actor $actor -StepName $StepName -Data $Data - & $writeIdleEventCmd -Event $evt -EventSink $EventSink -EventBuffer $events - }.GetNewClosure() + # Object-based, stable eventing contract. + # Steps and the engine call: $Context.EventSink.WriteEvent(...) + EventSink = $engineEventSink } # Emit run start event. - & $context.WriteEvent 'RunStarted' 'Plan execution started.' $null @{ + $context.EventSink.WriteEvent('RunStarted', 'Plan execution started.', $null, @{ LifecycleEvent = [string]$Plan.LifecycleEvent WorkflowName = if ($planProps -contains 'WorkflowName') { [string]$Plan.WorkflowName @@ -89,7 +78,7 @@ function Invoke-IdlePlanObject { $null } StepCount = @($Plan.Steps).Count - } + }) $stepResults = @() $failed = $false @@ -115,20 +104,20 @@ function Invoke-IdlePlanObject { Error = $null } - & $context.WriteEvent 'StepSkipped' "Step '$stepName' skipped (condition not met)." $stepName @{ + $context.EventSink.WriteEvent('StepSkipped', "Step '$stepName' skipped (condition not met).", $stepName, @{ StepType = $stepType Index = $i - } + }) $i++ continue } } - & $context.WriteEvent 'StepStarted' "Step '$stepName' started." $stepName @{ + $context.EventSink.WriteEvent('StepStarted', "Step '$stepName' started.", $stepName, @{ StepType = $stepType Index = $i - } + }) try { # Resolve implementation handler for this step type. @@ -151,10 +140,10 @@ function Invoke-IdlePlanObject { $stepResults += $stepResult - & $context.WriteEvent 'StepCompleted' "Step '$stepName' completed." $stepName @{ + $context.EventSink.WriteEvent('StepCompleted', "Step '$stepName' completed.", $stepName, @{ StepType = $stepType Index = $i - } + }) } catch { $failed = $true @@ -168,11 +157,11 @@ function Invoke-IdlePlanObject { Error = $err.Exception.Message } - & $context.WriteEvent 'StepFailed' "Step '$stepName' failed." $stepName @{ + $context.EventSink.WriteEvent('StepFailed', "Step '$stepName' failed.", $stepName, @{ StepType = $stepType Index = $i Error = $err.Exception.Message - } + }) # Fail-fast in this increment. break @@ -183,10 +172,10 @@ function Invoke-IdlePlanObject { $runStatus = if ($failed) { 'Failed' } else { 'Completed' } - & $context.WriteEvent 'RunCompleted' "Plan execution finished (status: $runStatus)." $null @{ + $context.EventSink.WriteEvent('RunCompleted', "Plan execution finished (status: $runStatus).", $null, @{ Status = $runStatus StepCount = @($Plan.Steps).Count - } + }) return [pscustomobject]@{ PSTypeName = 'IdLE.ExecutionResult' diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEmitEvent.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEmitEvent.ps1 index b1b8f5ce..b8dc1e63 100644 --- a/src/IdLE.Steps.Common/Public/Invoke-IdleStepEmitEvent.ps1 +++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepEmitEvent.ps1 @@ -5,10 +5,8 @@ function Invoke-IdleStepEmitEvent { .DESCRIPTION This step does not change external state. It emits a custom event message. - If the execution context provides an EventSink, the step will write to it. - If no EventSink is available, the step will still succeed (no-op). - - This keeps the step host-agnostic and safe to use in demos/tests. + The engine provides an EventSink on the execution context that the step can use + to write structured events. .PARAMETER Context Execution context created by IdLE.Core. @@ -42,42 +40,12 @@ function Invoke-IdleStepEmitEvent { "Custom event emitted by step '$([string]$Step.Name)'." } - # EventSink is optional. If it exists, it should accept an event object. - # We deliberately do not assume a specific method name on the context itself. - $sinkProp = $Context.PSObject.Properties['EventSink'] - if ($null -ne $sinkProp -and $null -ne $sinkProp.Value) { - - $eventObject = [pscustomobject]@{ - PSTypeName = 'IdLE.Event' - TimestampUtc = [DateTime]::UtcNow - Type = 'Custom' - StepName = [string]$Step.Name - Message = $message - Data = @{ - StepType = [string]$Step.Type - } - } - - # Support common sink shapes: - # - ScriptBlock: & $EventSink $event - # - Object with method 'Add' or 'Write' or 'Emit' - $sink = $sinkProp.Value - - if ($sink -is [scriptblock]) { - & $sink $eventObject - } - elseif ($sink.PSObject.Methods.Name -contains 'Add') { - $sink.Add($eventObject) - } - elseif ($sink.PSObject.Methods.Name -contains 'Write') { - $sink.Write($eventObject) - } - elseif ($sink.PSObject.Methods.Name -contains 'Emit') { - $sink.Emit($eventObject) - } - else { - # Sink is present but has an unknown shape -> do not fail the step. - # Host can decide how strictly it wants to enforce sink contract. + # The engine provides an EventSink contract with a WriteEvent(...) method. + # If the host is not interested in streaming events, the sink will still buffer events + # for the execution result. This keeps the step deterministic and host-agnostic. + if ($Context.PSObject.Properties.Name -contains 'EventSink' -and $null -ne $Context.EventSink) { + if ($Context.EventSink.PSObject.Methods.Name -contains 'WriteEvent') { + $Context.EventSink.WriteEvent('Custom', $message, [string]$Step.Name, @{ StepType = [string]$Step.Type }) } } diff --git a/src/IdLE/Public/Invoke-IdlePlan.ps1 b/src/IdLE/Public/Invoke-IdlePlan.ps1 index f2517eee..d798aae6 100644 --- a/src/IdLE/Public/Invoke-IdlePlan.ps1 +++ b/src/IdLE/Public/Invoke-IdlePlan.ps1 @@ -14,7 +14,7 @@ function Invoke-IdlePlan { Provider registry/collection passed through to execution. .PARAMETER EventSink - Optional event sink. Can be a ScriptBlock or an object with a WriteEvent() method. + Optional external event sink for streaming. Must be an object with a WriteEvent(event) method. .EXAMPLE Invoke-IdlePlan -Plan $plan -Providers $providers diff --git a/tests/Invoke-IdlePlan.Tests.ps1 b/tests/Invoke-IdlePlan.Tests.ps1 index 8ae825ab..fc35fc35 100644 --- a/tests/Invoke-IdlePlan.Tests.ps1 +++ b/tests/Invoke-IdlePlan.Tests.ps1 @@ -65,7 +65,7 @@ Describe 'Invoke-IdlePlan' { @($result.Events).Count | Should -Be 0 } - It 'can stream events to a ScriptBlock sink' { + It 'can stream events to an object sink with WriteEvent(event)' { $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' Set-Content -Path $wfPath -Encoding UTF8 -Value @' @{ @@ -98,18 +98,56 @@ Describe 'Invoke-IdlePlan' { } $sinkEvents = [System.Collections.Generic.List[object]]::new() - $sink = { + $sinkObject = [pscustomobject]@{} + $writeMethod = { param($e) [void]$sinkEvents.Add($e) }.GetNewClosure() + $null = Add-Member -InputObject $sinkObject -MemberType ScriptMethod -Name 'WriteEvent' -Value $writeMethod -Force - $result = Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sink + $result = Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sinkObject $sinkEvents.Count | Should -BeGreaterThan 0 $sinkEvents[0].PSTypeNames | Should -Contain 'IdLE.Event' $result.Events[0].Type | Should -Be 'RunStarted' } + It 'rejects a ScriptBlock -EventSink (security)' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'ResolveIdentity'; Type = 'IdLE.Step.ResolveIdentity' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req + + $noop = { + param($Context, $Step) + [pscustomobject]@{ + PSTypeName = 'IdLE.StepResult' + Name = [string]$Step.Name + Type = [string]$Step.Type + Status = 'Completed' + Error = $null + } + } + + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.ResolveIdentity' = $noop + } + } + + $sink = { param($e) } + { Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sink } | Should -Throw + } + It 'executes a registered step and returns Completed status' { $wfPath = Join-Path -Path $TestDrive -ChildPath 'emit.psd1' Set-Content -Path $wfPath -Encoding UTF8 -Value @' @@ -127,7 +165,7 @@ Describe 'Invoke-IdlePlan' { $emit = { param($Context, $Step) - & $Context.WriteEvent 'Custom' 'Hello from test step' $Step.Name @{ StepType = $Step.Type } + $Context.EventSink.WriteEvent('Custom', 'Hello from test step', $Step.Name, @{ StepType = $Step.Type }) [pscustomobject]@{ PSTypeName = 'IdLE.StepResult' @@ -187,4 +225,4 @@ Describe 'Invoke-IdlePlan' { $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' { New-IdlePlan -WorkflowPath $wfPath -Request $req } | Should -Throw } -} \ No newline at end of file +} diff --git a/tests/Invoke-IdlePlan.When.Tests.ps1 b/tests/Invoke-IdlePlan.When.Tests.ps1 index c1270e2d..93fee51b 100644 --- a/tests/Invoke-IdlePlan.When.Tests.ps1 +++ b/tests/Invoke-IdlePlan.When.Tests.ps1 @@ -26,7 +26,7 @@ Describe 'Invoke-IdlePlan - When conditions' { $emit = { param($Context, $Step) - & $Context.WriteEvent 'Custom' 'Hello' $Step.Name @{ StepType = $Step.Type } + $Context.EventSink.WriteEvent('Custom', 'Hello', $Step.Name, @{ StepType = $Step.Type }) [pscustomobject]@{ PSTypeName = 'IdLE.StepResult' Name = [string]$Step.Name @@ -71,7 +71,7 @@ Describe 'Invoke-IdlePlan - When conditions' { $emit = { param($Context, $Step) - & $Context.WriteEvent 'Custom' 'Hello' $Step.Name @{ StepType = $Step.Type } + $Context.EventSink.WriteEvent('Custom', 'Hello', $Step.Name, @{ StepType = $Step.Type }) [pscustomobject]@{ PSTypeName = 'IdLE.StepResult' Name = [string]$Step.Name