From 085f288eabb9cccaf099224bbae19df5541b39cb Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Sun, 21 Jun 2026 14:19:00 +1200 Subject: [PATCH 1/6] chore: Add Agentic AI development assets for GitHub Copilot - Add AGENTS.md at repository root (build/test workflow, resource types, conventions) - Add tests/AGENTS.md (Pester 5, test locations, required setup block templates) - Add .github/copilot-instructions.md with merged core DSC Community requirements, module identity, terminology, file organization, and instruction file index - Add .github/instructions/ with 10 instruction files: - ComputerManagementDsc-guidelines (repo-specific build/test/naming) - dsc-community-powershell (generalized PowerShell style) - dsc-community-pester-5 (generalized Pester 5 test style) - dsc-community-unit-tests (generalized unit test setup templates) - dsc-community-integration-tests (generalized integration test templates) - dsc-community-mof-resource (generalized MOF resource implementation) - dsc-community-class-resource (generalized class-based resource implementation) - dsc-community-localization (generalized localization conventions) - dsc-community-changelog (generalized changelog format) - dsc-community-markdown (generalized markdown style) Closes #464 --- .github/copilot-instructions.md | 72 ++++++ ...erManagementDsc-guidelines.instructions.md | 37 +++ .../dsc-community-changelog.instructions.md | 16 ++ ...c-community-class-resource.instructions.md | 102 ++++++++ ...ommunity-integration-tests.instructions.md | 70 +++++ ...dsc-community-localization.instructions.md | 46 ++++ .../dsc-community-markdown.instructions.md | 22 ++ ...dsc-community-mof-resource.instructions.md | 60 +++++ .../dsc-community-pester-4.instructions.md | 82 ++++++ .../dsc-community-powershell.instructions.md | 244 ++++++++++++++++++ .../dsc-community-unit-tests.instructions.md | 156 +++++++++++ AGENTS.md | 101 ++++++++ CHANGELOG.md | 11 + tests/AGENTS.md | 169 ++++++++++++ 14 files changed, 1188 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/ComputerManagementDsc-guidelines.instructions.md create mode 100644 .github/instructions/dsc-community-changelog.instructions.md create mode 100644 .github/instructions/dsc-community-class-resource.instructions.md create mode 100644 .github/instructions/dsc-community-integration-tests.instructions.md create mode 100644 .github/instructions/dsc-community-localization.instructions.md create mode 100644 .github/instructions/dsc-community-markdown.instructions.md create mode 100644 .github/instructions/dsc-community-mof-resource.instructions.md create mode 100644 .github/instructions/dsc-community-pester-4.instructions.md create mode 100644 .github/instructions/dsc-community-powershell.instructions.md create mode 100644 .github/instructions/dsc-community-unit-tests.instructions.md create mode 100644 AGENTS.md create mode 100644 tests/AGENTS.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..f8fdad75 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,72 @@ +# Requirements + +- ComputerManagementDsc-specific guidelines and requirements override general project + guidelines and requirements. + +## Module Identity + +**ComputerManagementDsc** is a DSC Community PowerShell module providing Desired State +Configuration resources for Windows computer and OS settings. + +## Terminology + +- **Command**: Public command +- **Function**: Private function +- **Resource**: DSC class-based resource + +## Core Requirements + +- Instructions take precedence over existing code patterns. +- Always update the `Unreleased` section of `CHANGELOG.md` for every code change. +- Localize all user-visible strings using `$script:localizedData` keys; never use + hardcoded string literals in `Write-Verbose`, `Write-Error`, or exception messages. +- Check [`DscResource.Common`](https://github.com/dsccommunity/DscResource.Common/wiki) + before creating private helper functions. +- Use `New-InvalidOperationException`, `New-ArgumentException`, and similar helpers from + `DscResource.Common` instead of `throw`. +- Separate reusable logic into private functions. +- Add unit tests for all commands, functions, and resources. +- Add integration tests for all public commands and resources. + +## Resource Types + +This repository contains two types of DSC resources: + +- **MOF-based resources** (`source/DSCResources/DSC_/`) — implement + `Get-TargetResource`, `Test-TargetResource`, and `Set-TargetResource` +- **Class-based resources** (`source/Classes/..ps1`) — inherit `ResourceBase` + from `DscResource.Base`; implement `GetCurrentState()` and `Modify()` +- Prefer class-based resources; use MOF-based only when required. + +## File Organization + +- Class-based resources: `source/Classes/..ps1` +- MOF-based resources: `source/DSCResources/DSC_/DSC_.psm1` +- Resource enums: `source/Enum/..ps1` +- Unit tests (MOF): `tests/Unit/DSC_.Tests.ps1` +- Unit tests (class): `tests/Unit/Classes/.Tests.ps1` +- Integration tests: `tests/Integration/.Integration.Tests.ps1` + +## Instruction Files + +Read the following instruction files before working on the corresponding areas: + +- `.github/instructions/ComputerManagementDsc-guidelines.instructions.md` — build/test + workflow and naming conventions +- `.github/instructions/dsc-community-powershell.instructions.md` — PowerShell style +- `.github/instructions/dsc-community-pester-4.instructions.md` — Pester test style +- `.github/instructions/dsc-community-unit-tests.instructions.md` — unit test setup and patterns +- `.github/instructions/dsc-community-integration-tests.instructions.md` — integration test patterns +- `.github/instructions/dsc-community-mof-resource.instructions.md` — MOF resource implementation +- `.github/instructions/dsc-community-class-resource.instructions.md` — class-based resource implementation +- `.github/instructions/dsc-community-localization.instructions.md` — localized string conventions +- `.github/instructions/dsc-community-changelog.instructions.md` — changelog format +- `.github/instructions/dsc-community-markdown.instructions.md` — markdown style + +## Key External References + +- [DSC Community Guidelines](https://dsccommunity.org/guidelines/) +- [DSC Community Blog](https://dsccommunity.org/blog/) +- [DscResource.Common](https://github.com/dsccommunity/DscResource.Common) +- [DscResource.Base](https://github.com/dsccommunity/DscResource.Base) +- [DscResource.Test](https://github.com/dsccommunity/DscResource.Test) diff --git a/.github/instructions/ComputerManagementDsc-guidelines.instructions.md b/.github/instructions/ComputerManagementDsc-guidelines.instructions.md new file mode 100644 index 00000000..1e858e37 --- /dev/null +++ b/.github/instructions/ComputerManagementDsc-guidelines.instructions.md @@ -0,0 +1,37 @@ +--- +description: ComputerManagementDsc-specific guidelines for AI development. +applyTo: "**" +--- + +# ComputerManagementDsc Requirements + +## Build & Test Workflow Requirements + +- Never use VS Code tasks; always use PowerShell scripts via terminal from the repository root. +- Setup build and test environment (once per `pwsh` session): `./build.ps1 -Tasks noop` +- Build project before running tests: `./build.ps1 -Tasks build` +- Run all unit tests: `Invoke-Pester -Path 'tests/Unit' -Output Detailed` +- Run a specific MOF resource test: + `Invoke-Pester -Path 'tests/Unit/DSC_.Tests.ps1' -Output Detailed` +- Run a specific class resource test: + `Invoke-Pester -Path 'tests/Unit/Classes/.Tests.ps1' -Output Detailed` +- Never run integration tests locally. + +## Naming + +- MOF-based resources: `DSC_` prefix on all files and exported functions + (e.g. `DSC_TimeZone.psm1`, `Get-TargetResource` is exported via `*-TargetResource`) +- Class-based resources: PascalCase class name; file prefix `..ps1` where + `N` is the dependency group number (e.g. `020.PSResourceRepository.ps1`) + +## Resource Type Selection + +- Implement new resources as class-based (`source/Classes/`) unless MOF-based is required + (e.g. WMF 4.0 support). + +## Tests + +- MOF unit tests: `tests/Unit/DSC_.Tests.ps1` +- Class unit tests: `tests/Unit/Classes/.Tests.ps1` +- Integration tests: `tests/Integration/` (flat) +- Run unit tests in a new `pwsh` session after changing class-based resources. diff --git a/.github/instructions/dsc-community-changelog.instructions.md b/.github/instructions/dsc-community-changelog.instructions.md new file mode 100644 index 00000000..5039f0b2 --- /dev/null +++ b/.github/instructions/dsc-community-changelog.instructions.md @@ -0,0 +1,16 @@ +--- +description: Guidelines for maintaining a clear and consistent changelog. +applyTo: "CHANGELOG.md" +version: 1.0.0 +--- + +# Changelog Guidelines + +- Always update the Unreleased section in `CHANGELOG.md` +- Use Keep a Changelog format +- Describe notable changes briefly, ≤2 items per change type +- Reference issues using format + `[issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/)` +- No empty lines between list items in same section +- Skip adding entry if same change already exists in Unreleased section +- No duplicate sections or items in Unreleased section diff --git a/.github/instructions/dsc-community-class-resource.instructions.md b/.github/instructions/dsc-community-class-resource.instructions.md new file mode 100644 index 00000000..acfae5d6 --- /dev/null +++ b/.github/instructions/dsc-community-class-resource.instructions.md @@ -0,0 +1,102 @@ +--- +description: Guidelines for implementing Desired State Configuration (DSC) class-based resources. +applyTo: "source/[cC]lasses/**/*.ps1" +version: 1.0.0 +--- + +# DSC Class-Based Resource Guidelines + +**Applies to:** Classes with `[DscResource(...)]` decoration only. + +## Requirements + +- File: `source/Classes/..ps1` +- Decoration: `[DscResource(RunAsCredential = 'Optional')]` (use `'Mandatory'` if required) +- Inherit `ResourceBase` (DscResource.Base) +- `$this.localizedData` auto-populated by `ResourceBase` from localization file +- Value-type properties: use `[Nullable[{FullTypeName}]]` (e.g. `[Nullable[System.Int32]]`) + +## Required Constructor + +```powershell +MyResourceName () : base ($PSScriptRoot) +{ + # Property names where state cannot be enforced, e.g. IsSingleInstance, Force + $this.ExcludeDscProperties = @() +} +``` + +## Required Method Pattern + +```powershell +[MyResourceName] Get() +{ + # Call base implementation to get current state + $currentState = ([ResourceBase] $this).Get() + + # If needed, post-processing on current state that cannot be handled by GetCurrentState() + + return $currentState +} + +[System.Boolean] Test() +{ + # Call base implementation to test current state + $inDesiredState = ([ResourceBase] $this).Test() + + # If needed, post-processing on test result that cannot be handled by base Test() + + return $inDesiredState +} + +[void] Set() +{ + # Call base implementation to set desired state + ([ResourceBase] $this).Set() + + # If needed, additional state changes that cannot be handled by Modify() +} + +hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) +{ + # Always return current state as hashtable; $properties contains key properties +} + +hidden [void] Modify([System.Collections.Hashtable] $properties) +{ + # Always set desired state; $properties contains those that must change state +} +``` + +## Optional Method Pattern + +```powershell +hidden [void] AssertProperties([System.Collections.Hashtable] $properties) +{ + # Validate user-provided properties; $properties contains user-assigned values +} + +hidden [void] NormalizeProperties([System.Collections.Hashtable] $properties) +{ + # Normalize user-provided properties; $properties contains user-assigned values +} +``` + +## Required Comment-based Help + +Add to `.DESCRIPTION` section: + +- `## Requirements`: List minimum requirements +- `## Known issues`: Critical issues + pattern: + `All issues are not listed here, see [all open issues](https://github.com/dsccommunity/ComputerManagementDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+{ResourceName}).` + +## Error Handling for Classes + +- Use `try/catch` blocks to handle exceptions +- Do not use `throw` for terminating errors; use `New-*Exception` commands: + - [`New-InvalidDataException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidDataException) + - [`New-ArgumentException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ArgumentException) + - [`New-InvalidOperationException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidOperationException) + - [`New-ObjectNotFoundException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ObjectNotFoundException) + - [`New-InvalidResultException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidResultException) + - [`New-NotImplementedException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91NotImplementedException) diff --git a/.github/instructions/dsc-community-integration-tests.instructions.md b/.github/instructions/dsc-community-integration-tests.instructions.md new file mode 100644 index 00000000..4cee83b9 --- /dev/null +++ b/.github/instructions/dsc-community-integration-tests.instructions.md @@ -0,0 +1,70 @@ +--- +description: Guidelines for implementing integration tests for DSC resources. +applyTo: "tests/[iI]ntegration/**/*.[iI]ntegration.[tT]ests.ps1" +version: 1.0.0 +--- + +# Integration Tests Guidelines + +## Requirements + +- Location: `tests/Integration/.Integration.Tests.ps1` +- No mocking — real environment only +- Cover all scenarios and code paths +- Use `Get-ComputerName` for computer names in CI +- Avoid `ExpectedMessage` for `Should -Throw` assertions +- Run integration tests in CI only unless explicitly instructed otherwise +- Call commands with `-Force` where applicable (avoids prompting) +- Use `-ErrorAction 'Stop'` so failures surface immediately + +## Required Setup Block + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } + + $script:dscModuleName = '' + $script:dscResourceName = 'DSC_' + + $script:skipIntegrationTests = $false +} + +BeforeAll { + $script:dscModuleName = '' + $script:dscResourceName = 'DSC_' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +AfterAll { + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} +``` diff --git a/.github/instructions/dsc-community-localization.instructions.md b/.github/instructions/dsc-community-localization.instructions.md new file mode 100644 index 00000000..83368ce2 --- /dev/null +++ b/.github/instructions/dsc-community-localization.instructions.md @@ -0,0 +1,46 @@ +--- +description: Guidelines for implementing localization. +applyTo: "source/**/*.ps1" +version: 1.0.0 +--- + +# Localization Guidelines + +## Requirements + +- Localize all `Write-Debug`, `Write-Verbose`, `Write-Error`, `Write-Warning`, and + `$PSCmdlet.ThrowTerminatingError()` messages; never use hardcoded strings +- Assume `$script:localizedData` is available + +## String Files + +- MOF-based resources: + `source/DSCResources/DSC_/en-US/DSC_.strings.psd1` +- Class-based resources: `source/en-US/.strings.psd1` +- Module-level strings: `source/en-US/.strings.psd1` + +## Key Naming Patterns + +- Format: `Verb_FunctionName_Action` (underscore separators), + e.g. `Get_TimeZone_GettingCurrentState` + +## String Format + +```powershell +ConvertFrom-StringData @' + KeyName = Message with {0} placeholder. (PREFIX0001) +'@ +``` + +## String IDs + +- Format: `(PREFIX####)` +- PREFIX: First letter of each word in class or function name + (e.g. `Get-TargetResource` for `DSC_TimeZone` → `GTR`, `PSResourceRepository` → `PRR`) +- Number: Sequential from 0001 + +## Usage + +```powershell +Write-Verbose -Message ($script:localizedData.KeyName -f $value1) +``` diff --git a/.github/instructions/dsc-community-markdown.instructions.md b/.github/instructions/dsc-community-markdown.instructions.md new file mode 100644 index 00000000..619ef616 --- /dev/null +++ b/.github/instructions/dsc-community-markdown.instructions.md @@ -0,0 +1,22 @@ +--- +description: Guidelines for writing and maintaining Markdown documentation. +applyTo: "**/*.md" +version: 1.0.0 +--- + +# Markdown Style Guidelines + +- Wrap lines at word boundaries when over 80 characters (except tables/code blocks) +- Use 2 spaces for indentation +- Use '1.' for all items in ordered lists (1/1/1 numbering style) +- Disable `MD013` rule by adding a comment for tables/code blocks exceeding 80 characters +- Empty lines required before/after code blocks and headings (except before line 1) +- Escape backslashes in file paths only (not in code blocks) +- Code blocks must specify language identifiers + +## Text Formatting + +- Parameters: **bold** +- Values/literals: `inline code` +- Resource/module/product names: _italic_ +- Commands/files/paths: `inline code` diff --git a/.github/instructions/dsc-community-mof-resource.instructions.md b/.github/instructions/dsc-community-mof-resource.instructions.md new file mode 100644 index 00000000..1cf39247 --- /dev/null +++ b/.github/instructions/dsc-community-mof-resource.instructions.md @@ -0,0 +1,60 @@ +--- +description: Guidelines for implementing MOF DSC resources. +applyTo: "source/DSCResources/**/*.psm1" +version: 1.0.0 +--- + +# MOF-based Desired State Configuration (DSC) Resources Guidelines + +## Required Functions + +- Define: `Get-TargetResource`, `Set-TargetResource`, `Test-TargetResource` +- Export using `*-TargetResource` pattern + +## Function Return Types + +- `Get-TargetResource`: Must return hashtable with all resource properties +- `Test-TargetResource`: Must return boolean (`$true`/`$false`) +- `Set-TargetResource`: Must not return anything (void) + +## Parameter Guidelines + +- `Get-TargetResource`: Only include parameters needed to retrieve actual current state values +- `Get-TargetResource`: Remove non-mandatory parameters that are never used +- `Set-TargetResource` and `Test-TargetResource`: Must have identical parameters +- `Set-TargetResource` and `Test-TargetResource`: Unused mandatory parameters: Add + "Not used in ``" to help comment + +## Required Elements + +- Each function must include `Write-Verbose` at least once + - `Get-TargetResource`: Use verbose message starting with "Getting the current state of..." + - `Set-TargetResource`: Use verbose message starting with "Setting the desired state of..." + - `Test-TargetResource`: Use verbose message starting with + "Determining the current state of..." +- Use localized strings for all messages (`Write-Verbose`, `Write-Error`, etc.) +- Import localized strings using `Get-LocalizedData` at module top + +## Error Handling for MOF-based Resources + +- Use `try/catch` blocks to handle exceptions +- Do not use `throw` for terminating errors; use `New-*Exception` commands: + - [`New-InvalidDataException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidDataException) + - [`New-ArgumentException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ArgumentException) + - [`New-InvalidOperationException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidOperationException) + - [`New-ObjectNotFoundException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ObjectNotFoundException) + - [`New-InvalidResultException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidResultException) + - [`New-NotImplementedException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91NotImplementedException) + +# MOF Resource Localization + +## File Structure + +- Create `en-US` folder in each resource directory +- Name strings file: `DSC_.strings.psd1` +- Use names returned from `Get-UICulture` for additional language folder names + +## String File Format + +- In `.strings.psd1` files, use underscores as word separators in localized string key + names (for multi-word keys) diff --git a/.github/instructions/dsc-community-pester-4.instructions.md b/.github/instructions/dsc-community-pester-4.instructions.md new file mode 100644 index 00000000..cc510f55 --- /dev/null +++ b/.github/instructions/dsc-community-pester-4.instructions.md @@ -0,0 +1,82 @@ +--- +description: Guidelines for writing and maintaining tests using Pester. +applyTo: "**/*.[Tt]ests.ps1" +version: 1.0.0 +--- + +# Tests Guidelines + +## Core Requirements + +- All public commands, private functions, and classes must have unit tests +- All public commands and class-based resources must have integration tests +- Test code only inside `Describe` blocks +- Assertions only in `It` blocks +- Never test verbose messages, debug messages, or parameter binding behavior +- Pass all mandatory parameters to avoid prompts + +## Requirements + +- Inside `It` blocks, assign unused return objects to `$null` (unless part of pipeline) +- Call the tested entity from within `It` blocks +- Keep results and assertions in same `It` block +- Avoid try-catch-finally for cleanup; use `AfterAll` or `AfterEach` +- Avoid unnecessary remove/recreate cycles + +## Naming + +- One `Describe` block per file matching the tested entity name +- `Context` descriptions start with 'When' +- `It` descriptions start with 'Should'; must not contain 'when' +- Mock variables prefix: 'mock' + +## Structure & Scope + +- Public commands: Never use `InModuleScope` (unless retrieving localized strings or + creating an object using an internal class) +- Private functions/class resources: Always use `InModuleScope` +- Each class method = separate `Context` block +- Each scenario = separate `Context` block +- Use nested `Context` blocks for complex scenarios +- Mocking in `BeforeAll` (`BeforeEach` only when required) +- Setup/teardown in `BeforeAll`, `BeforeEach`/`AfterAll`, `AfterEach` close to usage +- Spacing between blocks, arrange, act, and assert for readability + +## Syntax Rules + +- PascalCase: `Describe`, `Context`, `It`, `Should`, `BeforeAll`, `BeforeEach`, `AfterAll`, + `AfterEach` +- Use `-BeTrue`/`-BeFalse` never `-Be $true`/`-Be $false`/`-Contain $true`/`-Contain $false` +- Never use `Assert-MockCalled`; use `Should -Invoke` instead +- No `Should -Not -Throw` — invoke commands directly +- Never add an empty `-MockWith` block +- Omit `-MockWith` when returning `$null` +- Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, + `InModuleScope:ModuleName` +- Omit `-ModuleName` parameter on Pester commands +- Never use `Mock` inside `InModuleScope` block +- Never use `param()` block inside `-MockWith` scriptblocks; parameters are auto-bound +- In `InModuleScope` tests, add `Set-StrictMode -Version 1.0` immediately before invoking + the tested function +- Use `Should -Invoke -Exactly -Times -Scope It` for call-count assertions + - Assert `` calls inside the `It` block; do not assert call counts across an entire + `Describe` or `Context` + +## File Organization + +- Class resources: `tests/Unit/Classes/{Name}.Tests.ps1` +- MOF resources: `tests/Unit/DSC_{Name}.Tests.ps1` + +## Data-Driven Tests (Test Cases) + +- Define `-ForEach` variables in separate `BeforeDiscovery` (close to usage) +- `-ForEach` allowed on `Context` and `It` blocks +- Never add `param()` inside Pester blocks when using `-ForEach` +- Access test case properties directly: `$PropertyName` + +## Best Practices + +- Cover all scenarios and code paths +- Use `BeforeEach` and `AfterEach` sparingly +- Use `$PSDefaultParameterValues` only for Pester commands (`Describe`, `Context`, `It`, + `Mock`, `Should`, `InModuleScope`) diff --git a/.github/instructions/dsc-community-powershell.instructions.md b/.github/instructions/dsc-community-powershell.instructions.md new file mode 100644 index 00000000..1b04e1c4 --- /dev/null +++ b/.github/instructions/dsc-community-powershell.instructions.md @@ -0,0 +1,244 @@ +--- +description: Guidelines for writing PowerShell scripts and modules. +applyTo: "{**/*.ps1,**/*.psm1,**/*.psd1}" +version: 1.0.0 +--- + +# PowerShell Guidelines + +## Naming + +- Use descriptive names (3+ characters, no abbreviations) +- Functions: PascalCase with Verb-Noun format using approved verbs +- Parameters: PascalCase +- Variables: camelCase +- Keywords: lower-case +- Classes: PascalCase +- Include scope for script/global/environment variables: `$script:`, `$global:`, `$env:` + +## File Naming + +- Class files: `###.ClassName.ps1` format (e.g. `001.CMReason.ps1`, `020.PSResourceRepository.ps1`) + +## Formatting + +### Indentation & Spacing + +- Use 4 spaces (no tabs) +- One space around operators: `$a = 1 + 2` +- One space between type and variable: `[String] $name` +- One space between keyword and parenthesis: `if ($condition)` +- No spaces on empty lines +- Try to limit lines to 120 characters + +### Braces + +- Newline before opening brace (except variable assignments) +- One newline after opening brace +- Two newlines after closing brace (one if followed by another brace or continuation) + +### Quotes + +- Use single quotes unless variable expansion is needed: `'text'` vs `"text $variable"` + +### Arrays + +- Single line: `@('one', 'two', 'three')` +- Multi-line: each element on separate line with proper indentation +- Do not use the unary comma operator (`,`) in return statements to force an array + +### Hashtables + +- Empty: `@{}` +- Each property on separate line with proper indentation +- Properties: Use PascalCase + +### Comments + +- Single line: `# Comment` (capitalized, on own line) +- Multi-line: `<# Comment #>` format (opening and closing brackets on own line), and indent text +- No commented-out code + +### Comment-based Help + +- Always add comment-based help to all functions and scripts +- Sections required: SYNOPSIS, DESCRIPTION (40+ chars), PARAMETER, EXAMPLE +- Comment-based help indentation: keywords 4 spaces, text 8 spaces +- Include examples for all parameter sets and combinations +- INPUTS: List each pipeline-accepted type as inline code with a 1-line description. Repeat + keyword for each input type. If there are no inputs, specify `None.`. +- OUTPUTS: List each return type as inline code with a 1-line description. Repeat keyword for + each output type. Must match both `[OutputType()]` and actual returns. If there are no + outputs, specify `None.`. +- .NOTES: Include only if it conveys critical info (constraints, side effects, security, + version compatibility, breaking behavior). Keep to ≤2 short sentences. + +## Functions + +- Avoid aliases (use full command names) +- Avoid `Write-Host` (use `Write-Verbose`, `Write-Information`, etc.) +- Avoid `Write-Output` (use `return` instead) +- Avoid `ConvertTo-SecureString -AsPlainText` in production code +- Don't redefine reserved parameters (Verbose, Debug, etc.) +- Include a `Force` parameter for functions that use `$PSCmdlet.ShouldContinue` or + `$PSCmdlet.ShouldProcess` +- For state-changing functions, use `SupportsShouldProcess` + - Place ShouldProcess check immediately before each state-change + - Set `ConfirmImpact` to 'Low', 'Medium', or 'High' depending on risk + - `$PSCmdlet.ShouldProcess` must use required pattern + - Inside `$PSCmdlet.ShouldProcess` block, avoid using `Write-Verbose` +- Never use backtick as line continuation in production code +- Set `$ErrorActionPreference = 'Stop'` before commands using `-ErrorAction 'Stop'`; restore + previous value directly after invocation (do not use try-catch-finally) +- Use `[Alias()]` attribute for function aliases, never `Set-Alias` or `New-Alias` + +### Structure + +```powershell +<# + .SYNOPSIS + Brief description + + .DESCRIPTION + Detailed description + + .PARAMETER Name + Parameter description + + .INPUTS + TypeName1 + + Description1 + + .OUTPUTS + TypeName1 + + Description1 +#> +function Get-Something +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String] + $OptionalParam + ) + + # Implementation +} +``` + +### Requirements + +- Include `[CmdletBinding()]` on every function +- Parameter block at top +- Parameter block: `param ()` if empty, else opening/closing parentheses on own lines +- `[OutputType({return type})]` for functions with output; no output use `[OutputType()]` +- All parameters use `[Parameter()]` attribute; mandatory parameters use + `[Parameter(Mandatory = $true)]` +- Parameter attributes on separate lines +- Parameter type on line above parameter name +- Parameters separated by blank line +- Parameters should use full type name +- `ValueFromPipeline` must be consistent across all parameter sets declarations for the same + parameter + +## Output Streams + +- Never output sensitive data/secrets +- Use `Write-Debug` for: internal diagnostics; variable values/traces; developer-focused + details +- Use `Write-Verbose` for: high-level execution flow only; user-actionable information +- Use `Write-Information` for: user-facing status updates; important operational messages; + non-error state changes +- Use `Write-Warning` for: non-fatal issues requiring attention; deprecated functionality + usage; configuration problems that don't block execution +- Use `$PSCmdlet.ThrowTerminatingError()` for terminating errors (except for classes); use + relevant error category; in try-catch include exception with localized message +- Use `Write-Error` for non-terminating errors + - Always include `-Message` (localized string), `-Category` (relevant error category), + `-ErrorId` (unique ID matching localized string ID), `-TargetObject` (object causing error) + - In catch blocks, pass original exception using `-Exception` + - Always use `return` after `Write-Error` to avoid further processing + +## ShouldProcess Required Pattern + +- Ensure `$descriptionMessage` explains what will happen +- Ensure `$confirmationMessage` succinctly asks for confirmation +- Keep `$captionMessage` short and descriptive (no trailing `.`) + +```powershell +$descriptionMessage = $script:localizedData.FunctionName_Action_ShouldProcessDescription -f $param1, $param2 +$confirmationMessage = $script:localizedData.FunctionName_Action_ShouldProcessConfirmation -f $param1 +$captionMessage = $script:localizedData.FunctionName_Action_ShouldProcessCaption + +if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) +{ + # state changing code +} +``` + +## Force Parameter Pattern + +```powershell +if ($Force.IsPresent -and -not $Confirm) +{ + $ConfirmPreference = 'None' +} +``` + +## Best Practices + +### Code Organization + +- Use named parameters in function calls +- Use splatting for long parameter lists +- Limit piping to one pipe per line +- Assign function results to variables rather than inline calls +- Return a single, consistent object type per function + - Return `$null` for no objects/non-terminating errors +- For most .NET types, use the `::new()` static method instead of `New-Object`, + e.g. `[System.DateTime]::new()` +- For error handling, use dedicated helper commands instead: + - Use `New-Exception` instead of `[System.Exception]::new(...)` + - Use `New-ErrorRecord` instead of `[System.Management.Automation.ErrorRecord]::new(...)` + +### Security & Safety + +- Use `PSCredential` for credentials +- Avoid hardcoded computer names; use cross-platform + [`Get-ComputerName`](https://github.com/dsccommunity/DscResource.Common/wiki/Get%E2%80%91ComputerName) + instead of `$env:COMPUTERNAME` +- Place `$null` on left side of comparisons +- Avoid empty catch blocks (instead use `-ErrorAction SilentlyContinue`) +- Don't use `Invoke-Expression` (use `&` operator) +- Use CIM commands instead of WMI commands + +### Variables + +- Avoid global variables (exception: `$global:DSCMachineStatus`) +- Use declared variables more than once +- Avoid unnecessary type declarations when type is clear +- Use full type name when type casting +- No default values for mandatory or switch parameters + +## File Rules + +- End files with only one blank line +- Use line endings based on `.gitattributes` policy +- Maximum two consecutive newlines +- No line shall have trailing whitespace + +## File Encoding + +- Use UTF-8 encoding (no BOM) for all files + +## Module Manifest + +- Don't use `NestedModules` for shared commands without `RootModule` diff --git a/.github/instructions/dsc-community-unit-tests.instructions.md b/.github/instructions/dsc-community-unit-tests.instructions.md new file mode 100644 index 00000000..bf2d50c1 --- /dev/null +++ b/.github/instructions/dsc-community-unit-tests.instructions.md @@ -0,0 +1,156 @@ +--- +description: Guidelines for writing and maintaining unit tests using Pester. +applyTo: "tests/[Uu]nit/**/*.[Tt]ests.ps1" +version: 1.0.0 +--- + +# Unit Tests Guidelines + +- Test localized strings: `InModuleScope -ScriptBlock { $script:localizedData.Key }` +- Mock files: use `$TestDrive` +- All public commands require parameter set validation tests +- Run tests in a new session after modifying class-based resources + +## Test Setup Requirements + +### MOF Resource Unit Test Setup Block + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = '' + $script:dscResourceName = 'DSC_' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscResourceName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Restore-TestEnvironment -TestEnvironment $script:testEnvironment + + Get-Module -Name $script:dscResourceName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +### Class-based Resource Unit Test Setup Block + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = '' + + Import-Module -Name $script:dscModuleName + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +## Required Test Templates + +### Parameter Set Validation + +Single parameter set: + +```powershell +It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '{ParameterSetName}' # e.g. __AllParameterSets + ExpectedParameters = '[-Parameter1] [-Parameter2] []' + } +) { + $result = (Get-Command -Name 'CommandName').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters +} +``` + +Multiple parameter sets: Use same pattern with multiple hashtables in `-ForEach` array. + +### Parameter Properties + +```powershell +It 'Should have ParameterName as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'CommandName').Parameters['ParameterName'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue +} +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..92a14296 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# ComputerManagementDsc — AI Agent Guide + +## Repository Overview + +**ComputerManagementDsc** is a PowerShell Desired State Configuration (DSC) resource module +for managing Windows computer and operating system settings. + +- **Target platform:** Windows Server 2012 R2 and later; Windows 8.1 and later +- **DSC engines:** DSC v2 (WMF 5.0, MOF-based) and DSC v3 (PowerShell classes) +- **Organization:** [DSC Community](https://dsccommunity.org/) + +## Resource Types + +This repository contains two types of DSC resources: + +| Type | Location | Pattern | +|------|----------|---------| +| MOF-based | `source/DSCResources/DSC_/` | WMF 5.0 compatible, function-based | +| Class-based | `source/Classes/..ps1` | PowerShell class, inherits `ResourceBase` | + +### MOF-based Resource Structure + +``` +source/DSCResources/DSC_/ + DSC_.psm1 # Resource implementation + DSC_.schema.mof # MOF schema definition + en-US/ + DSC_.strings.psd1 # Localized strings +``` + +Required functions: `Get-TargetResource`, `Test-TargetResource`, `Set-TargetResource` + +### Class-based Resource Structure + +``` +source/Classes/..ps1 # N = dependency group number +source/en-US/.strings.psd1 # Localized strings +``` + +Class resources inherit `ResourceBase` from `DscResource.Base`. Implement `GetCurrentState()` +and `Modify()`. Optionally implement `AssertProperties()` and `NormalizeProperties()`. + +## Build & Test Workflow + +### One-time setup (once per `pwsh` session) + +```powershell +./build.ps1 -Tasks noop +``` + +### Build before running tests + +```powershell +./build.ps1 -Tasks build +``` + +### Run all unit tests + +```powershell +Invoke-Pester -Path 'tests/Unit' -Output Detailed +``` + +### Run a specific MOF resource unit test + +```powershell +Invoke-Pester -Path 'tests/Unit/DSC_.Tests.ps1' -Output Detailed +``` + +### Run class resource unit tests + +```powershell +Invoke-Pester -Path 'tests/Unit/Classes/.Tests.ps1' -Output Detailed +``` + +> **NEVER run integration tests locally.** Integration tests require a full Windows +> environment configured as a DSC node and run only in CI. They may change system state. + +## Key Conventions + +- **Localized strings:** All `Write-Verbose`, `Write-Error`, and exception messages must use + `$script:localizedData` keys. Load strings with `Get-LocalizedData` at the top of each + `.psm1`. +- **DscResource.Common:** Check + [`DscResource.Common`](https://github.com/dsccommunity/DscResource.Common/wiki) before + writing new helper functions. Use `New-InvalidOperationException`, `New-ArgumentException`, + and similar helpers instead of `throw`. +- **CHANGELOG.md:** Every code change must add an entry to the `Unreleased` section. +- **New resources:** Prefer class-based resources; use MOF-based only when required. + +## External Resources + +- [DSC Community Guidelines](https://dsccommunity.org/guidelines/) +- [DSC Community Blog](https://dsccommunity.org/blog/) +- [DscResource.Common wiki](https://github.com/dsccommunity/DscResource.Common/wiki) +- [DscResource.Base](https://github.com/dsccommunity/DscResource.Base) +- [DscResource.Test](https://github.com/dsccommunity/DscResource.Test) + +## Detailed Guidance + +See `.github/instructions/` for detailed style guidelines on PowerShell, Pester, unit tests, +integration tests, MOF resources, class-based resources, localization, markdown, and changelog. diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c5e898..e5a7e6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `AGENTS.md` at repository root with build/test workflow, resource type overview, + and key conventions for AI agents - Fixes [Issue #464](https://github.com/dsccommunity/ComputerManagementDsc/issues/464). +- Added `tests/AGENTS.md` with Pester v5 guidance, test location map, and required setup + block templates for MOF and class-based resource tests. +- Added `.github/copilot-instructions.md` with workspace-level GitHub Copilot instructions. +- Added `.github/instructions/` directory with 11 instruction files covering PowerShell + style, Pester, unit tests, integration tests, MOF resources, class-based resources, + localization, changelog, and markdown guidelines. + ### Changed - `azure-pipelines.yml` diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..faa45df5 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,169 @@ +# tests/ — AI Agent Guide + +## Test Framework + +Use [Pester](https://pester.dev/) v5. Pester 4 syntax is forbidden. + +### Pester 4 → 5 Key Changes + +| Pester 4 | Pester 5 | +|----------|----------| +| `Assert-MockCalled` | `Should -Invoke` | +| `Should -Be $true` | `Should -BeTrue` | +| `Should -Be $false` | `Should -BeFalse` | +| Script-level test case variables | `BeforeDiscovery` block | +| `-ModuleName` on each call | `$PSDefaultParameterValues` with `Mock:ModuleName` etc. | + +All Pester keywords (`Describe`, `Context`, `It`, `BeforeAll`, `AfterAll`, `BeforeEach`, +`AfterEach`) use PascalCase. + +## Test Locations + +| Type | Location | +|------|----------| +| MOF resource unit tests | `tests/Unit/DSC_.Tests.ps1` | +| Class resource unit tests | `tests/Unit/Classes/.Tests.ps1` | +| Integration tests | `tests/Integration/` (one `.config.ps1` + one `.Integration.Tests.ps1` per resource) | + +## Required Setup Block + +### MOF Resource Unit Test + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + $script:dscResourceName = 'DSC_' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscResourceName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Restore-TestEnvironment -TestEnvironment $script:testEnvironment + + Get-Module -Name $script:dscResourceName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +### Class-based Resource Unit Test + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + # Same DscResource.Test bootstrap as MOF unit tests above +} + +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + + Import-Module -Name $script:dscModuleName + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +### Integration Test + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + # Same DscResource.Test bootstrap as unit tests above + + $script:dscModuleName = 'ComputerManagementDsc' + $script:dscResourceName = 'DSC_' + + $script:skipIntegrationTests = $false +} + +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + $script:dscResourceName = 'DSC_' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +AfterAll { + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} +``` + +## Running Tests + +```powershell +# All unit tests (new pwsh session recommended after class-based resource changes) +Invoke-Pester -Path 'tests/Unit' -Output Detailed + +# Specific MOF resource unit test +Invoke-Pester -Path 'tests/Unit/DSC_.Tests.ps1' -Output Detailed + +# Specific class resource unit test +Invoke-Pester -Path 'tests/Unit/Classes/.Tests.ps1' -Output Detailed +``` + +> **NEVER run integration tests locally.** Integration tests run only in CI and may change +> system state. From 1e19a64deee1d2489496cb35fa73e7461d85028d Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Sun, 21 Jun 2026 20:22:11 +1200 Subject: [PATCH 2/6] CHORE: Update guidelines and instructions for tests - Refactor integration and unit test guidelines for clarity. - Add new Pester testing guidelines for consistency. - Remove outdated ComputerManagementDsc-specific guidelines. - Update README to reflect requirements for class-based resources. --- .github/copilot-instructions.md | 31 ++++++- ...erManagementDsc-guidelines.instructions.md | 37 -------- ...ommunity-integration-tests.instructions.md | 42 +++------ ...d => dsc-community-pester.instructions.md} | 51 ++++------- .../dsc-community-unit-tests.instructions.md | 90 +++---------------- README.md | 4 +- 6 files changed, 73 insertions(+), 182 deletions(-) delete mode 100644 .github/instructions/ComputerManagementDsc-guidelines.instructions.md rename .github/instructions/{dsc-community-pester-4.instructions.md => dsc-community-pester.instructions.md} (65%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f8fdad75..ce5ad6a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -36,7 +36,8 @@ This repository contains two types of DSC resources: `Get-TargetResource`, `Test-TargetResource`, and `Set-TargetResource` - **Class-based resources** (`source/Classes/..ps1`) — inherit `ResourceBase` from `DscResource.Base`; implement `GetCurrentState()` and `Modify()` -- Prefer class-based resources; use MOF-based only when required. +- Prefer class-based resources; use MOF-based only when required + (e.g. WMF 4.0 support). ## File Organization @@ -47,14 +48,36 @@ This repository contains two types of DSC resources: - Unit tests (class): `tests/Unit/Classes/.Tests.ps1` - Integration tests: `tests/Integration/.Integration.Tests.ps1` +## Naming Conventions + +- MOF-based resources: `DSC_` prefix on all files and all exported + functions (e.g. `DSC_TimeZone.psm1`; `Get-TargetResource` is exported via + `*-TargetResource`). +- Class-based resources: PascalCase class name; file prefix + `..ps1` where the number is the dependency + group (e.g. `020.PSResourceRepository.ps1`). + +## Build & Test Workflow + +- Never use VS Code tasks; always use PowerShell scripts via terminal from the + repository root. +- Setup build and test environment (once per `pwsh` session): + `./build.ps1 -Tasks noop` +- Build project before running tests: `./build.ps1 -Tasks build` +- Run all unit tests: `Invoke-Pester -Path 'tests/Unit' -Output Detailed` +- Run a specific MOF resource unit test: + `Invoke-Pester -Path 'tests/Unit/DSC_.Tests.ps1' -Output Detailed` +- Run a specific class resource unit test: + `Invoke-Pester -Path 'tests/Unit/Classes/.Tests.ps1' -Output Detailed` +- Never run integration tests locally. +- Run unit tests in a new `pwsh` session after changing class-based resources. + ## Instruction Files Read the following instruction files before working on the corresponding areas: -- `.github/instructions/ComputerManagementDsc-guidelines.instructions.md` — build/test - workflow and naming conventions - `.github/instructions/dsc-community-powershell.instructions.md` — PowerShell style -- `.github/instructions/dsc-community-pester-4.instructions.md` — Pester test style +- `.github/instructions/dsc-community-pester.instructions.md` — Pester test style - `.github/instructions/dsc-community-unit-tests.instructions.md` — unit test setup and patterns - `.github/instructions/dsc-community-integration-tests.instructions.md` — integration test patterns - `.github/instructions/dsc-community-mof-resource.instructions.md` — MOF resource implementation diff --git a/.github/instructions/ComputerManagementDsc-guidelines.instructions.md b/.github/instructions/ComputerManagementDsc-guidelines.instructions.md deleted file mode 100644 index 1e858e37..00000000 --- a/.github/instructions/ComputerManagementDsc-guidelines.instructions.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -description: ComputerManagementDsc-specific guidelines for AI development. -applyTo: "**" ---- - -# ComputerManagementDsc Requirements - -## Build & Test Workflow Requirements - -- Never use VS Code tasks; always use PowerShell scripts via terminal from the repository root. -- Setup build and test environment (once per `pwsh` session): `./build.ps1 -Tasks noop` -- Build project before running tests: `./build.ps1 -Tasks build` -- Run all unit tests: `Invoke-Pester -Path 'tests/Unit' -Output Detailed` -- Run a specific MOF resource test: - `Invoke-Pester -Path 'tests/Unit/DSC_.Tests.ps1' -Output Detailed` -- Run a specific class resource test: - `Invoke-Pester -Path 'tests/Unit/Classes/.Tests.ps1' -Output Detailed` -- Never run integration tests locally. - -## Naming - -- MOF-based resources: `DSC_` prefix on all files and exported functions - (e.g. `DSC_TimeZone.psm1`, `Get-TargetResource` is exported via `*-TargetResource`) -- Class-based resources: PascalCase class name; file prefix `..ps1` where - `N` is the dependency group number (e.g. `020.PSResourceRepository.ps1`) - -## Resource Type Selection - -- Implement new resources as class-based (`source/Classes/`) unless MOF-based is required - (e.g. WMF 4.0 support). - -## Tests - -- MOF unit tests: `tests/Unit/DSC_.Tests.ps1` -- Class unit tests: `tests/Unit/Classes/.Tests.ps1` -- Integration tests: `tests/Integration/` (flat) -- Run unit tests in a new `pwsh` session after changing class-based resources. diff --git a/.github/instructions/dsc-community-integration-tests.instructions.md b/.github/instructions/dsc-community-integration-tests.instructions.md index 4cee83b9..5a704f01 100644 --- a/.github/instructions/dsc-community-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-integration-tests.instructions.md @@ -1,27 +1,25 @@ --- -description: Guidelines for implementing integration tests for DSC resources. +description: Guidelines for implementing integration tests for commands. applyTo: "tests/[iI]ntegration/**/*.[iI]ntegration.[tT]ests.ps1" -version: 1.0.0 --- # Integration Tests Guidelines ## Requirements - -- Location: `tests/Integration/.Integration.Tests.ps1` -- No mocking — real environment only +- Location Commands: `tests/Integration/Commands/{CommandName}.Integration.Tests.ps1` +- Location Resources: `tests/Integration/Resources/{ResourceName}.Integration.Tests.ps1` +- No mocking - real environment only - Cover all scenarios and code paths - Use `Get-ComputerName` for computer names in CI - Avoid `ExpectedMessage` for `Should -Throw` assertions -- Run integration tests in CI only unless explicitly instructed otherwise -- Call commands with `-Force` where applicable (avoids prompting) -- Use `-ErrorAction 'Stop'` so failures surface immediately +- Only run integration tests in CI unless explicitly instructed. +- Call commands with `-Force` parameter where applicable (avoids prompting). +- Use `-ErrorAction 'Stop'` on commands so failures surface immediately ## Required Setup Block ```powershell -# Suppressing this rule because Script Analyzer does not understand Pester's syntax. -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] param () BeforeDiscovery { @@ -33,7 +31,7 @@ BeforeDiscovery { if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) { # Redirect all streams to $null, except the error stream (stream 2) - & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null } # If the dependencies have not been resolved, this will throw an error. @@ -42,29 +40,13 @@ BeforeDiscovery { } catch [System.IO.FileNotFoundException] { - throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' } - - $script:dscModuleName = '' - $script:dscResourceName = 'DSC_' - - $script:skipIntegrationTests = $false } BeforeAll { - $script:dscModuleName = '' - $script:dscResourceName = 'DSC_' - - $script:testEnvironment = Initialize-TestEnvironment ` - -DSCModuleName $script:dscModuleName ` - -DSCResourceName $script:dscResourceName ` - -ResourceType 'Mof' ` - -TestType 'Integration' - - Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') -} + $script:moduleName = '{MyModuleName}' -AfterAll { - Restore-TestEnvironment -TestEnvironment $script:testEnvironment + Import-Module -Name $script:moduleName -ErrorAction 'Stop' } ``` diff --git a/.github/instructions/dsc-community-pester-4.instructions.md b/.github/instructions/dsc-community-pester.instructions.md similarity index 65% rename from .github/instructions/dsc-community-pester-4.instructions.md rename to .github/instructions/dsc-community-pester.instructions.md index cc510f55..bc90da8e 100644 --- a/.github/instructions/dsc-community-pester-4.instructions.md +++ b/.github/instructions/dsc-community-pester.instructions.md @@ -1,82 +1,69 @@ --- description: Guidelines for writing and maintaining tests using Pester. applyTo: "**/*.[Tt]ests.ps1" -version: 1.0.0 --- # Tests Guidelines ## Core Requirements - -- All public commands, private functions, and classes must have unit tests +- All public commands, private functions and classes must have unit tests - All public commands and class-based resources must have integration tests +- Use Pester v5 syntax only - Test code only inside `Describe` blocks - Assertions only in `It` blocks -- Never test verbose messages, debug messages, or parameter binding behavior +- Never test verbose messages, debug messages or parameter binding behavior - Pass all mandatory parameters to avoid prompts ## Requirements - - Inside `It` blocks, assign unused return objects to `$null` (unless part of pipeline) -- Call the tested entity from within `It` blocks +- Tested entity must be called from within the `It` blocks - Keep results and assertions in same `It` block -- Avoid try-catch-finally for cleanup; use `AfterAll` or `AfterEach` +- Avoid try-catch-finally for cleanup, use `AfterAll` or `AfterEach` - Avoid unnecessary remove/recreate cycles ## Naming - - One `Describe` block per file matching the tested entity name - `Context` descriptions start with 'When' -- `It` descriptions start with 'Should'; must not contain 'when' +- `It` descriptions start with 'Should', must not contain 'when' - Mock variables prefix: 'mock' ## Structure & Scope - -- Public commands: Never use `InModuleScope` (unless retrieving localized strings or - creating an object using an internal class) +- Public commands: Never use `InModuleScope` (unless retrieving localized strings or creating an object using an internal class) - Private functions/class resources: Always use `InModuleScope` - Each class method = separate `Context` block - Each scenario = separate `Context` block - Use nested `Context` blocks for complex scenarios - Mocking in `BeforeAll` (`BeforeEach` only when required) -- Setup/teardown in `BeforeAll`, `BeforeEach`/`AfterAll`, `AfterEach` close to usage +- Setup/teardown in `BeforeAll`,`BeforeEach`/`AfterAll`,`AfterEach` close to usage - Spacing between blocks, arrange, act, and assert for readability ## Syntax Rules - -- PascalCase: `Describe`, `Context`, `It`, `Should`, `BeforeAll`, `BeforeEach`, `AfterAll`, - `AfterEach` +- PascalCase: `Describe`, `Context`, `It`, `Should`, `BeforeAll`, `BeforeEach`, `AfterAll`, `AfterEach` - Use `-BeTrue`/`-BeFalse` never `-Be $true`/`-Be $false`/`-Contain $true`/`-Contain $false` -- Never use `Assert-MockCalled`; use `Should -Invoke` instead -- No `Should -Not -Throw` — invoke commands directly +- Never use `Assert-MockCalled`, use `Should -Invoke` instead +- No `Should -Not -Throw` - invoke commands directly - Never add an empty `-MockWith` block - Omit `-MockWith` when returning `$null` -- Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, - `InModuleScope:ModuleName` +- Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, `InModuleScope:ModuleName` - Omit `-ModuleName` parameter on Pester commands -- Never use `Mock` inside `InModuleScope` block -- Never use `param()` block inside `-MockWith` scriptblocks; parameters are auto-bound -- In `InModuleScope` tests, add `Set-StrictMode -Version 1.0` immediately before invoking - the tested function +- Never use `Mock` inside `InModuleScope`-block +- Never use `param()`-block inside `-MockWith` scriptblocks, parameters are auto-bound +- In `InModuleScope` tests, add `Set-StrictMode -Version 1.0` immediately before invoking the tested function - Use `Should -Invoke -Exactly -Times -Scope It` for call-count assertions - - Assert `` calls inside the `It` block; do not assert call counts across an entire - `Describe` or `Context` + - Assert calls inside the `It` block; do not assert call counts across an entire `Describe` or `Context` ## File Organization - - Class resources: `tests/Unit/Classes/{Name}.Tests.ps1` -- MOF resources: `tests/Unit/DSC_{Name}.Tests.ps1` +- Public commands: `tests/Unit/Public/{Name}.Tests.ps1` +- Private functions: `tests/Unit/Private/{Name}.Tests.ps1` ## Data-Driven Tests (Test Cases) - - Define `-ForEach` variables in separate `BeforeDiscovery` (close to usage) - `-ForEach` allowed on `Context` and `It` blocks - Never add `param()` inside Pester blocks when using `-ForEach` - Access test case properties directly: `$PropertyName` ## Best Practices - - Cover all scenarios and code paths - Use `BeforeEach` and `AfterEach` sparingly -- Use `$PSDefaultParameterValues` only for Pester commands (`Describe`, `Context`, `It`, - `Mock`, `Should`, `InModuleScope`) +- Use `$PSDefaultParameterValues` only for Pester commands (`Describe`, `Context`, `It`, `Mock`, `Should`, `InModuleScope`) diff --git a/.github/instructions/dsc-community-unit-tests.instructions.md b/.github/instructions/dsc-community-unit-tests.instructions.md index bf2d50c1..4f33140f 100644 --- a/.github/instructions/dsc-community-unit-tests.instructions.md +++ b/.github/instructions/dsc-community-unit-tests.instructions.md @@ -1,23 +1,21 @@ --- description: Guidelines for writing and maintaining unit tests using Pester. applyTo: "tests/[Uu]nit/**/*.[Tt]ests.ps1" -version: 1.0.0 --- # Unit Tests Guidelines -- Test localized strings: `InModuleScope -ScriptBlock { $script:localizedData.Key }` -- Mock files: use `$TestDrive` +- Test with localized strings: Use `InModuleScope -ScriptBlock { $script:localizedData.Key }` +- Mock files: Use `$TestDrive` variable (path to the test drive) - All public commands require parameter set validation tests -- Run tests in a new session after modifying class-based resources +- After modifying classes, always run tests in new session (for changes to take effect) ## Test Setup Requirements -### MOF Resource Unit Test Setup Block +Use this exact setup block before `Describe`: ```powershell -# Suppressing this rule because Script Analyzer does not understand Pester's syntax. -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] param () BeforeDiscovery { @@ -29,7 +27,7 @@ BeforeDiscovery { if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) { # Redirect all streams to $null, except the error stream (stream 2) - & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null } # If the dependencies have not been resolved, this will throw an error. @@ -38,93 +36,31 @@ BeforeDiscovery { } catch [System.IO.FileNotFoundException] { - throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' } } BeforeAll { - $script:dscModuleName = '' - $script:dscResourceName = 'DSC_' + $script:moduleName = '{MyModuleName}' - $script:testEnvironment = Initialize-TestEnvironment ` - -DSCModuleName $script:dscModuleName ` - -DSCResourceName $script:dscResourceName ` - -ResourceType 'Mof' ` - -TestType 'Unit' + Import-Module -Name $script:moduleName -ErrorAction 'Stop' - Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscResourceName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscResourceName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscResourceName -} - -AfterAll { - $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') - $PSDefaultParameterValues.Remove('Mock:ModuleName') - $PSDefaultParameterValues.Remove('Should:ModuleName') - - Restore-TestEnvironment -TestEnvironment $script:testEnvironment - - Get-Module -Name $script:dscResourceName -All | Remove-Module -Force - Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force -} -``` - -### Class-based Resource Unit Test Setup Block - -```powershell -# Suppressing this rule because Script Analyzer does not understand Pester's syntax. -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] -param () - -BeforeDiscovery { - try - { - if (-not (Get-Module -Name 'DscResource.Test')) - { - if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) - { - & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null - } - - Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' - } - } - catch [System.IO.FileNotFoundException] - { - throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' - } -} - -BeforeAll { - $script:dscModuleName = '' - - Import-Module -Name $script:dscModuleName - - Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\TestHelpers\CommonTestHelper.psm1') - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName } AfterAll { $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') $PSDefaultParameterValues.Remove('Mock:ModuleName') $PSDefaultParameterValues.Remove('Should:ModuleName') - - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force - Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force } ``` ## Required Test Templates ### Parameter Set Validation - Single parameter set: - ```powershell It 'Should have the correct parameters in parameter set ' -ForEach @( @{ @@ -138,7 +74,6 @@ It 'Should have the correct parameters in parameter set Date: Sun, 21 Jun 2026 21:25:06 +1200 Subject: [PATCH 3/6] CHORE: Update documentation guidelines for various resources - Enhanced changelog guidelines for consistency - Added detailed instructions for MOF and class-based resources - Improved Pester testing guidelines and setup instructions - Clarified PowerShell script and module writing standards - Updated localization and unit testing documentation --- .../dsc-community-changelog.instructions.md | 11 + ...c-community-class-resource.instructions.md | 66 ++++++ ...ommunity-integration-tests.instructions.md | 96 ++++++++ ...dsc-community-localization.instructions.md | 52 +++++ .../dsc-community-markdown.instructions.md | 6 +- ...dsc-community-mof-resource.instructions.md | 90 ++++++++ .../dsc-community-pester.instructions.md | 145 ++++++++++++ .../dsc-community-powershell.instructions.md | 83 +++++++ .../dsc-community-unit-tests.instructions.md | 209 ++++++++++++++++++ 9 files changed, 756 insertions(+), 2 deletions(-) diff --git a/.github/instructions/dsc-community-changelog.instructions.md b/.github/instructions/dsc-community-changelog.instructions.md index 5039f0b2..7224a44d 100644 --- a/.github/instructions/dsc-community-changelog.instructions.md +++ b/.github/instructions/dsc-community-changelog.instructions.md @@ -14,3 +14,14 @@ version: 1.0.0 - No empty lines between list items in same section - Skip adding entry if same change already exists in Unreleased section - No duplicate sections or items in Unreleased section +- Issue reference format: `Fixes [Issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/).` + — capital `I` in `Issue`, `Fixes` keyword prefix, full stop after the closing parenthesis +- Mark breaking changes with `BREAKING CHANGE:` prefix on the entry in `### Changed` + or `### Fixed` +- Group multiple changes for the same resource using two-level indentation: + + ```markdown + - ResourceName + - First change description for this resource + - Second change description for this resource + ``` diff --git a/.github/instructions/dsc-community-class-resource.instructions.md b/.github/instructions/dsc-community-class-resource.instructions.md index acfae5d6..a9e25d03 100644 --- a/.github/instructions/dsc-community-class-resource.instructions.md +++ b/.github/instructions/dsc-community-class-resource.instructions.md @@ -100,3 +100,69 @@ Add to `.DESCRIPTION` section: - [`New-ObjectNotFoundException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ObjectNotFoundException) - [`New-InvalidResultException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidResultException) - [`New-NotImplementedException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91NotImplementedException) + +## Property Attributes + +- `[DscProperty(Key)]` — key (identity) properties; always mark at least one per resource +- `[DscProperty(Mandatory)]` — required (non-key) properties +- `[DscProperty()]` — optional properties +- `[DscProperty(NotConfigurable)]` — read-only / computed properties not configurable by the + user (e.g. `Reasons`) +- `[ValidateSet('Value1', 'Value2')]` — restrict allowed values on string properties +- For Enum-typed properties, set a default value: + + ```powershell + [DscProperty()] + [Ensure] + $Ensure = [Ensure]::Present + ``` + +## Machine Configuration Compliance (`Reasons`) + +Resources that support Azure Policy / Machine Configuration compliance auditing must include +a `Reasons` property using the `CMReason` helper class: + +```powershell +[DscProperty(NotConfigurable)] +[CMReason[]] +$Reasons +``` + +## Class-Level Comment-Based Help + +Place a comment block **above** the `[DscResource()]` decoration with: + +- `.SYNOPSIS` — one-line description of the resource +- `.PARAMETER` — one entry per DSC property (name + description) +- `.EXAMPLE` — at least one `Invoke-DscResource` usage example + +```powershell +<# + .SYNOPSIS + A resource to manage... + + .PARAMETER Ensure + Specifies whether the resource should be Present or Absent. + + .PARAMETER Name + Specifies the name. + + .EXAMPLE + Invoke-DscResource -Name 'MyResource' -ModuleName 'MyModule' -Method 'Get' -Property @{ + Name = 'Value' + Ensure = 'Present' + } +#> +[DscResource()] +class MyResource : ResourceBase +``` + +## Localized Data in Class Resources + +- Strings are loaded automatically by `ResourceBase` when passing `$PSScriptRoot` to the + base constructor: `MyResource () : base ($PSScriptRoot)` +- Access localized strings via `$this.localizedData.KeyName` (not `$script:localizedData`) + + ```powershell + Write-Verbose -Message ($this.localizedData.GetTargetResourceMessage -f $this.Name) + ``` diff --git a/.github/instructions/dsc-community-integration-tests.instructions.md b/.github/instructions/dsc-community-integration-tests.instructions.md index 5a704f01..dabd958c 100644 --- a/.github/instructions/dsc-community-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-integration-tests.instructions.md @@ -50,3 +50,99 @@ BeforeAll { Import-Module -Name $script:moduleName -ErrorAction 'Stop' } ``` + +## DSC Resource Test Variables + +Define these variables in **both** `BeforeDiscovery` (for discovery-phase use) and `BeforeAll` +(for runtime use): + +```powershell +$script:dscModuleName = 'ComputerManagementDsc' +$script:dscResourceName = 'DSC_{ResourceName}' # for MOF resources +$script:skipIntegrationTests = $false +``` + +## Required Test Environment Setup (MOF Resources) + +```powershell +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + $script:dscResourceName = 'DSC_{ResourceName}' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Integration' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +AfterAll { + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} +``` + +## Config File Pattern + +- Name the companion config file `{DSC_ResourceName}.config.ps1` (or `.Config.ps1`) +- Dot-source it in the `Describe BeforeAll`: + + ```powershell + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile -Verbose -ErrorAction Stop + ``` + +## Configuration Naming Convention + +Configuration functions must follow the pattern `DSC_{ResourceName}_{Purpose}_Config`: + +```powershell +Configuration DSC_TimeZone_SetTimeZone_Config { ... } +Configuration DSC_SmbShare_CreateShare1_Config { ... } +Configuration DSC_SmbShare_Cleanup_Config { ... } +``` + +## Prerequisites and Cleanup Configurations + +When a test requires external state (users, folders, shares, registry keys), define: + +- A `_Prerequisites_Config` configuration at the start of the test suite +- A `_Cleanup_Config` configuration at the end of the test suite + +## Standard Test Sequence + +Each configuration `Context` block must contain these `It` tests in order: + +1. `'Should compile the MOF without throwing'` — compile with `$TestDrive` as output: + + ```powershell + & $configName -OutputPath $TestDrive -ConfigurationData $configData + ``` + +2. `'Should apply the MOF without throwing'` — reset LCM then start configuration: + + ```powershell + Reset-DscLcm + Start-DscConfiguration -Path $TestDrive -ComputerName 'localhost' -Wait -Verbose -Force -ErrorAction Stop + ``` + +3. `'Should be able to call Get-DscConfiguration without throwing'`: + + ```powershell + Get-DscConfiguration -Verbose -ErrorAction Stop + ``` + +4. `'Should have set the resource and all the parameters should match'` — property assertions + +5. *(optional)* `'Should return $true when Test-DscConfiguration is run'`: + + ```powershell + Test-DscConfiguration -Verbose | Should -BeTrue + ``` + +## Key Rules + +- Always call `Reset-DscLcm` immediately before `Start-DscConfiguration` +- Use `$TestDrive` as the `-OutputPath` when compiling MOF configurations +- Use `'localhost'` as `ComputerName` in `Start-DscConfiguration` diff --git a/.github/instructions/dsc-community-localization.instructions.md b/.github/instructions/dsc-community-localization.instructions.md index 83368ce2..9037c914 100644 --- a/.github/instructions/dsc-community-localization.instructions.md +++ b/.github/instructions/dsc-community-localization.instructions.md @@ -44,3 +44,55 @@ ConvertFrom-StringData @' ```powershell Write-Verbose -Message ($script:localizedData.KeyName -f $value1) ``` + +## Loading Mechanism by Resource Type + +### MOF-based resources + +At module scope, after importing `DscResource.Common`: + +```powershell +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +``` + +Access strings as `$script:localizedData.KeyName`. + +### Class-based resources + +Pass `$PSScriptRoot` to the `ResourceBase` base constructor in the class constructor: + +```powershell +MyResourceName () : base ($PSScriptRoot) +{ + ... +} +``` + +The `ResourceBase` class loads the strings automatically. +Access strings as `$this.localizedData.KeyName` — **not** `$script:localizedData`: + +```powershell +Write-Verbose -Message ($this.localizedData.GetTargetResourceMessage -f $this.Name) +``` + +## Error Messages + +Pass localized error strings to `DscResource.Common` exception helpers rather than calling +`$PSCmdlet.ThrowTerminatingError()` directly: + +```powershell +$errorMessage = $script:localizedData.SomeErrorKey -f $value +New-InvalidOperationException -Message $errorMessage +New-ArgumentException -ArgumentName 'ParameterName' -Message $errorMessage +``` + +## String File Header + +Optionally include `# culture="en-US"` as the first line of a `.strings.psd1` file: + +```powershell +# culture="en-US" +ConvertFrom-StringData -StringData @' + KeyName = Message text. (PREFIX0001) +'@ +``` diff --git a/.github/instructions/dsc-community-markdown.instructions.md b/.github/instructions/dsc-community-markdown.instructions.md index 619ef616..ab849ae0 100644 --- a/.github/instructions/dsc-community-markdown.instructions.md +++ b/.github/instructions/dsc-community-markdown.instructions.md @@ -6,9 +6,11 @@ version: 1.0.0 # Markdown Style Guidelines -- Wrap lines at word boundaries when over 80 characters (except tables/code blocks) +- Wrap lines at word boundaries when over 80 characters (except tables/code blocks, + badge/shield image lines, and URLs inside markdown links) - Use 2 spaces for indentation -- Use '1.' for all items in ordered lists (1/1/1 numbering style) +- Use `1.` for all items in ordered lists (1/1/1 numbering style) +- Use `-` for all unordered list items (never `*` or `+`) - Disable `MD013` rule by adding a comment for tables/code blocks exceeding 80 characters - Empty lines required before/after code blocks and headings (except before line 1) - Escape backslashes in file paths only (not in code blocks) diff --git a/.github/instructions/dsc-community-mof-resource.instructions.md b/.github/instructions/dsc-community-mof-resource.instructions.md index 1cf39247..fad60af5 100644 --- a/.github/instructions/dsc-community-mof-resource.instructions.md +++ b/.github/instructions/dsc-community-mof-resource.instructions.md @@ -58,3 +58,93 @@ version: 1.0.0 - In `.strings.psd1` files, use underscores as word separators in localized string key names (for multi-word keys) + +## Function Requirements + +- All three functions must include `[CmdletBinding()]` +- `Get-TargetResource` must declare `[OutputType([System.Collections.Hashtable])]` +- `Test-TargetResource` must declare `[OutputType([System.Boolean])]` +- `Set-TargetResource` omits `[OutputType()]` + +## Module Import Boilerplate + +At the top of every `.psm1`, import the required helper modules before loading localized data: + +```powershell +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) ` + -ChildPath 'Modules' + +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +``` + +## Script-Scoped Module Variables + +Define constants and shared lookup tables at module scope using `$script:` prefix: + +```powershell +$script:registryKey = 'HKLM:\SOFTWARE\...' +$script:parameterNames = @('Param1', 'Param2') +``` + +## Reboot Signalling (`$global:DSCMachineStatus`) + +When a resource must signal that a reboot is required, use the pattern below. Include a +`SuppressRestart` parameter to allow callers to suppress the reboot signal. +Suppress the PSScriptAnalyzer warning at module top: + +```powershell +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', + Justification = 'DSC requires $global:DSCMachineStatus to signal a reboot.')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Script Analyzer does not understand Pester syntax.')] +param () + +# ...inside Set-TargetResource: +if (-not $SuppressRestart) +{ + $global:DSCMachineStatus = 1 +} +``` + +## Private Helper Functions + +Resource-specific helper functions may be defined in the `.psm1` file below the three +required functions. Extract logic to `ComputerManagementDsc.Common` only when it is +needed by multiple resources. + +## Preferred Comparison Helper + +Use `Test-DscParameterState` from `DscResource.Common` as the preferred mechanism for +comparing current vs. desired state in `Test-TargetResource`. + +## Mutual Exclusivity Validation + +Use `Assert-BoundParameter` from `DscResource.Common` to validate mutually exclusive +parameter combinations in `Set-TargetResource` and `Test-TargetResource`. + +## Schema (.schema.mof) Structure + +Every MOF resource requires a `.schema.mof` file in the same directory: + +- Declare `ClassVersion` and `FriendlyName` in the class qualifier line +- Inherit from `OMI_BaseResource` +- Qualifier types: + - `[Key]` — key (identity) property + - `[Required]` — mandatory input property + - `[Write]` — optional input property + - `[Read]` — output-only / computed property + +```mof +[ClassVersion("1.0.0"), FriendlyName("TimeZone")] +class DSC_TimeZone : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance.")] String IsSingleInstance; + [Required, Description("The desired time zone.")] String TimeZone; +}; +``` diff --git a/.github/instructions/dsc-community-pester.instructions.md b/.github/instructions/dsc-community-pester.instructions.md index bc90da8e..a17df398 100644 --- a/.github/instructions/dsc-community-pester.instructions.md +++ b/.github/instructions/dsc-community-pester.instructions.md @@ -67,3 +67,148 @@ applyTo: "**/*.[Tt]ests.ps1" - Cover all scenarios and code paths - Use `BeforeEach` and `AfterEach` sparingly - Use `$PSDefaultParameterValues` only for Pester commands (`Describe`, `Context`, `It`, `Mock`, `Should`, `InModuleScope`) + +## File Boilerplate + +Every test file must begin with: + +```powershell +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () +``` + +## Describe Block Naming and Tags + +- MOF resources: `'DSC_{ResourceName}\{FunctionName}'` with a tag matching the function: + + ```powershell + Describe 'DSC_TimeZone\Get-TargetResource' -Tag 'Get' { ... } + Describe 'DSC_TimeZone\Set-TargetResource' -Tag 'Set' { ... } + Describe 'DSC_TimeZone\Test-TargetResource' -Tag 'Test' { ... } + ``` + +- Class-based resources: `'{ClassName}\{MethodName}()'` with a matching tag: + + ```powershell + Describe 'PSResourceRepository\GetCurrentState()' -Tag 'GetCurrentState' { ... } + Describe 'PSResourceRepository\Modify()' -Tag 'Modify' { ... } + Describe 'PSResourceRepository\AssertProperties()' -Tag 'AssertProperties' { ... } + ``` + +- Standard tags: `Get`, `Set`, `Test`, `Modify`, `GetCurrentState`, `AssertProperties`, + `Private` + +## File Organization — DSC Resources + +- MOF resource unit tests: `tests/Unit/DSC_{ResourceName}.Tests.ps1` +- Integration tests: `tests/Integration/DSC_{ResourceName}.Integration.Tests.ps1` + (note `_Integration` suffix on the `Describe` block name as well) + +## Class Resource Method Mocking + +Class methods cannot be mocked with `Mock`. Use `Add-Member -MemberType ScriptMethod`: + +```powershell +$mockInstance = [MyResource] @{ Name = 'Test' } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return [System.Collections.Hashtable] @{ Name = 'Test'; Ensure = 'Present' } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru +``` + +Methods commonly stubbed: `GetCurrentState`, `AssertProperties`, `Compare`, `Modify`. + +## Script-Scoped Call Counter (Class Methods) + +Since `Should -Invoke` cannot track class method calls, use a `$script:` counter: + +```powershell +BeforeAll { + InModuleScope -ScriptBlock { + $script:mockInstance = [MyResource] @{ Name = 'Test' } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { + $script:mockMethodModifyCallCount += 1 + } -PassThru + } +} + +BeforeEach { + InModuleScope -ScriptBlock { + $script:mockMethodModifyCallCount = 0 + } +} + +It 'Should call Modify() exactly once' { + InModuleScope -ScriptBlock { + $script:mockInstance.Set() + $script:mockMethodModifyCallCount | Should -Be 1 + } +} +``` + +## `InModuleScope -Parameters $_` for Data-Driven Tests + +When using `-TestCases` or `-ForEach`, pass parameters into `InModuleScope` with +`-Parameters $_`: + +```powershell +It 'Should set the correct value for ' -TestCases $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + Set-StrictMode -Version 1.0 + { Set-TargetResource -Property $Property } | Should -Not -Throw + } +} +``` + +## Cross-Module Mocking + +To mock a function from a different module (e.g., a helper module), pass `-ModuleName` +explicitly on `Mock` even when `$PSDefaultParameterValues` is set: + +```powershell +Mock -ModuleName 'ComputerManagementDsc.Common' -CommandName 'Get-TimeZoneId' -MockWith { + return 'Pacific Standard Time' +} +``` + +## `ParameterFilter` with Catch-All + +When mocking the same command for different parameter combinations, add a final catch-all +mock (no filter) that throws to detect unexpected calls: + +```powershell +Mock -CommandName Get-RegistryPropertyValue ` + -ParameterFilter { $Name -eq 'FilterAdministratorToken' } ` + -MockWith { return 1 } + +Mock -CommandName Get-RegistryPropertyValue ` + -MockWith { throw 'Called with unexpected parameter values.' } +``` + +## `BeforeDiscovery` Inside `Describe` for Test Cases + +Define test case arrays in a `BeforeDiscovery` block inside the `Describe` block so they +are available during the discovery phase: + +```powershell +Describe 'MyResource\Set()' -Tag 'Set' { + BeforeDiscovery { + $testCases = @( + @{ Value = 'A'; Expected = 1 } + @{ Value = 'B'; Expected = 2 } + ) + } + + Context 'When value is ' -ForEach $testCases { + It 'Should return ' { + InModuleScope -Parameters $_ -ScriptBlock { + Set-StrictMode -Version 1.0 + # ... + } + } + } +} +``` diff --git a/.github/instructions/dsc-community-powershell.instructions.md b/.github/instructions/dsc-community-powershell.instructions.md index 1b04e1c4..82da9462 100644 --- a/.github/instructions/dsc-community-powershell.instructions.md +++ b/.github/instructions/dsc-community-powershell.instructions.md @@ -242,3 +242,86 @@ if ($Force.IsPresent -and -not $Confirm) ## Module Manifest - Don't use `NestedModules` for shared commands without `RootModule` + +## Closing Brace Comments + +In complex functions or long script blocks, add a trailing comment on the closing brace to +identify the construct it closes: + +```powershell +} # function Get-TargetResource +} # if ($Ensure -eq 'Present') +} # try +} # foreach ($item in $collection) +``` + +## Hashtable and Splatting Alignment + +When assigning multiple properties to a hashtable or building a splatting block, align the +`=` operators in a column for readability: + +```powershell +$returnValue = @{ + Name = $repository.Name + SourceLocation = $repository.SourceLocation + PublishLocation = $repository.PublishLocation + Ensure = 'Present' +} +``` + +## Class-Level Comment-Based Help + +For PowerShell class files, place comment-based help **above** the class declaration (and +any attributes) using `.SYNOPSIS`, one `.PARAMETER` entry per significant property, and +at least one `.EXAMPLE`: + +```powershell +<# + .SYNOPSIS + A class to manage... + + .PARAMETER Name + Specifies the name. + + .EXAMPLE + [MyClass]::new('Value') +#> +[DscResource()] +class MyClass { ... } +``` + +## Private Function Exemptions + +Private (unexported) helper functions do not require the full comment-based help set +(`[CmdletBinding()]`, `[OutputType()]`, `.DESCRIPTION`, `.INPUTS`, `.OUTPUTS`, `.EXAMPLE` +are optional). They require at minimum a `.SYNOPSIS` line. + +## `[SuppressMessageAttribute]` Usage + +When a PSScriptAnalyzer rule must be suppressed (e.g., `PSAvoidGlobalVars` for +`$global:DSCMachineStatus`, or `PSUseDeclaredVarsMoreThanAssignments` for Pester blocks), +add the attribute with a justification comment at the top of the file before `param ()`: + +```powershell +# Justification comment describing why the rule is suppressed. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] +param () +``` + +## DSC Resource Module Import Boilerplate + +At the top of every MOF resource `.psm1`, import helper modules before loading localized +data (adjust relative path depth as needed): + +```powershell +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) ` + -ChildPath 'Modules' + +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +``` diff --git a/.github/instructions/dsc-community-unit-tests.instructions.md b/.github/instructions/dsc-community-unit-tests.instructions.md index 4f33140f..bb96d441 100644 --- a/.github/instructions/dsc-community-unit-tests.instructions.md +++ b/.github/instructions/dsc-community-unit-tests.instructions.md @@ -88,3 +88,212 @@ It 'Should have ParameterName as a mandatory parameter' { $parameterInfo.Attributes.Mandatory | Should -BeTrue } ``` + +## DSC Resource Unit Test Setup + +### MOF Resource `BeforeAll` / `AfterAll` + +```powershell +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + $script:dscResourceName = 'DSC_{ResourceName}' + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot ` + -ChildPath '..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscResourceName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscResourceName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Restore-TestEnvironment -TestEnvironment $script:testEnvironment + + Get-Module -Name $script:dscResourceName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +### Class Resource `BeforeAll` / `AfterAll` + +```powershell +BeforeAll { + $script:dscModuleName = 'ComputerManagementDsc' + + Import-Module -Name $script:dscModuleName + + Import-Module -Name (Join-Path -Path $PSScriptRoot ` + -ChildPath '..\..\TestHelpers\CommonTestHelper.psm1') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} +``` + +## Describe Block Naming, Tags, and `InModuleScope` + +- Use `Set-StrictMode -Version 1.0` as the **first line inside every `InModuleScope` + scriptblock** +- MOF resource `Describe` blocks: `'DSC_{ResourceName}\{FunctionName}'` with a tag: + + ```powershell + Describe 'DSC_TimeZone\Get-TargetResource' -Tag 'Get' { ... } + Describe 'DSC_TimeZone\Set-TargetResource' -Tag 'Set' { ... } + Describe 'DSC_TimeZone\Test-TargetResource' -Tag 'Test' { ... } + ``` + +- Class resource `Describe` blocks: `'{ClassName}\{MethodName}()'` with a tag: + + ```powershell + Describe 'PSResourceRepository\GetCurrentState()' -Tag 'GetCurrentState' { ... } + Describe 'PSResourceRepository\Modify()' -Tag 'Modify' { ... } + Describe 'PSResourceRepository\AssertProperties()' -Tag 'AssertProperties' { ... } + ``` + +- Standard tags: `Get`, `Set`, `Test`, `Modify`, `GetCurrentState`, `AssertProperties`, + `Private` + +## Mock Assertions + +Use `Should -Invoke` (Pester v5) for all mock call-count assertions: + +```powershell +Should -Invoke -CommandName Set-TimeZoneId -Exactly -Times 1 -Scope It +Should -Invoke -CommandName Get-TimeZoneId -Exactly -Times 0 -Scope It +``` + +## `ParameterFilter` with Catch-All + +When mocking the same command for different parameter values, add a final catch-all mock +(no filter) that throws to detect unexpected calls: + +```powershell +Mock -CommandName Get-RegistryPropertyValue ` + -ParameterFilter { $Name -eq 'ConsentPromptBehaviorAdmin' } ` + -MockWith { return 2 } + +Mock -CommandName Get-RegistryPropertyValue ` + -MockWith { throw 'Called with unexpected parameter values.' } +``` + +## Cross-Module Mocking + +To mock a function from a module other than the one set in `$PSDefaultParameterValues`, +pass `-ModuleName` explicitly: + +```powershell +Mock -ModuleName 'ComputerManagementDsc.Common' -CommandName 'Get-TimeZoneId' -MockWith { + return 'Pacific Standard Time' +} +``` + +## Class Resource Method Mocking + +Class methods cannot be mocked with `Mock`. Use `Add-Member -MemberType ScriptMethod`: + +```powershell +BeforeAll { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [PSResourceRepository] @{ Name = 'PSGallery' } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return [System.Collections.Hashtable] @{ Name = 'PSGallery'; Ensure = 'Present' } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } -PassThru + } +} +``` + +## Script-Scoped Call Counter (Class Methods) + +Since `Should -Invoke` cannot track class method calls, use a `$script:` counter: + +```powershell +BeforeAll { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [PSResourceRepository] @{ Name = 'Test' } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { + $script:mockMethodModifyCallCount += 1 + } -PassThru + } +} + +BeforeEach { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + $script:mockMethodModifyCallCount = 0 + } +} + +It 'Should call Modify() exactly once' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + $script:mockInstance.Set() + $script:mockMethodModifyCallCount | Should -Be 1 + } +} +``` + +## `InModuleScope -Parameters $_` for Data-Driven Tests + +When using `-TestCases` or `-ForEach`, pass the test case parameters into `InModuleScope`: + +```powershell +It 'Should set the correct value for ' -TestCases $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + Set-StrictMode -Version 1.0 + { Set-UserAccountControlToNotificationLevel -NotificationLevel $NotificationLevel } | + Should -Not -Throw + } +} +``` + +## `BeforeDiscovery` Inside `Describe` for Test Cases + +Define test case arrays inside `BeforeDiscovery` within the `Describe` block so they are +available during the discovery phase: + +```powershell +Describe 'MyResource\Set-TargetResource' -Tag 'Set' { + BeforeDiscovery { + $testCases = @( + @{ NotificationLevel = 'AlwaysNotify'; ConsentPromptBehaviorAdmin = 2 } + @{ NotificationLevel = 'NotifyChanges'; ConsentPromptBehaviorAdmin = 5 } + ) + } + + Context 'When notification level is ' -ForEach $testCases { + It 'Should apply the correct registry value' { + InModuleScope -Parameters $_ -ScriptBlock { + Set-StrictMode -Version 1.0 + # ... + } + } + } +} +``` From 858e471b0e1e86f70b273f43feb79c733f2631b9 Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Mon, 22 Jun 2026 09:54:20 +1200 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 3 +-- .../dsc-community-integration-tests.instructions.md | 8 ++++---- .../dsc-community-localization.instructions.md | 2 +- .github/instructions/dsc-community-pester.instructions.md | 7 +++---- CHANGELOG.md | 4 ++-- tests/AGENTS.md | 6 ++---- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ce5ad6a0..a41430ba 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -46,8 +46,7 @@ This repository contains two types of DSC resources: - Resource enums: `source/Enum/..ps1` - Unit tests (MOF): `tests/Unit/DSC_.Tests.ps1` - Unit tests (class): `tests/Unit/Classes/.Tests.ps1` -- Integration tests: `tests/Integration/.Integration.Tests.ps1` - +- Integration tests: `tests/Integration/*.Tests.ps1` (class resources in `tests/Integration/Classes/`) ## Naming Conventions - MOF-based resources: `DSC_` prefix on all files and all exported diff --git a/.github/instructions/dsc-community-integration-tests.instructions.md b/.github/instructions/dsc-community-integration-tests.instructions.md index dabd958c..c844e501 100644 --- a/.github/instructions/dsc-community-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-integration-tests.instructions.md @@ -1,13 +1,13 @@ --- -description: Guidelines for implementing integration tests for commands. -applyTo: "tests/[iI]ntegration/**/*.[iI]ntegration.[tT]ests.ps1" +description: Guidelines for implementing integration tests. +applyTo: "tests/[iI]ntegration/**/*.[Tt]ests.ps1" --- # Integration Tests Guidelines ## Requirements -- Location Commands: `tests/Integration/Commands/{CommandName}.Integration.Tests.ps1` -- Location Resources: `tests/Integration/Resources/{ResourceName}.Integration.Tests.ps1` +- Location (MOF resources): `tests/Integration/DSC_*.Tests.ps1` (plus a companion `.config.ps1`) +- Location (class resources): `tests/Integration/Classes/*.Tests.ps1` - No mocking - real environment only - Cover all scenarios and code paths - Use `Get-ComputerName` for computer names in CI diff --git a/.github/instructions/dsc-community-localization.instructions.md b/.github/instructions/dsc-community-localization.instructions.md index 9037c914..880b7160 100644 --- a/.github/instructions/dsc-community-localization.instructions.md +++ b/.github/instructions/dsc-community-localization.instructions.md @@ -1,6 +1,6 @@ --- description: Guidelines for implementing localization. -applyTo: "source/**/*.ps1" +applyTo: "source/**/*.{ps1,psm1}" version: 1.0.0 --- diff --git a/.github/instructions/dsc-community-pester.instructions.md b/.github/instructions/dsc-community-pester.instructions.md index a17df398..d6406c30 100644 --- a/.github/instructions/dsc-community-pester.instructions.md +++ b/.github/instructions/dsc-community-pester.instructions.md @@ -41,7 +41,6 @@ applyTo: "**/*.[Tt]ests.ps1" - PascalCase: `Describe`, `Context`, `It`, `Should`, `BeforeAll`, `BeforeEach`, `AfterAll`, `AfterEach` - Use `-BeTrue`/`-BeFalse` never `-Be $true`/`-Be $false`/`-Contain $true`/`-Contain $false` - Never use `Assert-MockCalled`, use `Should -Invoke` instead -- No `Should -Not -Throw` - invoke commands directly - Never add an empty `-MockWith` block - Omit `-MockWith` when returning `$null` - Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, `InModuleScope:ModuleName` @@ -53,9 +52,9 @@ applyTo: "**/*.[Tt]ests.ps1" - Assert calls inside the `It` block; do not assert call counts across an entire `Describe` or `Context` ## File Organization -- Class resources: `tests/Unit/Classes/{Name}.Tests.ps1` -- Public commands: `tests/Unit/Public/{Name}.Tests.ps1` -- Private functions: `tests/Unit/Private/{Name}.Tests.ps1` +- MOF resources: `tests/Unit/DSC_{ResourceName}.Tests.ps1` +- Class resources: `tests/Unit/Classes/{ClassName}.Tests.ps1` +- Common module tests: `tests/Unit/ComputerManagementDsc.Common.Tests.ps1` ## Data-Driven Tests (Test Cases) - Define `-ForEach` variables in separate `BeforeDiscovery` (close to usage) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a7e6b8..55ddd7a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `tests/AGENTS.md` with Pester v5 guidance, test location map, and required setup block templates for MOF and class-based resource tests. - Added `.github/copilot-instructions.md` with workspace-level GitHub Copilot instructions. -- Added `.github/instructions/` directory with 11 instruction files covering PowerShell - style, Pester, unit tests, integration tests, MOF resources, class-based resources, +- Added `.github/instructions/` directory with instruction files covering PowerShell style, + Pester, unit tests, integration tests, MOF resources, class-based resources, localization, changelog, and markdown guidelines. ### Changed diff --git a/tests/AGENTS.md b/tests/AGENTS.md index faa45df5..399e2fd5 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -23,8 +23,7 @@ All Pester keywords (`Describe`, `Context`, `It`, `BeforeAll`, `AfterAll`, `Befo |------|----------| | MOF resource unit tests | `tests/Unit/DSC_.Tests.ps1` | | Class resource unit tests | `tests/Unit/Classes/.Tests.ps1` | -| Integration tests | `tests/Integration/` (one `.config.ps1` + one `.Integration.Tests.ps1` per resource) | - +| Integration tests | `tests/Integration/` (MOF: one `.config.ps1` + one `*.Tests.ps1`; class-based: `tests/Integration/Classes/`) | ## Required Setup Block ### MOF Resource Unit Test @@ -41,8 +40,7 @@ BeforeDiscovery { { # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) - { - # Redirect all streams to $null, except the error stream (stream 2) + # Redirect all streams to $null & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null } From 1b0193bb5f3ad938419d8e9c3bf05371bfcd7f3a Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Mon, 22 Jun 2026 09:59:04 +1200 Subject: [PATCH 5/6] CHORE: Update instruction files for consistency - Standardize formatting in `.github/instructions/*.md` files - Update integration test guidelines in `AGENTS.md` - Minor formatting adjustments in `CHANGELOG.md` --- .github/copilot-instructions.md | 2 +- .../dsc-community-integration-tests.instructions.md | 8 ++++---- .../dsc-community-localization.instructions.md | 2 +- .github/instructions/dsc-community-pester.instructions.md | 6 +++--- CHANGELOG.md | 4 ++-- tests/AGENTS.md | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a41430ba..6afe2dbe 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -46,7 +46,7 @@ This repository contains two types of DSC resources: - Resource enums: `source/Enum/..ps1` - Unit tests (MOF): `tests/Unit/DSC_.Tests.ps1` - Unit tests (class): `tests/Unit/Classes/.Tests.ps1` -- Integration tests: `tests/Integration/*.Tests.ps1` (class resources in `tests/Integration/Classes/`) +- Integration tests: `tests/Integration/*.Tests.ps1` (class resources in `tests/Integration/Classes/`) ## Naming Conventions - MOF-based resources: `DSC_` prefix on all files and all exported diff --git a/.github/instructions/dsc-community-integration-tests.instructions.md b/.github/instructions/dsc-community-integration-tests.instructions.md index c844e501..10a24044 100644 --- a/.github/instructions/dsc-community-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-integration-tests.instructions.md @@ -1,13 +1,13 @@ --- -description: Guidelines for implementing integration tests. -applyTo: "tests/[iI]ntegration/**/*.[Tt]ests.ps1" +description: Guidelines for implementing integration tests. +applyTo: "tests/[iI]ntegration/**/*.[Tt]ests.ps1" --- # Integration Tests Guidelines ## Requirements -- Location (MOF resources): `tests/Integration/DSC_*.Tests.ps1` (plus a companion `.config.ps1`) -- Location (class resources): `tests/Integration/Classes/*.Tests.ps1` +- Location (MOF resources): `tests/Integration/DSC_*.Tests.ps1` (plus a companion `.config.ps1`) +- Location (class resources): `tests/Integration/Classes/*.Tests.ps1` - No mocking - real environment only - Cover all scenarios and code paths - Use `Get-ComputerName` for computer names in CI diff --git a/.github/instructions/dsc-community-localization.instructions.md b/.github/instructions/dsc-community-localization.instructions.md index 880b7160..9f69155a 100644 --- a/.github/instructions/dsc-community-localization.instructions.md +++ b/.github/instructions/dsc-community-localization.instructions.md @@ -1,6 +1,6 @@ --- description: Guidelines for implementing localization. -applyTo: "source/**/*.{ps1,psm1}" +applyTo: "source/**/*.{ps1,psm1}" version: 1.0.0 --- diff --git a/.github/instructions/dsc-community-pester.instructions.md b/.github/instructions/dsc-community-pester.instructions.md index d6406c30..b1128109 100644 --- a/.github/instructions/dsc-community-pester.instructions.md +++ b/.github/instructions/dsc-community-pester.instructions.md @@ -52,9 +52,9 @@ applyTo: "**/*.[Tt]ests.ps1" - Assert calls inside the `It` block; do not assert call counts across an entire `Describe` or `Context` ## File Organization -- MOF resources: `tests/Unit/DSC_{ResourceName}.Tests.ps1` -- Class resources: `tests/Unit/Classes/{ClassName}.Tests.ps1` -- Common module tests: `tests/Unit/ComputerManagementDsc.Common.Tests.ps1` +- MOF resources: `tests/Unit/DSC_{ResourceName}.Tests.ps1` +- Class resources: `tests/Unit/Classes/{ClassName}.Tests.ps1` +- Common module tests: `tests/Unit/ComputerManagementDsc.Common.Tests.ps1` ## Data-Driven Tests (Test Cases) - Define `-ForEach` variables in separate `BeforeDiscovery` (close to usage) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ddd7a8..2cc34ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `tests/AGENTS.md` with Pester v5 guidance, test location map, and required setup block templates for MOF and class-based resource tests. - Added `.github/copilot-instructions.md` with workspace-level GitHub Copilot instructions. -- Added `.github/instructions/` directory with instruction files covering PowerShell style, - Pester, unit tests, integration tests, MOF resources, class-based resources, +- Added `.github/instructions/` directory with instruction files covering PowerShell style, + Pester, unit tests, integration tests, MOF resources, class-based resources, localization, changelog, and markdown guidelines. ### Changed diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 399e2fd5..7b397dad 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -23,7 +23,7 @@ All Pester keywords (`Describe`, `Context`, `It`, `BeforeAll`, `AfterAll`, `Befo |------|----------| | MOF resource unit tests | `tests/Unit/DSC_.Tests.ps1` | | Class resource unit tests | `tests/Unit/Classes/.Tests.ps1` | -| Integration tests | `tests/Integration/` (MOF: one `.config.ps1` + one `*.Tests.ps1`; class-based: `tests/Integration/Classes/`) | +| Integration tests | `tests/Integration/` (MOF: one `.config.ps1` + one `*.Tests.ps1`; class-based: `tests/Integration/Classes/`) | ## Required Setup Block ### MOF Resource Unit Test @@ -40,7 +40,7 @@ BeforeDiscovery { { # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) - # Redirect all streams to $null + # Redirect all streams to $null & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null } From 9fd1aa10948ea3db3b9a1507fd85d0e06285e1db Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Mon, 22 Jun 2026 10:35:45 +1200 Subject: [PATCH 6/6] CHORE: Update changelog guidelines for clarity - Refine description for maintaining a consistent changelog - Clarify formatting requirements for the Unreleased section - Improve issue reference format for better consistency - Ensure no duplicate sections or items in the changelog --- .../dsc-community-changelog.instructions.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/instructions/dsc-community-changelog.instructions.md b/.github/instructions/dsc-community-changelog.instructions.md index 7224a44d..b4f1ca43 100644 --- a/.github/instructions/dsc-community-changelog.instructions.md +++ b/.github/instructions/dsc-community-changelog.instructions.md @@ -1,27 +1,26 @@ --- -description: Guidelines for maintaining a clear and consistent changelog. +description: Guidelines for a consistent changelog. applyTo: "CHANGELOG.md" version: 1.0.0 --- # Changelog Guidelines -- Always update the Unreleased section in `CHANGELOG.md` +- Always update the `## [Unreleased]` section in `CHANGELOG.md` +- One section per change type (`Added`, `Changed`, `Fixed`) under `## [Unreleased]` - Use Keep a Changelog format -- Describe notable changes briefly, ≤2 items per change type +- Describe changes briefly; ≤2 items per change type - Reference issues using format - `[issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/)` + ` - Fixes [Issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/)` + - capital `I` in `Issue`; `Fixes` prefix; full stop after closing parenthesis - No empty lines between list items in same section -- Skip adding entry if same change already exists in Unreleased section -- No duplicate sections or items in Unreleased section -- Issue reference format: `Fixes [Issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/).` - — capital `I` in `Issue`, `Fixes` keyword prefix, full stop after the closing parenthesis +- No duplicate sections or items in `## [Unreleased]`; skip if entry already exists - Mark breaking changes with `BREAKING CHANGE:` prefix on the entry in `### Changed` or `### Fixed` - Group multiple changes for the same resource using two-level indentation: ```markdown - ResourceName - - First change description for this resource - - Second change description for this resource + - First change description for this resource - Fixes [Issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/) + - Second change description for this resource - Fixes [Issue #](https://github.com/dsccommunity/ComputerManagementDsc/issues/) ```