diff --git a/.github/actions/ci-serialisation/action.yml b/.github/actions/ci-serialisation/action.yml index e3036c8..0b783b1 100644 --- a/.github/actions/ci-serialisation/action.yml +++ b/.github/actions/ci-serialisation/action.yml @@ -196,7 +196,10 @@ runs: run: | git fetch origin ${{ inputs.base_ref }} git checkout ${{ inputs.base_ref }} - Remove-Item "C:\ProgramData\BHoM\Assemblies\*" -Recurse -Force -ErrorAction SilentlyContinue + # No assemblies reset here. resolve-dependencies removes and recreates both + # ProgramData\BHoM\Assemblies and ProgramData\BHoM\Upgrades in its own Prepare + # folders step, which is the next step and runs unconditionally. A reset here + # would cover one of those two directories and be redundant with the step after it. # prefer_branch is what makes this a baseline. Without it the head branch is preferred on # both legs, because resolve-dependencies takes PR_BRANCH from the event payload and that @@ -223,7 +226,10 @@ runs: shell: pwsh run: | dotnet restore "${{ steps.solution.outputs.path }}" - dotnet build "${{ steps.solution.outputs.path }}" --no-restore -c Release --nologo -m + # -clp:ErrorsOnly for the same reason as the branch leg above, and with more force: + # this compiles the base branch, so every warning belongs to code the author did + # not write and cannot act on. + dotnet build "${{ steps.solution.outputs.path }}" --no-restore -c Release --nologo -m -clp:ErrorsOnly # Re-infer against the baseline checkout: Verification.sln may declare # different Release variants on branch versus base (e.g. the branch @@ -240,7 +246,7 @@ runs: shell: pwsh run: | dotnet restore "${{ steps.ver_sln.outputs.path }}" - dotnet build "${{ steps.ver_sln.outputs.path }}" --no-restore -c ${{ steps.ver_config_baseline.outputs.config }} --nologo + dotnet build "${{ steps.ver_sln.outputs.path }}" --no-restore -c ${{ steps.ver_config_baseline.outputs.config }} --nologo -clp:ErrorsOnly # --- Comparison --- @@ -266,83 +272,51 @@ runs: "verdict=$verdict" | Out-File $env:GITHUB_OUTPUT -Append if ($code -ne 0) { exit $code } - # Distinguish an infrastructure failure from a serialisation regression. A - # dependency build failure otherwise shows as a red serialisation check with no - # indication that serialisation never ran. - - name: Write to Job Summary (did not run) - if: always() && steps.changed.outputs.count != '0' && (steps.deps_branch.conclusion == 'failure' || steps.build_branch.conclusion == 'failure') - shell: pwsh - run: | - $step = if ("${{ steps.deps_branch.conclusion }}" -eq 'failure') { 'Resolve dependencies (branch)' } else { 'Build primary repo (branch)' } - @( - "### Serialisation did not run" - "" - "Failed in: $step" - "" - "The dependency closure did not build, so no serialisation comparison was made." - "This is not a serialisation finding. See the ci-build check on this pull request" - "for the same failure." - ) | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - - - name: Write to Job Summary (branch passed) - if: always() && steps.changed.outputs.count != '0' && steps.branch_run.conclusion == 'success' && steps.branch_run.outputs.status != 'Error' - shell: pwsh - run: | - @( - "### Serialisation" - "" - "| Status |" - "|---|" - "| No serialisation failures (${{ steps.branch_run.outputs.status }}) |" - "" - "Objects exercised: ${{ steps.branch_run.outputs.population }} across ${{ steps.branch_run.outputs.assemblies }} loaded assemblies, ${{ steps.branch_run.outputs.legs }} legs. Failures reported by the runner: ${{ steps.branch_run.outputs.failures }}." - "" - ) | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - - - name: Write to Job Summary (pre-existing failures) - if: always() && steps.changed.outputs.count != '0' && steps.compare.outputs.verdict == 'ok' + # One summary step, reached on every path. + # + # This was five steps, each gated on its own combination of step conclusions and outputs. + # The conditions had to enumerate the states worth reporting, and between them they named + # five of this action's eighteen functional steps, so a failure anywhere else matched no + # condition and the check went red with an empty summary. Seven of the unnamed steps run + # before serialisation is attempted, which is the case a reader is least able to diagnose. + # + # The decision moved to a script so it can be tested without running a serialisation check, + # and so the branch that handles "none of the above" is a branch in code rather than a + # condition nobody wrote. + # + # Values go through env: rather than into the script arguments, so a step output cannot be + # interpolated into a command line. + - name: Write to Job Summary + if: always() && steps.changed.outputs.count != '0' shell: pwsh + env: + DEPS_BRANCH_CONCLUSION: ${{ steps.deps_branch.conclusion }} + BUILD_BRANCH_CONCLUSION: ${{ steps.build_branch.conclusion }} + BRANCH_RUN_CONCLUSION: ${{ steps.branch_run.conclusion }} + BRANCH_STATUS: ${{ steps.branch_run.outputs.status }} + COMPARE_VERDICT: ${{ steps.compare.outputs.verdict }} + POPULATION: ${{ steps.branch_run.outputs.population }} + ASSEMBLIES: ${{ steps.branch_run.outputs.assemblies }} + LEGS: ${{ steps.branch_run.outputs.legs }} + FAILURES: ${{ steps.branch_run.outputs.failures }} run: | - @( - "### Serialisation" - "" - "| Status |" - "|---|" - "| Serialisation failures detected. All are pre-existing on the base branch, so this job passes. |" - "" - "Objects exercised: ${{ steps.branch_run.outputs.population }} across ${{ steps.branch_run.outputs.assemblies }} loaded assemblies, ${{ steps.branch_run.outputs.legs }} legs. Failures reported by the runner: ${{ steps.branch_run.outputs.failures }}." - "" - ) | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + # GITHUB_ACTION_PATH is .github/actions/ci-serialisation, so ../../scripts is + # .github/scripts, the same path ci-build and ci-versioning use for shared scripts. + $script = Join-Path $env:GITHUB_ACTION_PATH "../../scripts/Write-SerialisationSummary.ps1" + . $script - - name: Write to Job Summary (regression detected) - if: always() && steps.changed.outputs.count != '0' && steps.compare.outputs.verdict == 'regression' - shell: pwsh - run: | - @( - "### Serialisation" - "" - "| Status |" - "|---|" - "| Serialisation regression detected. Affected types are in the step log. |" - "" - "Objects exercised: ${{ steps.branch_run.outputs.population }} across ${{ steps.branch_run.outputs.assemblies }} loaded assemblies, ${{ steps.branch_run.outputs.legs }} legs. Failures reported by the runner: ${{ steps.branch_run.outputs.failures }}." - "" - ) | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + $lines = Get-SerialisationSummary ` + -DepsBranchConclusion $env:DEPS_BRANCH_CONCLUSION ` + -BuildBranchConclusion $env:BUILD_BRANCH_CONCLUSION ` + -BranchRunConclusion $env:BRANCH_RUN_CONCLUSION ` + -BranchStatus $env:BRANCH_STATUS ` + -CompareVerdict $env:COMPARE_VERDICT ` + -Population $env:POPULATION ` + -Assemblies $env:ASSEMBLIES ` + -Legs $env:LEGS ` + -Failures $env:FAILURES - - name: Write to Job Summary (serialisation unverifiable) - if: always() && steps.changed.outputs.count != '0' && (steps.branch_run.conclusion == 'failure' || steps.compare.outputs.verdict == 'baseline-unusable') - shell: pwsh - run: | - @( - "### Serialisation" - "" - "| Status |" - "|---|" - "| Serialisation could not be verified. This is a CI runner failure, not a defect in this pull request. |" - "" - "Objects exercised: ${{ steps.branch_run.outputs.population }} across ${{ steps.branch_run.outputs.assemblies }} loaded assemblies, ${{ steps.branch_run.outputs.legs }} legs. Failures reported by the runner: ${{ steps.branch_run.outputs.failures }}." - "" - ) | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + $lines | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - name: Upload results if: always() && steps.changed.outputs.count != '0' diff --git a/.github/scripts/Write-SerialisationSummary.ps1 b/.github/scripts/Write-SerialisationSummary.ps1 new file mode 100644 index 0000000..9b95334 --- /dev/null +++ b/.github/scripts/Write-SerialisationSummary.ps1 @@ -0,0 +1,133 @@ +# Write-SerialisationSummary.ps1 — the serialisation check's job-summary decision, on its +# own so it can be tested without running a serialisation check. +# +# Dot-sourced by .github/actions/ci-serialisation/action.yml and by +# .github/scripts/tests/Write-SerialisationSummary.Tests.ps1. Defines a function and does +# nothing else, so dot-sourcing has no side effects. + +function Get-SerialisationSummary { + <# + .SYNOPSIS + Turns the serialisation check's step outcomes into the lines of its job summary. + + .DESCRIPTION + This replaced five separate summary steps, each gated on its own combination of step + conclusions and outputs. The problem with that shape was not any one condition: it was + that the conditions had to enumerate the states worth reporting, and a state nobody + enumerated produced no summary at all. Between them they named five of the action's + eighteen functional steps, so a failure anywhere else left the check red and the summary + empty, with nothing to tell the reader whether serialisation had even been attempted. + + A single function reached on every path cannot have that hole. The last branch is a + catch-all, so an outcome nobody anticipated still produces a readable summary naming the + phase it stopped in, rather than silence. + + Every parameter is a string because that is what a workflow expression yields. An absent + value arrives as the empty string, and the empty string is meaningful here: it is how a + step that never ran is distinguished from one that ran and failed. + + .PARAMETER DepsBranchConclusion + Conclusion of the branch-leg dependency resolution. + + .PARAMETER BuildBranchConclusion + Conclusion of the branch-leg build of the calling repository. + + .PARAMETER BranchRunConclusion + Conclusion of the branch-leg serialisation run. Empty when it never ran. + + .PARAMETER BranchStatus + Status the branch-leg runner reported: Pass, Warning or Error. Empty when it never ran. + + .PARAMETER CompareVerdict + Verdict of the comparison step: ok, regression, baseline-unusable. Empty when the + comparison never ran, which is every path that stopped before it. + + .PARAMETER Population, Assemblies, Legs, Failures + Coverage figures the branch-leg runner reported. Emitted only when the branch leg + produced them, so a summary never claims coverage the run did not measure. + + .OUTPUTS + [string[]] markdown lines, ready to append to the step summary. + #> + [CmdletBinding()] + param( + [string]$DepsBranchConclusion = '', + [string]$BuildBranchConclusion = '', + [string]$BranchRunConclusion = '', + [string]$BranchStatus = '', + [string]$CompareVerdict = '', + [string]$Population = '', + [string]$Assemblies = '', + [string]$Legs = '', + [string]$Failures = '' + ) + + # The dependency closure or the caller's own build failed, so serialisation never started. + # Reported separately from every other failure because it is not a serialisation finding + # at all and the same failure is already on the build check, where it belongs. + if ($DepsBranchConclusion -eq 'failure' -or $BuildBranchConclusion -eq 'failure') { + $step = if ($DepsBranchConclusion -eq 'failure') { 'Resolve dependencies (branch)' } + else { 'Build primary repo (branch)' } + return @( + "### Serialisation did not run" + "" + "Failed in: $step" + "" + "The dependency closure did not build, so no serialisation comparison was made." + "This is not a serialisation finding. See the ci-build check on this pull request" + "for the same failure." + ) + } + + $status = + if ($BranchRunConclusion -eq 'success' -and $BranchStatus -ne 'Error') { + "No serialisation failures ($BranchStatus)" + } + elseif ($CompareVerdict -eq 'ok') { + "Serialisation failures detected. All are pre-existing on the base branch, so this job passes." + } + elseif ($CompareVerdict -eq 'regression') { + "Serialisation regression detected. Affected types are in the step log." + } + elseif ($BranchRunConclusion -eq 'failure' -or $CompareVerdict -eq 'baseline-unusable') { + "Serialisation could not be verified. This is a CI runner failure, not a defect in this pull request." + } + else { + # The catch-all, and the reason this function exists. Everything above describes a + # state someone thought of; this describes the rest. It names the phase rather than + # the step, because deriving the phase needs only the signals already passed in, + # whereas naming the step would mean giving every step an id and threading each + # conclusion through — which is the enumeration that failed in the first place. + $phase = + if ($BranchRunConclusion -eq '' -or $BranchRunConclusion -eq 'skipped') { + "while preparing the branch leg, before serialisation ran" + } + elseif ($BranchStatus -eq 'Error') { + "while preparing the baseline leg, after the branch leg reported failures" + } + else { + "at a point this summary cannot identify" + } + "Serialisation did not reach a verdict. The check stopped $phase. Look at the failed step in the log; this is a CI failure, not a finding about this pull request." + } + + $lines = @( + "### Serialisation" + "" + "| Status |" + "|---|" + "| $status |" + "" + ) + + # Coverage is evidence that the run examined something, so a green summary can be told + # apart from a vacuous one. Only emitted when the branch leg actually reported figures: + # printing "Objects exercised: across loaded assemblies" for a run that never happened + # would be worse than printing nothing. + if ($Population -ne '') { + $lines += "Objects exercised: $Population across $Assemblies loaded assemblies, $Legs legs. Failures reported by the runner: $Failures." + $lines += "" + } + + return $lines +} diff --git a/.github/scripts/tests/Write-SerialisationSummary.Tests.ps1 b/.github/scripts/tests/Write-SerialisationSummary.Tests.ps1 new file mode 100644 index 0000000..c87a9c0 --- /dev/null +++ b/.github/scripts/tests/Write-SerialisationSummary.Tests.ps1 @@ -0,0 +1,150 @@ +# Write-SerialisationSummary.Tests.ps1 — Pester tests for the serialisation job-summary decision. +# +# The decision is a pure function over the step outcomes a workflow expression yields, which is +# why it was extracted: reaching these states for real needs a dependency closure, a build, a +# runner and, for half of them, a repository that fails serialisation. None of that is needed to +# check which summary a given set of outcomes produces. +# +# Every parameter is a string because that is what a workflow expression yields, and the empty +# string is meaningful: it is how a step that never ran is told apart from one that ran and +# failed. The tests pass empty strings deliberately wherever a real run would. +# +# Run locally: pwsh -Command "Invoke-Pester .github/scripts/tests -Output Detailed" +# Run in CI: lint-workflows.yml, the powershell-tests job. + +BeforeAll { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path + . (Join-Path $repoRoot '.github/scripts/Write-SerialisationSummary.ps1') + + # The coverage figures a healthy branch leg reports. Values are shaped like a real run's + # but carry no test weight beyond being non-empty. + $script:coverage = @{ + Population = '6053'; Assemblies = '83'; Legs = '6'; Failures = '0' + } +} + +Describe 'Get-SerialisationSummary' { + + Context 'the states the five conditions used to enumerate' { + + It 'reports that serialisation never started when the dependency closure fails' { + $out = Get-SerialisationSummary -DepsBranchConclusion 'failure' + ($out -join "`n") | Should -Match 'Serialisation did not run' + ($out -join "`n") | Should -Match 'Resolve dependencies \(branch\)' + ($out -join "`n") | Should -Match 'not a serialisation finding' + } + + It 'names the caller build instead when that is what failed' { + $out = Get-SerialisationSummary -BuildBranchConclusion 'failure' + ($out -join "`n") | Should -Match 'Build primary repo \(branch\)' + } + + It 'reports a clean branch leg without reaching for a baseline' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Pass' @coverage + ($out -join "`n") | Should -Match 'No serialisation failures \(Pass\)' + } + + It 'passes the job when every failure is already on the base branch' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Error' ` + -CompareVerdict 'ok' @coverage + ($out -join "`n") | Should -Match 'All are pre-existing on the base branch' + } + + It 'reports a regression when the comparison found new failures' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Error' ` + -CompareVerdict 'regression' @coverage + ($out -join "`n") | Should -Match 'Serialisation regression detected' + } + + It 'reports a runner failure when the branch leg itself died' { + $out = Get-SerialisationSummary -BranchRunConclusion 'failure' + ($out -join "`n") | Should -Match 'could not be verified' + ($out -join "`n") | Should -Match 'not a defect in this pull request' + } + + It 'reports a runner failure when the baseline was too broken to diff against' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Error' ` + -CompareVerdict 'baseline-unusable' @coverage + ($out -join "`n") | Should -Match 'could not be verified' + } + } + + Context 'the states nobody enumerated, which produced no summary at all' { + + # Seven steps run between the caller build and the branch-leg serialisation run: + # locating and building the verification solution, inferring its configuration, and + # publishing the runner. A failure in any of them left the check red and the summary + # blank, with nothing to say serialisation had not been attempted. + It 'describes a failure before the branch leg ran, rather than saying nothing' { + $out = Get-SerialisationSummary -DepsBranchConclusion 'success' -BuildBranchConclusion 'success' + + $out | Should -Not -BeNullOrEmpty -Because 'silence is what this replaced' + ($out -join "`n") | Should -Match 'did not reach a verdict' + ($out -join "`n") | Should -Match 'before serialisation ran' + } + + # Six steps run between the branch leg reporting failures and the comparison: the base + # checkout, a second dependency resolution, two builds, a configuration inference and + # the baseline run. + It 'describes a failure in the baseline leg, rather than saying nothing' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Error' @coverage + + ($out -join "`n") | Should -Match 'did not reach a verdict' + ($out -join "`n") | Should -Match 'baseline leg' + ($out -join "`n") | Should -Match 'after the branch leg reported failures' + } + + It 'still produces a summary when it can identify nothing at all' { + $out = Get-SerialisationSummary + + $out | Should -Not -BeNullOrEmpty + ($out -join "`n") | Should -Match 'did not reach a verdict' + } + + It 'says a stop is a CI failure and not a finding about the pull request' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Error' @coverage + ($out -join "`n") | Should -Match 'not a finding about this pull request' + } + } + + Context 'coverage figures' { + + # A green summary that examined nothing and a green summary that examined thousands + # read identically without these, which is the whole reason the runner reports them. + It 'carries the figures through when the branch leg measured them' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Pass' @coverage + ($out -join "`n") | Should -Match 'Objects exercised: 6053 across 83 loaded assemblies, 6 legs' + ($out -join "`n") | Should -Match 'Failures reported by the runner: 0' + } + + It 'omits them when the run never produced any, rather than printing empty ones' { + $out = Get-SerialisationSummary -DepsBranchConclusion 'success' + ($out -join "`n") | Should -Not -Match 'Objects exercised' + } + + It 'omits them on the path where serialisation never started' { + $out = Get-SerialisationSummary -DepsBranchConclusion 'failure' + ($out -join "`n") | Should -Not -Match 'Objects exercised' + } + } + + Context 'shape' { + + It 'always returns lines, never a bare string' { + foreach ($case in @( + @{ DepsBranchConclusion = 'failure' } + @{ BranchRunConclusion = 'success'; BranchStatus = 'Pass' } + @{ BranchRunConclusion = 'failure' } + @{} + )) { + $out = Get-SerialisationSummary @case + ,$out | Should -BeOfType [System.Object[]] -Because 'the caller pipes this straight into Out-File' + } + } + + It 'heads every summary except the did-not-run case with the check name' { + $out = Get-SerialisationSummary -BranchRunConclusion 'success' -BranchStatus 'Pass' @coverage + $out[0] | Should -Be '### Serialisation' + } + } +} diff --git a/.github/scripts/tests/ci-serialisation-action.Tests.ps1 b/.github/scripts/tests/ci-serialisation-action.Tests.ps1 new file mode 100644 index 0000000..de44b41 --- /dev/null +++ b/.github/scripts/tests/ci-serialisation-action.Tests.ps1 @@ -0,0 +1,88 @@ +# ci-serialisation-action.Tests.ps1 — structural assertions over ci-serialisation's action.yml. +# +# These test the shape of the workflow file rather than any script it calls. That is unusual +# here, and deliberate: all three properties below are invariants a reader cannot check by +# looking at one place in the file, and each of them was broken in a way that produced no +# failure anywhere. A composite action with a dozen steps has emergent properties, and nothing +# else in this repository asserts one. +# +# Text assertions rather than a YAML parse. The powershell-tests job installs Pester and +# nothing else, and adding a YAML module to reach three line-shaped facts would cost more than +# it returns. Each assertion below is written so that a restructure fails it loudly rather than +# passing vacuously. +# +# Run locally: pwsh -Command "Invoke-Pester .github/scripts/tests -Output Detailed" +# Run in CI: lint-workflows.yml, the powershell-tests job. + +BeforeAll { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path + $script:actionPath = Join-Path $repoRoot '.github/actions/ci-serialisation/action.yml' + $script:lines = Get-Content $actionPath + $script:text = $lines -join "`n" +} + +Describe 'ci-serialisation action.yml' { + + Context 'every outcome reaches the job summary' { + + # The failure this guards against is silent and reads as a tooling fault rather than a + # check finding: the job goes red and the summary is empty, so the author is told + # nothing at all about why. + # + # The action used to carry five summary steps, each gated on a different combination of + # step conclusions and outputs. Between them they named five of the eighteen functional + # steps, so a failure in any of the other thirteen matched no condition and wrote + # nothing. Seven of those thirteen run before serialisation is even attempted, which is + # the case a reader is least able to diagnose unaided. + # + # One always-gated step cannot have that hole. Enumerating conditions in YAML can, + # every time a step is added, which is why this asserts the shape rather than the list. + It 'writes the summary from a single step that always runs' { + $summarySteps = $lines | Where-Object { $_ -match '^\s*- name:\s*Write to Job Summary' } + @($summarySteps).Count | Should -Be 1 -Because 'one always-gated step cannot leave an outcome unreported; several mutually-exclusive ones can, and did' + + # The `if:` belonging to that step is the next one in the file. + $summaryIndex = ($lines | Select-String -Pattern '^\s*- name:\s*Write to Job Summary' | Select-Object -First 1).LineNumber - 1 + $condition = $lines[$summaryIndex..($summaryIndex + 4)] | + Where-Object { $_ -match '^\s*if:' } | + Select-Object -First 1 + $condition | Should -Match 'always\(\)' -Because 'a summary gated on success cannot describe a failure' + } + } + + Context 'build output does not drown the check it belongs to' { + + # setup-dotnet registers the csc problem matcher, which turns every MSBuild diagnostic + # line into a check-run annotation. Dependency and baseline builds compile code the + # author did not write, from paths outside the workspace that GitHub cannot resolve, so + # their warnings land against the wrong file with the wrong line number and consume the + # per-step annotation cap that the caller's own diagnostics need. + # + # Build-Dependencies.ps1 already applies this to dependency builds and records the + # measurement behind it. The four builds in this action are the same kind of work: a + # means to an end, where build warnings belong to ci-build instead. + It 'passes -clp:ErrorsOnly on every build it runs' { + $builds = $lines | Where-Object { $_ -match 'dotnet build' } + @($builds).Count | Should -BeGreaterThan 0 -Because 'if this finds nothing the pattern has drifted and the test is vacuous' + + $missing = @($builds | Where-Object { $_ -notmatch '-clp:ErrorsOnly' }) + $missing | Should -BeNullOrEmpty -Because "these builds annotate the check with warnings from code the author did not write:`n$($missing -join "`n")" + } + } + + Context 'staging is reset in one place' { + + # The assemblies directory is reset by resolve-dependencies, in its own Prepare folders + # step, which removes the directory and recreates it and does the same for Upgrades. + # A second reset in this action removes the contents of one of those two directories + # and runs immediately before the step that does the job properly, so it is both + # weaker and redundant. + # + # It is worth a test rather than a comment because a reader looking for where the two + # legs are separated finds this line first and reasonably concludes it is the mechanism. + It 'leaves the assemblies reset to resolve-dependencies' { + $resets = @($lines | Where-Object { $_ -match 'Remove-Item.*ProgramData\\BHoM\\Assemblies' }) + $resets | Should -BeNullOrEmpty -Because "resolve-dependencies resets this directory in its own Prepare folders step, so a reset here is redundant and reads as the between-legs separation when it is not:`n$($resets -join "`n")" + } + } +}