From f3856d156fd86c101850b00db6b60b6dad3ed185 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:52:33 +0000 Subject: [PATCH 1/6] Initial plan From 10f67018212bae62c3975ef753d54b0e725c3a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:56:15 +0000 Subject: [PATCH 2/6] Update IdLE module manifest and docs for non-blocking baseline - Remove optional modules from IdLE.psd1 NestedModules (keep only Core and Steps.Common) - Add comprehensive test to verify baseline-only auto-import behavior - Update examples/Invoke-IdleDemo.ps1 to remove redundant Steps.Common import - Update docs/getting-started/installation.md to clarify baseline vs optional modules - Update docs/advanced/releases.md to describe packaging baseline policy Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/advanced/releases.md | 8 ++++++-- docs/getting-started/installation.md | 27 ++++++++++++++++++--------- examples/Invoke-IdleDemo.ps1 | 2 +- src/IdLE/IdLE.psd1 | 5 +---- tests/ModuleSurface.Tests.ps1 | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index 3f77899d..c424728d 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -160,11 +160,15 @@ Staging is performed by: - `tools/New-IdleModulePackage.ps1` -This script copies the `IdLE` meta-module and required nested modules into a local `Modules/` folder and patches the staged +This script copies the `IdLE` meta-module and baseline nested modules (`IdLE.Core`, `IdLE.Steps.Common`) into a local `Modules/` folder and patches the staged `IdLE.psd1` so `NestedModules` use in-package relative paths (e.g. `./Modules/IdLE.Core/IdLE.Core.psd1`). +**Baseline vs Optional modules:** +- Baseline modules (`IdLE.Core`, `IdLE.Steps.Common`) are staged and auto-imported by `IdLE`. +- Optional modules (providers, additional step modules) may be shipped separately or referenced but are **not auto-imported** to ensure `Import-Module IdLE` remains non-blocking. + > This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works -> after installation. +> reliably on any clean PowerShell 7 environment without external dependencies. ## Versioning and naming diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 053ead88..ed837888 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -22,8 +22,8 @@ Install-Module -Name IdLE -Scope CurrentUser Import-Module IdLE ``` -> Note: The `IdLE` meta-module loads the bundled nested modules (e.g. `IdLE.Core`, built-in steps, and the mock provider -> used by examples) from within the installed package. +> Note: The `IdLE` module automatically imports the baseline modules (`IdLE.Core` and `IdLE.Steps.Common`). +> Optional modules (providers, additional step modules) are shipped with the package but not auto-imported. ### Verify installation @@ -61,9 +61,9 @@ Get-Command -Module IdLE.Core ## What gets imported -### Default: `IdLE` meta-module (batteries included) +### Default: `IdLE` meta-module (baseline) -`IdLE` is the **batteries-included** entrypoint. Importing it loads: +`IdLE` is the **baseline** entrypoint. Importing it automatically loads: - **IdLE.Core** — the workflow engine (step-agnostic) - **IdLE.Steps.Common** — first-party built-in steps (e.g. `IdLE.Step.EmitEvent`, `IdLE.Step.EnsureAttribute`) @@ -71,7 +71,9 @@ Get-Command -Module IdLE.Core Built-in steps are **available to the engine by default**, but are intentionally **not exported into the global session state**. This keeps your PowerShell session clean while still allowing workflows to reference built-in steps by `Step.Type`. -**When to use:** Most users and production scenarios. +**Non-blocking guarantee:** `Import-Module IdLE` always succeeds on a clean PowerShell 7 environment without any external dependencies (RSAT, AD tools, third-party modules, etc.). + +**When to use:** All users and production scenarios. ### Advanced: Engine-only import @@ -85,14 +87,21 @@ Import-Module ./src/IdLE.Core/IdLE.Core.psd1 -Force --- -## Provider modules (optional) +## Optional modules (shipped but not auto-imported) + +The `IdLE` package ships additional modules that are **not automatically imported**. These modules may have system-specific or tool-specific dependencies and are imported explicitly when needed: -The core engine is provider-agnostic. Provider modules are **packaged with IdLE** but must be **imported separately** when needed. +- **Provider modules:** `IdLE.Provider.AD`, `IdLE.Provider.EntraID`, `IdLE.Provider.ExchangeOnline`, `IdLE.Provider.DirectorySync.EntraConnect` +- **Optional step modules:** `IdLE.Steps.DirectorySync`, `IdLE.Steps.Mailbox` +- **Development/testing modules:** `IdLE.Provider.Mock` -Example: +Example (from source): ```powershell -# From source +# Import baseline (auto-imports Core and Steps.Common) +Import-Module ./src/IdLE/IdLE.psd1 -Force + +# Import optional provider when needed Import-Module ./src/IdLE.Provider.AD/IdLE.Provider.AD.psd1 -Force ``` diff --git a/examples/Invoke-IdleDemo.ps1 b/examples/Invoke-IdleDemo.ps1 index 9e2e11b4..b3ebe188 100644 --- a/examples/Invoke-IdleDemo.ps1 +++ b/examples/Invoke-IdleDemo.ps1 @@ -226,7 +226,7 @@ function Select-DemoWorkflows { # Import modules from the repo (path-based import, no global installation required). Import-Module (Join-Path $PSScriptRoot '..\src\IdLE\IdLE.psd1') -Force -ErrorAction Stop -Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Steps.Common\IdLE.Steps.Common.psd1') -Force -ErrorAction Stop +# Mock provider is optional and must be imported explicitly (not auto-imported by IdLE) Import-Module (Join-Path $PSScriptRoot '..\src\IdLE.Provider.Mock\IdLE.Provider.Mock.psd1') -Force -ErrorAction Stop $available = @(Get-DemoWorkflows -Category $Category) diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index 24951b81..77a3b171 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -9,10 +9,7 @@ NestedModules = @( '..\IdLE.Core\IdLE.Core.psd1', - '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1', - '..\IdLE.Steps.DirectorySync\IdLE.Steps.DirectorySync.psd1', - '..\IdLE.Provider.DirectorySync.EntraConnect\IdLE.Provider.DirectorySync.EntraConnect.psd1', - '..\IdLE.Provider.AD\IdLE.Provider.AD.psd1' + '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1' ) FunctionsToExport = @( diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 0e0f7ee3..4e9c2f13 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -156,6 +156,26 @@ Describe 'Module manifests and public surface' { ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty } + It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' { + Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common, IdLE.Steps.DirectorySync, IdLE.Provider.AD, IdLE.Provider.Mock -Force -ErrorAction SilentlyContinue + Import-Module $idlePsd1 -Force -ErrorAction Stop + + $idle = Get-Module IdLE + $idle | Should -Not -BeNullOrEmpty + + # Baseline modules should be auto-imported + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Core') | Should -Not -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty + + # Optional modules should NOT be auto-imported + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.DirectorySync') | Should -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.AD') | Should -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.Mock') | Should -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.DirectorySync.EntraConnect') | Should -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.EntraID') | Should -BeNullOrEmpty + ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.ExchangeOnline') | Should -BeNullOrEmpty + } + It 'Steps module exports the intended step functions' { Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue Import-Module $stepsPsd1 -Force -ErrorAction Stop From 49412664c47df7649c0396b1690f200ccae94ee7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:59:47 +0000 Subject: [PATCH 3/6] Fix module-qualified handler resolution for nested modules - Update Resolve-IdleStepHandler to properly resolve module-qualified commands - Support both global and nested module command lookup - Fixes step execution when steps are loaded as nested modules Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdlePlanObject.ps1 | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index e3409041..348d04e3 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -111,7 +111,39 @@ function Invoke-IdlePlanObject { ) } - $cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction Stop + # Resolve the handler command. + # The handler name can be: + # 1) A simple function name (e.g. "Invoke-IdleStepEmitEvent") - globally available + # 2) A module-qualified name (e.g. "IdLE.Steps.Common\Invoke-IdleStepEmitEvent") - from a nested module + # + # Module-qualified names are used for built-in steps that are loaded as nested modules + # and not exported globally to keep the session clean. + + $cmd = $null + + # Try simple lookup first (globally available commands) + $cmd = Get-Command -Name ([string]$handlerName) -CommandType Function -ErrorAction SilentlyContinue + + # If not found and name contains backslash, try module-qualified lookup + if ($null -eq $cmd -and ([string]$handlerName).Contains('\')) { + $parts = ([string]$handlerName).Split('\', 2) + if ($parts.Count -eq 2) { + $moduleName = $parts[0] + $commandName = $parts[1] + + $module = Get-Module -Name $moduleName -All | Select-Object -First 1 + if ($null -ne $module -and $null -ne $module.ExportedCommands) { + if ($module.ExportedCommands.ContainsKey($commandName)) { + $cmd = $module.ExportedCommands[$commandName] + } + } + } + } + + if ($null -eq $cmd) { + throw [System.ArgumentException]::new("Step handler '$handlerName' for step type '$StepType' could not be resolved to a valid command.", 'Providers') + } + return $cmd } From 2ec4c4e06acc1d98d348eddc88d53cf046d2c4a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:02:32 +0000 Subject: [PATCH 4/6] Refine module resolution and add clarifying comments - Optimize module lookup to avoid unnecessary pipeline operations - Add clarifying comment to test about explicit module list - Address code review feedback Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 | 9 ++++++--- tests/ModuleSurface.Tests.ps1 | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index 348d04e3..596b35f4 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -131,9 +131,12 @@ function Invoke-IdlePlanObject { $moduleName = $parts[0] $commandName = $parts[1] - $module = Get-Module -Name $moduleName -All | Select-Object -First 1 - if ($null -ne $module -and $null -ne $module.ExportedCommands) { - if ($module.ExportedCommands.ContainsKey($commandName)) { + # Get-Module -All returns loaded modules (including nested/hidden modules) + # We use -All to find modules that are loaded but not in the global session state + $modules = @(Get-Module -Name $moduleName -All) + if ($modules.Count -gt 0) { + $module = $modules[0] + if ($null -ne $module.ExportedCommands -and $module.ExportedCommands.ContainsKey($commandName)) { $cmd = $module.ExportedCommands[$commandName] } } diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 4e9c2f13..677c96ba 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -157,6 +157,8 @@ Describe 'Module manifests and public surface' { } It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' { + # Explicitly remove known modules to ensure clean test state. + # This list is intentionally explicit (not pattern-based) to document exactly which modules we're testing. Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common, IdLE.Steps.DirectorySync, IdLE.Provider.AD, IdLE.Provider.Mock -Force -ErrorAction SilentlyContinue Import-Module $idlePsd1 -Force -ErrorAction Stop From 31702809d35c95d68fef0d71c7963e3219d227d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:19:49 +0000 Subject: [PATCH 5/6] Address PR feedback: generalize test and move baseline policy docs - Generalize baseline test to use pattern matching instead of explicit module list - Move baseline vs optional policy from releases.md to installation.md (reference only) - Add count check to ensure exactly 2 nested modules - Maintain test isolation by only removing specific modules Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/advanced/releases.md | 4 +--- tests/ModuleSurface.Tests.ps1 | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index c424728d..d545ad04 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -163,9 +163,7 @@ Staging is performed by: This script copies the `IdLE` meta-module and baseline nested modules (`IdLE.Core`, `IdLE.Steps.Common`) into a local `Modules/` folder and patches the staged `IdLE.psd1` so `NestedModules` use in-package relative paths (e.g. `./Modules/IdLE.Core/IdLE.Core.psd1`). -**Baseline vs Optional modules:** -- Baseline modules (`IdLE.Core`, `IdLE.Steps.Common`) are staged and auto-imported by `IdLE`. -- Optional modules (providers, additional step modules) may be shipped separately or referenced but are **not auto-imported** to ensure `Import-Module IdLE` remains non-blocking. +For details on baseline vs optional modules and the non-blocking import policy, see **[Installation Guide](../getting-started/installation.md#what-gets-imported)**. > This approach avoids repository restructuring while ensuring that `Install-Module IdLE` + `Import-Module IdLE` works > reliably on any clean PowerShell 7 environment without external dependencies. diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 677c96ba..ca4920ff 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -157,25 +157,26 @@ Describe 'Module manifests and public surface' { } It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' { - # Explicitly remove known modules to ensure clean test state. - # This list is intentionally explicit (not pattern-based) to document exactly which modules we're testing. - Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common, IdLE.Steps.DirectorySync, IdLE.Provider.AD, IdLE.Provider.Mock -Force -ErrorAction SilentlyContinue + # Clean test state for specific test-related modules only + Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue Import-Module $idlePsd1 -Force -ErrorAction Stop $idle = Get-Module IdLE $idle | Should -Not -BeNullOrEmpty - # Baseline modules should be auto-imported + # Baseline modules should be auto-imported (explicit positive check) ($idle.NestedModules | Where-Object Name -eq 'IdLE.Core') | Should -Not -BeNullOrEmpty ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty - # Optional modules should NOT be auto-imported - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.DirectorySync') | Should -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.AD') | Should -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.Mock') | Should -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.DirectorySync.EntraConnect') | Should -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.EntraID') | Should -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Provider.ExchangeOnline') | Should -BeNullOrEmpty + # Only baseline modules should be nested (count check ensures no extras) + @($idle.NestedModules).Count | Should -Be 2 + + # Verify no optional modules are nested (generalized negative check using pattern) + # This pattern matches: IdLE.Provider.* or IdLE.Steps.* (except Steps.Common) + $nestedNames = @($idle.NestedModules | Select-Object -ExpandProperty Name) + $optionalModulePattern = '^IdLE\.(Provider\.|Steps\.(?!Common$))' + $unexpectedModules = $nestedNames | Where-Object { $_ -match $optionalModulePattern } + $unexpectedModules | Should -BeNullOrEmpty -Because "Optional modules should not be auto-imported" } It 'Steps module exports the intended step functions' { From 078c3af2883001ac079d0d660323a3082b9bbf4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:54:53 +0000 Subject: [PATCH 6/6] Refactor baseline test with abstracted module list - Define baseline modules in single array for maintainability - Use foreach loop to validate each baseline module is auto-imported - Use count check with $baselineModules.Count instead of hardcoded value - Clarify cleanup strategy in comments (maintain test isolation) - Addresses PR feedback comments 2725593807 and 2725594726 Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/ModuleSurface.Tests.ps1 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index ca4920ff..b74009dd 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -157,19 +157,24 @@ Describe 'Module manifests and public surface' { } It 'IdLE auto-imports only baseline modules (Core and Steps.Common), not optional modules' { - # Clean test state for specific test-related modules only + # Clean test state - remove baseline modules to ensure fresh import + # We don't remove optional modules to maintain test isolation (other tests may have them loaded) Remove-Module IdLE, IdLE.Core, IdLE.Steps.Common -Force -ErrorAction SilentlyContinue Import-Module $idlePsd1 -Force -ErrorAction Stop $idle = Get-Module IdLE $idle | Should -Not -BeNullOrEmpty + # Define expected baseline modules in one place + $baselineModules = @('IdLE.Core', 'IdLE.Steps.Common') + # Baseline modules should be auto-imported (explicit positive check) - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Core') | Should -Not -BeNullOrEmpty - ($idle.NestedModules | Where-Object Name -eq 'IdLE.Steps.Common') | Should -Not -BeNullOrEmpty + foreach ($moduleName in $baselineModules) { + ($idle.NestedModules | Where-Object Name -eq $moduleName) | Should -Not -BeNullOrEmpty -Because "$moduleName should be auto-imported" + } # Only baseline modules should be nested (count check ensures no extras) - @($idle.NestedModules).Count | Should -Be 2 + @($idle.NestedModules).Count | Should -Be $baselineModules.Count # Verify no optional modules are nested (generalized negative check using pattern) # This pattern matches: IdLE.Provider.* or IdLE.Steps.* (except Steps.Common)