Skip to content

Refactor: descriptors, one Step runner, vocabulary and translation guards (#17, #18, #20, #21, #22) - #32

Merged
DireDoch merged 5 commits into
mainfrom
refactor
Sep 3, 2026
Merged

DireDoch merged 5 commits into
mainfrom
refactor

Conversation

@DireDoch

@DireDoch DireDoch commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Implements #17, #18, #20, #21 and #22, in the order their dependencies impose:
#22 → #21 → #20 → #17 → #18. One commit per issue.

#19 is not implemented — see the note at the bottom.

What each commit does

Issue Commit The point
#22 82d9d82 Test-WcdNetworkAdapterActive folded into its callers; the virtual-adapter exclusion list promoted to a named script-scope variable at the top of the file, where someone whose VPN client turned up in the summary will find it.
#21 91d5921 The Form Factor / Environment / Optional Tool drift renamed out of Resolve-WcdExecutionOptions and both $T tables; $moduleStatus loses the three French identifiers. The extended stale-vocabulary pattern is the actual deliverable.
#20 c5923f1 tests/Translations.Tests.ps1 asserts the EN and FR tables expose identical keys, recursing into Checklist, Labels, Remedy and ComputerNameRejected.
#17 5da3ed2 Every Config-*.ps1 exports a descriptor. The $modules array and the switch ($modName) dispatch are gone; the orchestrator globs src/Config-*.ps1.
#18 d7e1394 Invoke-WcdStep owns the progress events, both log lines, the try/catch and the Result shape. Config-Power and Config-TaskbarLeft converted; the other ten left alone deliberately.

Two issues disagreed with the code, and the code won

How "byte-identical" was verified

Not by reading the diff. Two harnesses dot-source both trees and compare:

  • Modules declare themselves with a descriptor instead of six registration edits #17 — progress plan, every checklist row (Step, Label, Kind,
    Detail) and the rendered line, for 7 scenarios × 2 languages:
    laptop/desktop, workstation/vdi, declined apps, identity applied, update
    reboot pending, no printers, winget missing, nothing ran, unelevated power.
    554 lines, zero diff.
  • Invoke-WcdStep: one Step runner behind every Module #18 — every Result property, every log line (timestamps stripped) and
    every progress event, for 8 paths: laptop/desktop × elevated/unelevated,
    powercfg throwing, registry write succeeding / denied / failing generically.
    58 lines, zero diff — including property order, which the JSON report
    reproduces and which needed [ordered] on the Power fragment to preserve.

Each new guard was also verified by breaking the thing it guards:

Also in here

  • CLAUDE.md: how to run pwsh and Pester on Linux without sudo, and which
    two tests fail there on purpose (New-WinUserLanguageList and
    Get-CimInstance cannot be mocked away — Pester's Mock needs the command to
    exist first). A clean local run is 165 passed / 2 failed; CI on Windows is
    still the authority.
  • Manual chapter 7 rewritten — registration is one code block in one file — and
    a new section on when Invoke-WcdStep is the wrong tool. docs/manual.pdf
    recompiled.
  • Dropped as dead: $T.ModuleNotFound in both tables (a globbed file cannot be
    missing) and the PrinterAdd / PrinterSkip step labels, which no Module
    emitted.
  • .gitignore picks up docs/research/, which was already uncommitted in the
    working tree.

#19 is deliberately not here

Its own author writes that it "should be closed as wontfix without
embarrassment if it keeps losing to more useful work", and that it is worth
doing for tidiness rather than for extensibility. #20 is what actually stops the
bug, and it landed. Closing #19 as wontfix for the reason the issue already
gives.

🤖 Generated with Claude Code

DireDoch and others added 5 commits September 3, 2026 16:46
Test-WcdNetworkAdapterActive was one regex behind 22 lines of function and
comment-based help. The issue counted one caller; there are two - the
Where-Object in Get-WcdActiveNetworkAdapters and the Wi-Fi branch in
Set-WcdNetworkDiagnostics - so the regex is now written at both, and the
function is gone.

The virtual-adapter exclusion list is the real domain knowledge in this file
and was buried between two functions that did nothing. It is now
$script:WcdIrrelevantAdapterPattern at the top, where someone whose VPN client
turned up in the summary will find it.

tests/Config-Network.Tests.ps1 gains three tests that hit the exclusion
directly rather than through a whole diagnostic run, plus one that pins the
active-and-relevant combination the inlining touched.

Also adds CLAUDE.md: how to run pwsh and Pester on Linux without sudo, and
which two tests fail there on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CONTEXT.md names Form Factor, Environment and Optional Tool. The interface
already honoured them; the implementation drifted two lines in, so someone
grepping for Environment found the parameter and stopped, because the value
was called $usageResult from the moment it was read.

Locals in Resolve-WcdExecutionOptions:
  $deviceResult   -> $formFactorResult
  $usageResult    -> $environmentResult    $usageLabel   -> $environmentLabel
  $engineerResult -> $optionalToolResult   $engineerTypes -> $optionalToolNames
  $engineerYesNo  -> $optionalToolAnswer

$T keys, in both language tables:
  PromptUsageDesc1/2 -> PromptEnvironmentDesc1/2
  PromptEngineer, PromptEngineerDesc1/2 -> PromptOptionalTool, ...Desc1/2
  Engineer{BoxTitle,CombineHint,ChoicePrompt,AtLeastOne,InvalidChoice,Selection}
    -> OptionalTool...

The issue lists only the first four Engineer keys; renaming the other four too
is what lets the guard pattern anchor on them rather than leave half the drift
in place. The EN display strings still say "Engineering workstation" - that is
correct English for the technician, and only the keys were wrong.

$moduleStatus loses the three French identifiers: Etapes/Echecs/Avertissements
-> Steps/Failures/Warnings. Format-WcdModuleLine was the only reader.

The guard is the actual deliverable. tests/Help.Tests.ps1 now builds the stale
pattern once in BeforeAll and uses it twice: once to sweep src/*.ps1, once to
assert it does NOT flag Config-DeviceManager, Get-WcdPnPDevices,
Set-WcdDeviceManagerStatus, or the FR display strings 'Echecs: {0}' and
'Avertissements: {0}'. Verified by reintroducing $deviceResult, the Etapes
hashtable key and $ModuleStatus.Etapes one at a time - each fails the test.

132 passed / 2 failed locally; both failures are the known Windows-only ones
documented in CLAUDE.md. PSScriptAnalyzer clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key present in one table and absent from the other does not throw:
PowerShell returns $null and the checklist renders a blank label, in one
language only. Nobody notices until a technician runs the tool in French.

tests/Translations.Tests.ps1 extracts the single $T assignment from
src/Invoke-WcdConfiguration.ps1 with the AST parser - the orchestrator cannot
be dot-sourced, it would run the whole run and exit - and replays it once per
-ScriptUI value. Both tables are pure literals, so replaying them builds two
hashtables and does nothing else.

The comparison recurses: Checklist, Labels, Remedy and ComputerNameRejected
are nested, and a flat .Keys comparison misses exactly the cases that hurt.
It runs both directions, so a key added to FR alone fails too.

Also asserts no value is empty - a present-but-blank key renders the same
blank line an absent one does.

Verified by deleting one key at a time and re-running: a top-level FR key
(WaitEnterFinish), a nested FR key (Checklist.Erp) and a nested EN key
(Remedy.RequiresAdmin) each fail the test. No divergence exists today, so
nothing needed fixing.

Not covered: format placeholders differing between the two languages, e.g. an
EN string with {0} whose FR translation dropped it. Same class of silent bug,
but out of scope here - add it when one bites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion edits (#17)

Adding a Module was four small edits according to the manual and six in
practice, across three files. Miss one and nothing fails: a missing
progress-plan entry dropped the Module from the run silently, because
$moduleStepPlan.ContainsKey treated "not planned" and "planned as empty"
differently and an absent Module ran with a $null step list. A missing
$T.Checklist key rendered a blank label.

Every src/Config-*.ps1 now exports Get-Wcd<Name>Descriptor, returning Name,
Order, RowOrder, Steps, Rows and Invoke. The orchestrator globs Config-*.ps1,
dot-sources each, calls its descriptor and builds everything from what comes
back. The $modules array and the switch ($modName) dispatch are gone.
Get-WcdModuleProgressPlan and Get-WcdTechnicalStepLabels now assemble
descriptors instead of hardcoding twelve Modules.

Absorbing the non-uniformity the issue lists:

- Form-Factor Steps: a Step carries Planned = $false rather than being absent,
  so Config-Power still plans five on a Laptop and two on a Desktop while the
  three battery/lid labels survive for the diagnostic.
- Manifest Steps: Config-Applications and Config-Printer read $Config, which
  the descriptor receives.
- Conditional Modules: Steps = @() still means "skip this Module", and the
  Module's rows are emitted anyway - a declined rename owes the technician a
  Manual Step saying so.
- Rows that are not one-per-Module: Config-Disk gives two, Config-WindowsUpdate
  folds two Steps into one. A row is either @{ Label; Steps } or a fixed
  @{ Label; Kind; Detail }, with MissingKind / MissingDetail / OmitWhenMissing
  for the cases that need them.
- Run order: Order and RowOrder are separate, because the checklist is not in
  run order - it never was. Both live in the Module's own file.

Two things stay in the Diagnostic because they belong to no Module, now named
rather than inline: Resolve-WcdRestartEntry (raised from Results across
Config-Identity and Config-WindowsUpdate) and the trailing Manual Steps.

Descriptors take -Translations rather than reading $script:T, so a Module stays
testable on its own. The signature is uniform across all twelve, which means
some descriptors declare parameters they do not read; PSReviewUnusedParameter
is suppressed per function with that justification rather than repo-wide.

A descriptor that is missing or malformed stops the run at startup naming what
is wrong - Test-WcdModuleDescriptor holds the contract, and
tests/ModuleDescriptor.Tests.ps1 runs all twelve Modules through it under three
execution profiles, checks Order/RowOrder uniqueness, checks every row
references a Step the Module declares, and checks the guard itself rejects each
missing field. A Module that fails to dot-source still degrades to an ERROR row
and lets the run continue, as before.

Verified byte-identical against main: a harness dot-sources both trees and
dumps the progress plan, every checklist row (Step, Label, Kind, Detail) and
the rendered line for 7 scenarios x 2 languages - laptop/desktop,
workstation/vdi, declined apps, identity applied, update reboot pending, no
printers, winget missing, nothing ran, unelevated power. 554 lines, zero diff.

Dropped: $T.ModuleNotFound in both tables (a globbed file cannot be missing),
and the dead PrinterAdd / PrinterSkip step labels, which no Module emitted.

Manual chapter 7 rewritten: the registration section is one code block in one
file, with a table explaining each field. The row label in both $T tables is
called out as the one thing still outside - and #20's parity test now catches
forgetting the second one.

153 passed / 2 failed locally; both are the known Windows-only failures.
PSScriptAnalyzer clean. Manual recompiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rLeft (#18)

Every Module wrote the same twelve lines around the one line that does the
work, and the Result shape was a literal repeated across twelve files. Its
optional fields - Severity, RemedyKey, RemedyArgs, Applied, RebootPending - had
no schema anywhere: a typo in a property name was not an error, the Diagnostic
read $null, and the row quietly rendered as a plain success.

Invoke-WcdStep owns the progress events, both log lines, the try/catch and the
Result shape. The five awkward cases the issue lists are absorbed by one
mechanism, not five parameters: the Action returns nothing when the Step simply
worked, or a hashtable merged over the Result.

  @{ Severity; Error; RemedyKey }  did not run, and why - reported, not failed
  @{ Severity; Error }             severity read off the machine, not a throw
  @{ Applied }, @{ RebootPending } extra fields the Diagnostic reads
  @{ RemedyKey; RemedyArgs }       format arguments for the remediation
  @{ Log }                         a log line other than -SuccessLog

Anything the Action emits that is not a hashtable is ignored, so an Action
calling a command that writes to the pipeline still means "it worked".

The one extra parameter is -OnFailure, which classifies a caught error into a
failure fragment. Config-TaskbarLeft had the registry-GPO classification
written out twice; it is now one scriptblock both Steps share. Without it that
Module would need the -Raw escape hatch the issue warns about.

Progress kinds come from Complete-WcdProgressStep, which already decided that
mapping - the runner does not repeat it.

Config-Power and Config-TaskbarLeft converted. Their tests pass unchanged, but
those cover only 2 of the 8 paths, so the Results were also compared against
main directly: a harness dot-sources both trees and dumps every Result property,
every log line (timestamps stripped) and every progress event for laptop and
desktop, elevated and unelevated, powercfg throwing, and the registry write
succeeding, denied and failing generically. 58 lines, zero diff - including
property order, which the JSON report reproduces and which needed [ordered] on
the Power fragment to preserve.

tests/WcdHelpers.Tests.ps1 gains 12 tests: success, throw, pipeline noise
ignored, did-not-run-with-reason, OnFailure classifying, OnFailure not being
allowed to declare success, severity chosen by the Action, extra fields,
RemedyArgs, the Log override staying out of the Result, no log line when there
is nothing to say, and Step/Success/Error staying first.

The remaining 10 Modules are left alone deliberately, and the manual now says
why: Config-Applications and Config-Printer loop over manifest entries,
Config-Network branches on what the adapters reported, and a runner half its
callers escape from is not a runner.

165 passed / 2 failed locally; both are the known Windows-only failures.
PSScriptAnalyzer clean. Manual recompiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant