Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/instructions/ci--workflows.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ Triggers on push/PR touching `Doc/**`, `Samples/**`,
`.github/copilot-instructions.md`, `.claude/**`, `AGENTS.md`, `CLAUDE.md`, or the workflow file
itself. Parallel jobs (no `needs:` dependency between them):

- **`anonymization`**: installs Pester 5.3+, runs `TestAnonymization.Tests.ps1`, throws on failure.
- **`ai-instructions`**: installs Pester 5.3+, runs `TestAiInstructions.Tests.ps1`, throws on
- **`anonymization`**: installs Pester 6.0+, runs `TestAnonymization.Tests.ps1`, throws on failure.
- **`ai-instructions`**: installs Pester 6.0+, runs `TestAiInstructions.Tests.ps1`, throws on
failure.

No build, no live server. Runs on `ubuntu-latest` to keep it fast and free of Windows-only overhead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "How ISHRemote Pester (*.Tests.ps1) acceptance tests are structured

# ISHRemote Pester Test Conventions

These are **Pester 5.3+ acceptance/integration tests** that run against a **live Tridion Docs
These are **Pester 6.0+ acceptance/integration tests** that run against a **live Tridion Docs
(InfoShare) tenant**. They are not unit tests — almost every `It` makes real Web Services API calls,
creates server objects, asserts, and then deletes them. Keep new tests consistent with the patterns
below or you will create flaky runs for everyone.
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ jobs:
- name: Pester Test Anonymization (no live server needed)
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'5.3.0' })) {
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'6.0.0' })) {
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.3.0 -Force -SkipPublisherCheck -Scope CurrentUser
Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser
}
$result = Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestAnonymization.Tests.ps1 -Output Detailed -PassThru
if ($result.FailedCount -gt 0) {
Expand All @@ -65,9 +65,9 @@ jobs:
- name: Pester Test AI Instructions (no live server needed)
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'5.3.0' })) {
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'6.0.0' })) {
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 5.3.0 -Force -SkipPublisherCheck -Scope CurrentUser
Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser
}
$result = Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestAiInstructions.Tests.ps1 -Output Detailed -PassThru
if ($result.FailedCount -gt 0) {
Expand Down
74 changes: 47 additions & 27 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,14 @@ jobs:
shell: pwsh
run: dotnet build --no-restore --no-incremental --configuration release Source/ISHRemote/ISHRemote.sln

- name: Setup PowerShell modules (PSScriptAnalyzer + Pester)
- name: Module Setup using PowerShell 7.x (PSScriptAnalyzer + Pester)
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module PSScriptAnalyzer -ErrorAction Stop
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'5.3.0' })) {
Install-Module Pester -MinimumVersion 5.3.0 -Force -SkipPublisherCheck -Scope CurrentUser
}
Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser

- name: Lint with PSScriptAnalyzer
- name: Lint with PSScriptAnalyzer using PowerShell 7.x
shell: pwsh
run: |
Invoke-ScriptAnalyzer -Path Source/ISHRemote/Trisoft.ISHRemote/Scripts -Recurse -Outvariable issues
Expand All @@ -103,9 +101,10 @@ jobs:
Write-Output "There were $($errors.Count) errors and $($warnings.Count) warnings total."
}

- name: Pester Test Anonymization of public Doc and Samples on Windows using PowerShell 7.x
- name: Pester Test Anonymization of public Doc and Samples using PowerShell 7.x
shell: pwsh
run: |
Import-Module Pester -MinimumVersion 6.0.0 -Force
$result = Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestAnonymization.Tests.ps1 -Output Detailed -PassThru
if ($result.FailedCount -gt 0) {
throw "Anonymization guard failed: $($result.FailedCount) check(s) found a real URL, internal hostname, private IP or token. Use example.com placeholders or extend the known-exceptions list in the test."
Expand Down Expand Up @@ -145,7 +144,13 @@ jobs:
name: ISHRemote-MainCI-Module
path: Source/ISHRemote/Trisoft.ISHRemote/bin/Release/ISHRemote/

- name: Pester Test Preparation (...products.com) on Windows using PowerShell 7.x
- name: Module Setup on PowerShell 7.x (Pester)
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser

- name: Pester Test Preparation (...products.com) on PowerShell 7.x
shell: pwsh
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
Expand All @@ -168,38 +173,54 @@ jobs:
Add-Content -Path $filePath -Value ' $ishLngCombination = ''en-us'''
Add-Content -Path $filePath -Value '}'

- name: Pester Test Prerequisites on Windows using PowerShell 7.x
- name: Pester Test Prerequisites on PowerShell 7.x
shell: pwsh
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
ISH_USER_NAME: ${{ secrets.ISH_USER_NAME }}
ISH_PASSWORD: ${{ secrets.ISH_PASSWORD }}
ISH_CLIENT_ID: ${{ secrets.ISH_CLIENT_ID }}
ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }}
run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
run: |
Import-Module Pester -MinimumVersion 6.0.0 -Force
Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
- name: Upload test results
uses: actions/upload-artifact@v7
with:
name: ISHRemote-MainCI-PesterPrerequisiteOnPowerShellCore
path: Cmdlets.Pester.Tests.xml

- name: Pester Test Cmdlets on Windows using PowerShell 7.x
- name: Pester Test Cmdlets on PowerShell 7.x
shell: pwsh
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
ISH_USER_NAME: ${{ secrets.ISH_USER_NAME }}
ISH_PASSWORD: ${{ secrets.ISH_PASSWORD }}
ISH_CLIENT_ID: ${{ secrets.ISH_CLIENT_ID }}
ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }}
run: Invoke-Pester -Path @('Source/ISHRemote/Trisoft.ISHRemote/Scripts/Public/','Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/') -ExcludePath @('*GetIshDocumentObj.Tests.ps1','*TestAnonymization.Tests.ps1') -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
run: |
Import-Module Pester -MinimumVersion 6.0.0 -Force
$config = New-PesterConfiguration
$config.Run.Path = @('Source/ISHRemote/Trisoft.ISHRemote/Scripts/Public/','Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/')
$config.Run.ExcludePath = @('*TestPrerequisite.Tests.ps1','*TestAnonymization.Tests.ps1')
$config.Run.Parallel = $false # can be $true on PS7, will run serially on PS5.1
$config.Output.Verbosity = 'Detailed'
$config.Run.PassThru = $true
Invoke-Pester -Configuration $config | Export-CliXml -Path Cmdlets.Pester.Tests.xml
- name: Upload test results
uses: actions/upload-artifact@v7
with:
name: ISHRemote-MainCI-PesterOnPowerShellCore
path: Cmdlets.Pester.Tests.xml
if: ${{ always() }}

- name: Pester Test Preparation (...products.com) using Windows PowerShell 5.1
- name: Module Setup on Windows PowerShell 5.1 (Pester)
shell: powershell
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser

- name: Pester Test Preparation (...products.com) on Windows PowerShell 5.1
shell: powershell
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
Expand All @@ -222,30 +243,40 @@ jobs:
Add-Content -Path $filePath -Value ' $ishLngCombination = ''en-us'''
Add-Content -Path $filePath -Value '}'

- name: Pester Test Prerequisites using Windows PowerShell 5.1
- name: Pester Test Prerequisites on Windows PowerShell 5.1
shell: powershell
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
ISH_USER_NAME: ${{ secrets.ISH_USER_NAME }}
ISH_PASSWORD: ${{ secrets.ISH_PASSWORD }}
ISH_CLIENT_ID: ${{ secrets.ISH_CLIENT_ID }}
ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }}
run: Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
run: |
Import-Module Pester -MinimumVersion 6.0.0 -Force
Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
- name: Upload test results
uses: actions/upload-artifact@v7
with:
name: ISHRemote-MainCI-PesterPrerequisiteOnWindowsPowerShell
path: Cmdlets.Pester.Tests.xml

- name: Pester Test Cmdlets using Windows PowerShell 5.1
- name: Pester Test Cmdlets on Windows PowerShell 5.1
shell: powershell
env:
ISH_BASE_URL: ${{ secrets.ISH_BASE_URL }}
ISH_USER_NAME: ${{ secrets.ISH_USER_NAME }}
ISH_PASSWORD: ${{ secrets.ISH_PASSWORD }}
ISH_CLIENT_ID: ${{ secrets.ISH_CLIENT_ID }}
ISH_CLIENT_SECRET: ${{ secrets.ISH_CLIENT_SECRET }}
run: Invoke-Pester -Path @('Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/') -ExcludePath @('*GetIshDocumentObj.Tests.ps1','*TestAnonymization.Tests.ps1') -Output Detailed -Passthru | Export-CliXml -Path Cmdlets.Pester.Tests.xml
run: |
Import-Module Pester -MinimumVersion 6.0.0 -Force
$config = New-PesterConfiguration
$config.Run.Path = @('Source/ISHRemote/Trisoft.ISHRemote/Scripts/Public/','Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/')
$config.Run.ExcludePath = @('*TestPrerequisite.Tests.ps1','*TestAnonymization.Tests.ps1')
$config.Run.Parallel = $false # can be $true on PS7, will run serially on PS5.1
$config.Output.Verbosity = 'Detailed'
$config.Run.PassThru = $true
Invoke-Pester -Configuration $config | Export-CliXml -Path Cmdlets.Pester.Tests.xml
- name: Upload test results
uses: actions/upload-artifact@v7
with:
Expand Down Expand Up @@ -303,15 +334,4 @@ jobs:
echo ("Found recent module[ISHRemote] of version["+($remoteModule.Version)+"]. Skipping publish of module with version[$fullVersion]!")
}

- name: "Info: How to publish to Internal/Nexus Repositories"
shell: pwsh
run: |
echo "Either the steps below or in-house https://jenkins-docs.web.sdldev.net/job/Publish-ISHRemote/ job that downloads and publishes to Nexus"
echo "1. Login to Github, navigate to $env:ISHGITHUB_SERVER_URL/$env:ISHGITHUB_REPOSITORY/actions/runs/$env:ISHGITHUB_RUN_ID"
echo "2. Download ISHRemote-MainCI-Module artefact to C:\TEMP\ISHRemote\ISHRemote-MainCI-Module.zip"
echo "3. In a PowerShell Session..."
echo "3a. Remove-Item -Path C:\TEMP\ISHRemote\ToPublish -Recurse -Force"
echo "3b. Expand-Archive -Path C:\TEMP\ISHRemote\ISHRemote-MainCI-Module.zip -DestinationPath C:\TEMP\ISHRemote\ToPublish\ -Force"
echo "3c. Publish-Module -Path C:\TEMP\ISHRemote\ToPublish\ISHRemote.psd1 -Repository $psRepository -NuGetApiKey $nuGetApiKey -Force"
echo "3d. Find-Module -Name ISHRemote -Repository $psRepository -AllowPrerelease"

18 changes: 15 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@ C# classes; a few advanced functions are PowerShell scripts. Cmdlets follow `Ver
```
dotnet restore Source/ISHRemote/ISHRemote.sln
```
2. **Build:**
2. **Build — local iteration (use this when making and verifying code changes):**
```
dotnet build --no-restore --no-incremental --configuration debug Source/ISHRemote/ISHRemote.sln
```
`ISHRemote.PesterSetup.ps1` loads `bin\debug\ISHRemote` locally (outside GitHub Actions) and
`bin\release\ISHRemote` in CI. Building `release` locally does **not** update what the tests
load — always build `debug` when iterating locally.

> **Locked DLL:** If the debug build fails with a file-locked error on a `.dll` inside
> `bin\Debug\ISHRemote\`, a `powershell.exe` or `pwsh.exe` process has the module loaded.
> Close all PowerShell sessions that imported ISHRemote (including any running Pester test
> sessions), then retry the build.

3. **Build — pre-PR validation (mirrors CI, catches warning regressions):**
```
dotnet build --no-restore --no-incremental --configuration release Source/ISHRemote/ISHRemote.sln
```
- Use `Debug` for local iteration; tests load `bin\debug\ISHRemote` locally and `bin\release\ISHRemote` in CI.
- **Release fails on any warning** (`TreatWarningsAsErrors=true`). Keep the tree warning-clean.
- The `net48` target requires **both** `pwsh.exe` (PowerShell 7) and `powershell.exe` (Windows PowerShell 5.1).

Expand All @@ -55,7 +67,7 @@ CI reports analyzer findings but does not fail the build on them. Still, keep `S
Without a reachable server, most tests cannot pass.
- **Build before testing.** Tests import the compiled module from `bin\debug\ISHRemote` (local) or
`bin\release\ISHRemote` (CI).
- Use **Pester 5.3.0+**: `Install-Module -Name Pester -Force -SkipPublisherCheck`.
- Use **Pester 6.0.0+**: `Install-Module -Name Pester -Force -SkipPublisherCheck`.
- **Run order (fast → broad), from repo root:**
1. `Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/_TestEnvironment/TestPrerequisite.Tests.ps1 -Output Detailed`
2. `Invoke-Pester -Path Source/ISHRemote/Trisoft.ISHRemote/Cmdlets/<Domain>/ -Output Detailed`
Expand Down
7 changes: 6 additions & 1 deletion Doc/ReleaseNotes-ISHRemote-8.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The below text describes the delta compared to fielded release ISHRemote v8.2.
## Implementation Details

* Fixed `Start-IshRemoteMcpServer` failing to connect on Windows with newer MCP clients (e.g. OpenCode 1.18.11, protocol `2025-11-25`) with errors `MCP error -32001: Request timed out` and `Failed to get tools`. Three root causes: (1) `initialize` requests with `"id":0` were silently dropped because PowerShell treats `0` as falsy; (2) `[Console]::InputEncoding` defaults to OEM code page (`ibm437`) when `pwsh.exe` is spawned with redirected stdio on Windows, causing `ReadLine()` to block forever on UTF-8 JSON — fixed by explicitly setting UTF-8 encoding and replacing `Console.Out` with an auto-flushing `StreamWriter` via `[Console]::SetOut()`; (3) `Register-IshRemoteMcpTool` emitted an invalid `type: "object"` field in `ToolAnnotations` and used string `"true"`/`"false"` instead of boolean `$true`/`$false` for hint values, causing strict MCP schema validation to reject the tools list. Server name updated from `"PowerShell MCP Server (Template)"` to `"ISHRemote MCP Server"` and version bumped to `0.3.0`. Also fixed the server looping forever on stdin EOF (orphaned `pwsh` processes) by breaking the while loop when `ReadLine()` returns `$null`. See #243 and #261. Thanks @ddemeyer
* Migrated all 58 `*.Tests.ps1` files from Pester v5 to Pester v6 (`Should -Be` to `Should-Be`, `Should -BeExactly` to `Should-BeString -CaseSensitive`, `Should -Not -BeNullOrEmpty` to `Should-NotBeNull`, `Should -Throw "msg"` to `Should-Throw -ExceptionMessage "msg"`, etc.). CI install gates updated to `-MinimumVersion 6.0.0`. Classic `Should -Not -Throw` retained as there is no `Should-NotThrow` equivalent in Pester 6. Hardened the library for parallel test execution by replacing the process-wide `TrisoftCmdletLogger` singleton with per-cmdlet `ILogger` routing and adding a double-checked lock on `IshSession._ishTypeFieldSetup` to eliminate Collection was modified races under `Run.Parallel = $true`. CI Pester invocations now use `New-PesterConfiguration` (with `Run.Parallel = $false`) so parallel mode can be toggled in one place when ready. See #242, #265, #266.



## Breaking Changes - Cmdlets
Expand Down Expand Up @@ -58,7 +60,7 @@ All cmdlets and business logic are fully compatible.

## Quality Assurance

Added more Invoke-Pester 5.7.1 Tests, see Github actions for the Windows PowerShell 5.1 and PowerShell 7.6+ hosts where
Added more Invoke-Pester 6.0.0 Tests, see Github actions for the Windows PowerShell 5.1 and PowerShell 7.6+ hosts where
* the skipped are about SslPolicyErrors testing and `ISHRemoteMcpServer` is PowerShell 7+ only
* the failed are about IMetadata bound fields (issue #58)

Expand All @@ -80,3 +82,6 @@ Below is not an official performance compare, but a recurring thing noticed alon
| ISHRemote 8.2.13523.0 | PowerShell 7.5.4 on .NET 9.0.10 | LEUDEVDDE...@15.3.0b2303 | Tests completed in 141.61s AND Tests Passed: 1128, Failed: 0, Skipped: 4, Inconclusive: 0, NotRun: 0 |
| ISHRemote 8.2.13525.0 | Windows PowerShell 5.1 on .NET 4.8.1 | LEUDEVDDE...@15.3.0b2303 | Tests completed in 132.15s AND Tests Passed: 1087, Failed: 0, Skipped: 52, Inconclusive: 0, NotRun: 0 |
| ISHRemote 8.2.13525.0 | PowerShell 7.6.0 on .NET 10.0.5 | LEUDEVDDE...@15.3.0b2303 | Tests completed in 139.45s AND Tests Passed: 1135, Failed: 0, Skipped: 4, Inconclusive: 0, NotRun: 0 |
| ISHRemote 8.3.14019.0 | Windows PowerShell 5.1 on .NET 4.8.1 | LEUDEVDDE...@15.3.0b3005 | Tests completed in 123.35s AND Tests Passed: 1260, Failed: 0, Skipped: 54, Inconclusive: 0, NotRun: 0 |
| ISHRemote 8.3.14019.0 | PowerShell 7.6.5 on .NET 10.0.11 | LEUDEVDDE...@15.3.0b3005 | Tests completed in 149.16s AND Tests Passed: 1310, Failed: 0, Skipped: 4, Inconclusive: 0, NotRun: 0 |
| ISHRemote 8.3.14019.0 | PowerShell 7.6.5 on .NET 10.0.11 | LEUDEVDDE...@15.3.0b3005 | Tests completed in 79.77s AND Tests Passed: 1270, Failed: 0, Skipped: 4, Inconclusive: 0, NotRun: 0 (`$config.Run.Parallel = $true`) |
Loading
Loading