Fusion context_names Phase 1: session-scoped context and client-side validation (#25) - #109
Merged
juemerson-at-purestorage merged 53 commits intoAug 13, 2026
Conversation
The user-facing context surface, kept whole: the PfbContext object, connection context state, central injection in Invoke-PfbApiRequest, the three client-side gates (capability / cardinality / kind-vs-scope), Set-PfbContext, Clear-PfbContext, Invoke-PfbInContext, and -AllArrays. Depends on Phase 0 -- both the component resolver (dmann000#74) and the contextScope map field are load-bearing. Rebase onto main after Phase 0 merges rather than stacking. Six of the design doc's seven open questions are resolved here; OQ7 ships the client-side throw and stays open pending confirmation that one-fleet-per-array is guaranteed rather than a current limitation. Records that the design doc's line citations for Invoke-PfbApiRequest are stale -- the Assert-PfbApiCapability call is at :46, not :41 -- since injecting after it silently disables the version gate this design leans on. Refs dmann000#25 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The x-pure-remote-execution-context-domains-override declares ARRAY|FLEET, but the ARRAY half is satisfied only by the universal local-context short-circuit; measured from a remote member, a remote array name and <fleet>.arrays both return code 13 and only the bare fleet name is accepted. Recording 'array' made the Phase 1 kind-vs-scope gate throw on the only working context and permit one that silently targets the local replica. The operation also carries x-pure-block-remote-execution AND x-pure-incomplete-gre alongside the override -- block=true is itself falsified by a fleet context returning 200, and incomplete-gre is upstream's own statement that this annotation is unfinished. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ules Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
$script:PfbContextKinds and $script:PfbContextForms were never read: the live
vocabulary is the ValidateSet literals on the Kind/Form parameters. Editing the
constants changed no behavior, making them a fake source of truth.
ValidateSet cannot take a variable, so the literals must stay duplicated. A new
meta-test discovers every Kind/Form ValidateSet by parsing the AST of all .ps1
under Private/ and Public/, classifies each set by its own contents ('Fleet' =>
Kind, 'AllArrays' => Form), and asserts each category agrees. It also asserts at
least one site of each was found, so a no-match scan cannot pass silently. New
sites added by later tasks are covered with no edit to the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cover -Context behaviourally
Both cmdlets return a NEW connection object rather than mutating the
caller's, and make no network call. Set-PfbContext accumulates piped
names across process{} and emits exactly one connection in end{}, so
N piped fleet members yield one union-scoped connection instead of N
copies that each lose the others' entries. A missing -Context raises
an explicit throw rather than using [Parameter(Mandatory)], which
would prompt and hang under -NonInteractive.
Also, from review:
- Fix the Kind/Form ValidateSet drift meta-test's discriminator. It
classified each discovered site by its CONTENTS ('Fleet' => Kind,
'AllArrays' => Form), which silently left a site that DROPPED a
token unclassified and therefore never compared -- missing exactly
the drift the test exists to catch. It now classifies by the name
of the parameter the attribute decorates, read from the AST. The
scan stays fully dynamic, still asserts it located at least one
site of each kind, and still compares Kind only against Kind.
- Add [ValidateNotNull()] to Connect-PfbArray's -Context. $null
previously flowed into ConvertTo-PfbContextEntryList -Name, which
is [Parameter(Mandatory)] -- the interactive-prompt/hang risk. Not
added to Set-PfbContext's -Context: that is the ValueFromPipeline
slot, where $null must keep falling through to the friendly throw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review fixes for Set-PfbContext/Clear-PfbContext.
Important 1 -- the switch->Form mapping
`if ($AllArrays) { 'AllArrays' } else { 'Object' }` existed in both
Connect-PfbArray and Set-PfbContext with no test over either copy.
Extracted to a private Resolve-PfbContextForm beside the other context
primitives and called from both, so adding a third Form value cannot
update one copy and silently leave the other emitting 'Object'. No
ValidateSet on it -- it takes a switch, so it adds no site to the
vocabulary drift scan. One test per branch. Verified both call sites
actually route through it by mutating the helper to return 'Object'
unconditionally: the -AllArrays tests of BOTH cmdlets went red.
Important 2 -- a comment stating a falsified claim
Connect-PfbArray's -Context comment claimed $null into a mandatory
parameter "prompts and hangs under -NonInteractive". Measurement
disproved that: PowerShell prompts only when the argument is ABSENT;
an explicitly-supplied $null binds and is then rejected. Rewritten to
the true rationale -- the guard is about error ATTRIBUTION, moving the
failure to the binder naming -Context instead of a downstream -Name
the caller never typed. The ValidateNotNull-vs-NotNullOrEmpty sentence
is kept. Same correction applied to the matching test comment.
Important 3 -- the tri-state assertion
Clear-PfbContext's test used Should -BeNullOrEmpty, which passes for
$null, for @(), and for a context object with empty Entries -- so a
change emitting @() would not red, even though @() has a different
documented meaning at the Invoke-PfbInContext layer. Now asserts
$null -eq directly, matching the idiom in the Connect-PfbArray tests.
Also:
- Comment on Set-PfbContext's `if ($Context)` recording that the
truthiness test is deliberate: $null -ne $Context would admit ''
and mint an entry with an empty name. Behaviour unchanged.
- Cache-repointing test for Clear-PfbContext, closing the asymmetry
with Set-PfbContext's equivalent.
- Test that Connect-PfbArray -Context @() yields a NON-null
DefaultContext with zero entries -- the headline tri-state at the
connect layer, and the regression guard proving the new
ValidateNotNull still admits @().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also restores module connection-cache state after every mocked-connect test in Tests/Connect-PfbArray.Context.Tests.ps1, and adds a non-null guard so the unset-DefaultContext test cannot pass vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…p dead cast Connect-PfbArray mutates $script:PfbArrays in place, so capturing the reference and reassigning the same object left the 'fb.test' key leaking to later test files. Capture a shallow copy instead, and add a final ordered test asserting the key is absent so a silently-failing restore fails loudly. Also drop the [object[]] cast on New-PfbContext -Entries and its misleading comment: the @() wrapper around ConvertTo-PfbContextEntryList is the only thing required for the -Context @() escape hatch. Realign the -Array guard brace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y distinction Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add two mutation-killing tests for Resolve-PfbRequestContext: a falsy-but-present ContextOverride that a truthiness check would skip, and an explicitly empty QueryParams context_names that requires the @() wrapper. Comment-only clarifications in the implementation; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sion gate Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The URI cannot observe an injected context_names = '' -- both downstream sinks discard empty-string values -- so assert on what Assert-PfbApiCapability actually receives. Also corrects Get-PfbEndpointKey's doc comment: a drifted key silently disables the version gate, it does not throw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xt_names Fix round 1 on Task 9. Important 1: Resolve-PfbParameterComponent returns the map's DEFAULT component for entries that do not declare context_names, so the cardinality rule read $false for them and the gate advised narrowing a context the endpoint cannot take at all -- firing precisely where Assert-PfbContextCapability deliberately abstains (array beyond the map's scanned range). Extract Test-PfbEndpointDeclaresContextNames as the one home for that question and have both gates call it. This also makes the cardinality gate order-independent. Important 2: pin both gate call sites in Invoke-PfbApiRequest, and their relative order, which were completely unpinned -- deleting either call left every test green. Minors: recurse the single-home invariant sweep into Private/ subdirectories, add -Because explaining a 0-count refactor, and correct the if (-not $entry) comment which named an ordering dependency that measurement shows does not exist.
…rtition The GET partition's -Because claimed it detects a deleted scope early-return independently of the verb branch. Measured false: the allowlist added in fix round 2 returns before the throw for every unknown-scope GET, so the non-GET partition is what kills that mutation. The assertion is still needed -- it keeps the GET probe from vacuously passing on a $null key -- but its stated reason was wrong, the same defect class as the map-pin comment corrected in fix round 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the connect capture Fix round 1 for task 11. Critical: Resolve-PfbAuthorizationModel read .items off Invoke-PfbApiRequest's return value, which already unwraps the envelope and hands back an object[] of admin objects. The resolver therefore returned $null on every real array and the gate was permanently inert in production behind a green suite. Read the admin objects directly and match on name rather than taking row 0. The test that should have caught it mocked Invoke-PfbApiRequest and fed it the wire envelope -- a shape that function can never emit. Re-pinned at the Invoke-RestMethod boundary so the real unwrap runs. Also: pin the connect-time capture with a -Credential harness (deleting that one line disabled the whole feature and reddened nothing); move the gate below Assert-PfbApiCapability so a firmware blocker wins over an admin-model one, per Task 10's measured ruling, with a five-element ordering test; add an un-mocked request-path throw test; name the offending context values in the message; build one context object in Set-PfbContext instead of a throwaway; document the -ApiToken inertness and the 403 reconnect amplification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t is set
Maintainer ruling 2026-08-05: GET /admins is the only route to
authorization_model (POST /api/login returns just {"username":...}), so the
call is necessary -- but resolving it at every connect made sessions that never
touch Fusion pay for it.
Two sites only: Connect-PfbArray inside the -Context block, and Set-PfbContext's
end{}. Both are one-shot session setup, so no cache is needed and
AuthorizationModel stays two-state-plus-null. Invoke-PfbInContext is
deliberately excluded -- a per-call wrapper would mean one probe per loop
iteration.
Connect-PfbArray's cache assignment moved BELOW the context block so a rejected
context can no longer leave a connection the cmdlet never returned installed as
the default array. Set-PfbContext takes its copy before resolving, so the model
is written onto the copy and copy-on-write holds for it as it does for context.
Reworked FIX 2's capture test to pin the -Context path and added the test that
pins the point of the change: a bare connect issues no admin call at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s broke Fix round 3. Both Importants are the same mistake: round 2 added call sites without re-deriving what the old single site relied on. - Connect-PfbArray gated on $contextRequested alone, which is TRUE for -Context @() -- this codebase's explicit no-context state -- so a static admin's @() connect paid a probe and hard-threw with an empty value in the message. Now gates on both halves, mirroring Invoke-PfbApiRequest's $hasContext. - Resolve-PfbAuthorizationModel relied on "DefaultContext is still $null when I run", true only of the connect site. Set-PfbContext's $copy inherits the existing context and GET /admins declares context_names, so the identity probe was routed to a remote array -- or the kind gate threw inside the resolver and silently downgraded a known 'dynamic' to $null. The probe now strips both context slots from a copy, inside the resolver, so the invariant holds for every call site present and future. Docstring rewritten: it still asserted the false precondition. Also: Set-PfbContext's help said "No network call is made" and its guard test passed only because the fixture had no Username; both corrected. Comment at the connect block corrected -- the reconnect cache guards are endpoint-keyed, so the reordering narrows the window rather than closing it. Rejected -Context now releases the session it minted, without evicting a prior connection's cache entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…obe copy is benign Fix round 4, final for task 11. - The logout added last round had no detector: the existing test's throw-message and cache assertions all hold with the entire try/catch deleted. Now asserted at the Invoke-RestMethod boundary, -Times 1 -Exactly, with a second assertion requiring TimeoutSec so the timeout is pinned too rather than merely added. - The logout carries the connection timeout. An endpoint that accepts TCP and never answers would otherwise stall a cmdlet already on its way to failing. - Corrected the reconnect-window comment: since the context strip those writes repoint the caches at the resolver's PROBE COPY, not $connection. - Recorded at the strip site why the probe copy landing in the caches is benign -- the substitution only persists on a gate throw, a gate throw needs a static admin, and a static admin can never have had a context cached -- and stated the dependency: this stops being benign if a static admin is ever allowed to hold a context, or if either gate moves after its cache write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on model A controlled experiment on FB-A (REST 2.26) falsified Task 11's premise: flipping authorization_model on a remote admin changed nothing, while the local pureuser was denied cross-array regardless. Remote => permitted, local => cross-array denied. The shipped gate emitted false positives that blocked working sessions. Retarget the resolver at Admin.is_local (present from 2.17, the same floor as context_names, so no version guard), rename Resolve-PfbAuthorizationModel -> Resolve-PfbAdminLocality, Assert-PfbContextAuthorizationModel -> Assert-PfbContextAdminLocality and the connection property AuthorizationModel -> AdminLocality, and correct the docstrings, throw message and error annotation that asserted the falsified rule. Values stay tri-state strings 'local'/'remote'/$null. Update-PfbAdmin is untouched: authorization_model is a real request-body parameter there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…03 fixture, correct the falsified gate docstring Task 12c review round 1. Three medium findings, all mechanical: 1. Tests/Invoke-PfbApiRequest.ContextInjection.Tests.ps1 asserted Should -Not -BeLike '*Reconnect as a dynamic-model*', which the rename made unmatchable and therefore unfalsifiable. It is the mirror guard for the "do NOT re-merge these two clauses" invariant. Retargeted at the current production wording and verified armed by temporarily merging the clauses: it reds, on both editions. 2. Tests/Connect-PfbArray.Context.Tests.ps1's 'leaves AdminLocality null when the admin lookup fails' fixture still lacked DefaultContext and ContextOverride, so the resolver's probe strip crashed into its own catch and returned $null before the mocked 403 was reached. Declared both properties and added Should -Invoke Invoke-PfbApiRequest -Times 2 -Exactly, which reports "called 0 times" without them. 3. Assert-PfbContextAdminLocality's .DESCRIPTION claimed the gate "can never turn a would-be wrong-target success into a failure". Live testing falsified that: a local admin can successfully target its own array, so for a self-targeting context the gate does reject a call the server would have served. Stated honestly, with the ruling that makes it intended. Also: three stale model/cross-array wordings reconciled with Set-PfbContext's framing, AdminLocality hashtable alignment, and a pre-existing banned Should -BeNullOrEmpty in Tests/Set-PfbContext.Tests.ps1 converted to the tri-state idiom. The report's false claim that finding 2 was already fixed is corrected in place, marked as a correction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… pins Task 13 review Minor 1: the comment named the per-item-context test as the guard for "added AS RECEIVED -- never project or rebuild", but that test only pins that the per-item `context` field survives. A PSObject.Copy() or a rebuild forwarding `context` would both pass it, so a maintainer could read the comment as enforcing reference identity when nothing does. Reference identity is not a spec requirement, so narrow the comment rather than over-constrain the implementation with a new assertion. Comment-only; no executable change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…auth path
POST /api/login returns the authenticated admin's name in its 200 body, and has
done in every REST version 2.0-2.28. Both login paths captured only the
x-auth-token header and discarded the body, so Username was whatever the caller
typed -- and on the default -ApiToken set, which has no -Username parameter, it
was never populated at all. Resolve-PfbAdminLocality early-returns without a
username, so the Fusion admin-locality gate was permanently inert for the most
common way people connect.
Invoke-PfbApiTokenLogin now returns @{ AuthToken; Username } instead of a bare
token string, and the native username/password path parses the body too. Where
both a response username and a caller-supplied one exist the response wins: the
array's own spelling is what GET /admins?names= has to match. Certificate/OAuth2
keeps its parameter value -- a JWT exchange returns no username and there is no
/user endpoint to look one up from. Username is also normalized to $null rather
than the '' an unbound [string] parameter yields, so "no username known" is one
value on every path.
Comments in Resolve-PfbAdminLocality, Add-PfbContextErrorAnnotation and
Connect-PfbArray that documented the gate as ApiToken-inert are corrected, since
this change falsifies them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…in assertions Task 12b review minors, all comment- or assertion-level: - The docstring on Get-PfbLoginResponseUsername justified its PSObject.Properties read as required under StrictMode. That was false: Set-StrictMode appears nowhere in the module or the Pester harness, so a direct .Content read would return $null rather than throwing. The read is worth keeping, but as a deliberate defence against test doubles and rewritten responses -- not as a forced workaround. The test comment that repeated the claim is corrected too, and now says what the test actually pins: the observable contract, not the implementation choice. - 'leaves Username $null on the ApiToken set' asserted the exact value any swallowed failure would also produce. Added an AuthToken co-assertion so a green proves the login succeeded and the $null is the parse's answer rather than wreckage. - Two Username assertions used -Be where their four siblings use -BeExactly. Case is the whole point of this property; the file now teaches one idiom. - Connect-PfbArrayInternal is a third /api/login site and is now the only one that does not read the username from the body. Documented why that is correct (reconnect mutates the connection in place, so there is nothing to refresh) plus the one marginal gap it leaves, so the next reader does not take it for an oversight.
…not the caller's The best-effort read/mint of a long-lived API token matched $item.admin.name against $Username -- what the caller typed. That name comes from the same array as the login body, so the array's own spelling is the only one that can be relied on to match it. Now keys on $resolvedUsername. Maintainer's call. Be precise about what this closes, because the first description of it was wrong. PowerShell's -eq is CASE-INSENSITIVE, so 'pureuser' vs 'PUREUSER' always matched and case was never the failure mode. What did miss is a name differing beyond case: a directory-service admin logging in as 'jdoe' against an array that records 'jdoe@corp.example'. The match failed, no token was cached, and the session silently lost auto-reconnect with only a -Verbose line to say so. The new test therefore uses a directory-qualified name rather than a case difference. A case-only fixture would have passed against the old code too, proving nothing. ?names= on the mint POST is switched over for consistency only -- the endpoint ignores it and acts on the authenticated admin regardless.
…pe cmdlets Adds tools/Update-PfbContextHelp.ps1, which renders a delimited .NOTES block into every cmdlet whose endpoint carries a non-default (fleet or unknown) contextScope in Data/PfbCapabilityMap.json -- the same field the four client-side context gates validate against, so the help cannot drift from the enforced behaviour. 24 Public/ cmdlets affected (8 fleet-scoped preset endpoints, 16 unknown-scoped). Three fleet-scoped topology-group endpoints have no cmdlet in Public/ at all and are reported as MissingCmdlet rather than silently skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MissingCmdlet assertion only checked that the property existed, so deleting the reporting entirely would have survived it. Replaces that with an invariant: every non-array contextScope endpoint in the capability map must appear verbatim in a generated help block or in MissingCmdlet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…them Task 14 review findings I1, M1, M2 and M4. I1: a non-default contextScope value with no arm in Get-PfbContextHelpBody was dropped silently -- no warning, no summary entry -- which is the same "a silent skip reads as covered everything" failure the MissingCmdlet path already exists to prevent. The only difference is which half of the pairing is absent: there the cmdlet, here the render arm. Generate mode now warns and records the endpoint in a new UnrecognisedScope collection. -EmitLineOnly still returns a bare $null, which is what its array-scope test pins. This is defence in depth rather than a live bug: Build-PfbCapabilityMap maps an unrecognised domain token to 'unknown' rather than inventing a scope value, and Build-PfbCapabilityMap.Tests.ps1 asserts every endpoint's scope is one of fleet/array/unknown, so a new value cannot reach the shipped map without a deliberate edit in two other places. Hard to reach is not a reason to drop the endpoint quietly once it is reached. M2 and the phase's tri-state ruling on an explicitly empty scope: the selection test was a truthiness check, folding an empty scope in with an absent one and treating both as the 'array' default. An ABSENT scope is unset and correctly needs no note; a scope that is PRESENT and empty is a recorded value that says nothing, so it is now reported rather than guessed at. The test mirrors the generator's explicit null test for the same reason. M1: replaced the one banned Should -Not -BeNullOrEmpty, which also makes the file header's claim about it true. The negated form is unambiguous on its own, but the ban stays case-analysis-free. M4: the orphan-.NOTES limitation was documented only in the SDD report, which does not travel with the code. It is now in the generator's .DESCRIPTION. Tests: 13/13 both editions, 0 skipped (11 existing + 2 new).
…et-Help Get-Help -Full renders the opening delimiter verbatim, so users were shown "generated from Data/PfbCapabilityMap.json contextScope. Do not edit." -- an instruction addressed to a maintainer editing the .ps1. Shortened to "<!-- PfbContext (generated; do not edit) -->", which still marks the text as generated without the maintainer-facing detail, and regenerated all 24 blocks. Because that line is now expected to be reworded again, the strip phase keys on the stable "<!-- PfbContext" prefix instead of the full opening literal. Matching the whole literal would make any reword strip nothing and insert a second block beside the first, compounding on every later run -- and the idempotency test cannot see it, because by then the tree is already current and nothing needs stripping. Guarded by a new test whose fixture carries the previous real wording.
I5: the bare Should -Throw in 'does not repoint either cache when the composition is invalid' was satisfied by any throw raised before the cache write, including a login or mock-harness failure -- pin it to the composition message the way the sibling It 15 lines above already does. I6: replace all three -BeNullOrEmpty uses. The consequential one is the positive-polarity assertion on the tri-state AllowErrors, which passed for '' and @() and so never pinned the contract it reserves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I2: the Assert-PfbContextAdminLocality call in the request path cannot fire in production -- both AdminLocality resolution sites gate before returning a connection -- so describe it as defence-in-depth against a future third site or a hand-constructed connection, and point at the code-20 annotation for the Invoke-PfbInContext-only session. Call kept. M1: AdminLocality is not 'reserved for a later phase'; it is populated in the same function. Record the adjudicated I1 ruling on Get-PfbEndpointContextScope: dropping provenance is deliberate, because 'default' means no override AND not flagged x-pure-incomplete-gre, which is evidence rather than absent metadata. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on GET I3: Get-PfbContextHelpBody emitted 'targets a fleet-scoped resource and requires a bare fleet context' for every verb. Assert-PfbContextRequired is narrower for GET, twice over and both narrowings measured: an unfiltered read works with no context (served from the locally replicated, list-only copy), and a name-scoped read needs a context only on the endpoints in the measured allowlist. So the shipped .NOTES in Get-PfbPresetWorkload told the reader a fleet context was required six lines below an .EXAMPLE making exactly that context-free call. Add a GET arm keyed on the method parsed out of the endpoint key, split by the module's own $script:PfbNameScopedContextRequiredEndpoints -- dot-sourced, not copied, since a local copy is the very help/validation drift this generator exists to prevent. Regenerated Public/; one file changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-map key I4: Assert-PfbContextCapability treats 'no map entry at all' identically to 'entry without context_names', so with a session context set a cmdlet whose endpoint is absent from the map throws a false 'does not support the context_names parameter' before the wire. This feature is what makes a map gap fatal rather than merely uninformative, so it owes the invariant that catches the next one. AST discovery, no hardcoded files or lines, with a mandatory count guard so a scan that matched nothing cannot pass forever. The expected-unresolvable set is an exact-match tripwire in both directions and is deliberately self-retiring: GET /smtp and PATCH /smtp both go away with issue dmann000#80, and the failure message says to delete the set rather than update it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tions M9: the shipped-artifact test pinned scope and the fleet count but not provenance, so a regeneration that kept 'fleet' and flipped 'declared' -> 'default' would pass. The kind-vs-scope gate's whole design rests on that distinction, so pin GET /presets/workload as fleet/declared and pin the distribution of the three evidence-bearing provenances. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…has landed The endpoint-coverage invariant shipped with a deliberately self-retiring allowlist of GET /smtp and PATCH /smtp, asserted by exact equality so that it would fail in both directions: on a new map gap, and on the SMTP consolidation landing. dmann000#80 landed in PR dmann000#108 and the tripwire fired as designed. Get-PfbSmtp and Update-PfbSmtp are gone, consolidated onto /smtp-servers, and the module now carries no REST 1.x surface at all -- zero -ApiVersionOverride sites. The expected set is therefore deleted rather than updated, per its own failure message. Tests/RemovedCmdlets.Tests.ps1 (from dmann000#108) guards the removal itself. Every endpoint literal under Public/ now resolves to a capability-map key with no exceptions.
Phase 1 modifies 28 Public/ cmdlets and touches tools/Build-PfbCapabilityMap.ps1
but changes nothing under Reports/, so the committed artifacts went stale the
moment the branch rebased onto main. Regenerated with the full chain in order:
capability map, field-cmdlet map, response-shape map, drift report.
Data/PfbCapabilityMap.json came back byte-identical to the committed copy,
confirming the generator change does only what it claims. Data/PfbResponseShapeMap.json
is unchanged.
Two deltas, both expected and attributable:
* Drift report -- Resolve-PfbAdminLocality now appears alongside Get-PfbAdmin
for GET /admins, because it probes is_local there; and Update-PfbArray's
recorded paramBlockLine moves 29 -> 37, because the generated context help
block sits above the param block.
* Field-cmdlet map -- "typed but unresolved wire name" rises 40 -> 51. The 11
additions are the context parameters on Connect-PfbArray, Invoke-PfbInContext
and Set-PfbContext. These configure session state rather than a request body,
so having no wire field is correct rather than a gap.
Regenerated, never hand-edited: a prior integration produced a silent auto-merge
of Reports/PfbFieldCmdletMap.json matching no generator run on either side.
…p block The generated context help block was assembled with hardcoded CRLF and spliced into whatever the cmdlet file already contained. The repo commits LF blobs, so a Windows checkout has CRLF and matches, while a Linux/macOS checkout has LF and mismatches on every file. The generator therefore reported all 24 files as changed and rewrote them, and the idempotency test failed with "Expected 0, but got 24" on ubuntu-latest and macos-latest while both windows-latest legs passed. Set-PfbContextHelpBlock now detects the newline from the content it is splicing into and normalises the block to match, rather than trusting the platform: the checked-out file decides this, not [Environment]::NewLine. The two separators the function inserts itself follow the same value. The strip pattern already handled both, so it is unchanged. Behaviour-neutral on Windows -- a -WhatIf run against the committed tree still reports zero changes -- so no Public/ file needed regenerating. Adds two tests that build LF and CRLF fixtures explicitly via WriteAllText and run the generator against each. The pre-existing idempotency test could never catch this on a single runner, because it only ever sees the local platform's convention; these fail on every platform when the splice stops adopting the target's line ending. Verified by mutation: pinning the newline back to CRLF reds the LF case and leaves the CRLF case green, which is exactly the asymmetry that kept Windows CI green while Linux and macOS failed.
Phase 1 delivers context_names, so the shipped drift report claiming it is "not yet implemented" contradicts the same change that implements it. The report still lists the 269-endpoint gap -- the drift detector matches only a literal $Var['key'] IndexExpressionAst, and the injection site in Private/Invoke-PfbApiRequest.ps1 writes $QueryParams[$script:PfbContextParameterName], so the scan cannot see it. The note now says that, and points at issue dmann000#113, which owns teaching Get-PfbCentralInjectionSites to resolve the constant. Removing the gap itself is deliberately NOT done here: context_names is the largest single systemic gap, and dropping it moves the Task 6 real-data invariants, whose top-10 aggregation ratio is presently 54.81% against a 55% ceiling. That needs its own measurement and a reviewed re-tune. allow_errors stays a gap and keeps its "not yet implemented" note -- it is genuinely unimplemented, deferred to Phase 2 by this branch's own spec (docs/design/fusion-context-phase-1-spec.md, "Phase 2 -- allow_errors end-to-end"). Only the wording gains the deferral, so the distinction between the two fields is visible in the report. Both entries referenced docs/design/fusion-context-injection.md, which does not exist in the repo and never did -- that design doc was never committed. Both now point at the Phase 1 spec, which covers context_names' implementation and records allow_errors' deferral. Regenerated Reports/ so the committed artifacts match the annotation source: 10 changed lines total, all annotation propagation, no unrelated drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ding
The two Task 7 annotation tests pinned a substring of each note's prose
('not yet implemented', '403'). Rewording the context_names annotation in
f5d2ef3 turned that into a red build on all three pwsh legs -- one failure,
identical on ubuntu/macos/windows, and green on 5.1 only because the whole
Describe is PS7-gated.
Assert the wiring instead: read docs/drift-annotations.json, and require the
generator to attach each annotation's note to its matching finding verbatim.
Rewording, retiring, or adding an annotation is now a docs-only change, while
a broken or paraphrasing annotation path still fails. Each test carries a
non-vacuity floor, so a lookup that silently matches nothing fails rather than
passing over an empty loop.
Endpoint matching uses .Contains on lowered strings rather than -like, which
treats backtick and [ ] as pattern syntax.
Mutation-verified: paraphrasing the note in the generator, and making
Find-PfbDriftAnnotation match nothing, each turn both tests red; clean tree is
65/0/0 on pwsh7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of Fusion
context_namessupport (#25). Adds session-scoped context so a singleconnection can address other arrays in a fleet, with client-side validation that fails fast
instead of relaying the server's assorted rejection codes.
Phase 0 (#98) is the prerequisite and is already on
main. This branch is rebased onto it.What this adds
Three new cmdlets:
Set-PfbContextClear-PfbContextInvoke-PfbInContextConnect-PfbArrayalso gains-Context,-Kindand-AllArrays, so a connection can beestablished with a context already in place.
Context is session state, not a per-cmdlet parameter. No existing cmdlet gains a
-ContextNamesparameter. Resolution happens once, at the single choke point inInvoke-PfbApiRequest, which is what keeps 500+ cmdlets from each needing to know aboutcontexts.
Design points worth a reviewer's attention
One resolution point, one predicate.
Resolve-PfbRequestContextis the only place acontext is resolved, with a documented precedence order. The
$null(unset) vs@()(explicit "run this one call locally") distinction is a tri-state that is pinned at every
layer that consumes it, not just at the resolver — the two states behave differently on
purpose and must not be unified.
Injection is ordered before the version gate.
context_namesis added to the queryparams before
Assert-PfbApiCapabilityruns, so the version check sees the parameter it ismeant to be checking. A test drives this through the real injection path and asserts on what
the mocked capability gate actually received — asserting on the request URI cannot detect the
bug, because an empty injected value is discarded downstream by two separate code paths.
Four client-side gates, all wired at the choke point: capability (does this endpoint
declare
context_namesat this REST version), cardinality (single vs multi-value),kind-vs-scope (is a fleet name valid here, or an array name), and required-context (some
fleet-scoped operations cannot be performed without one). Each has a call-site wiring test
that goes red if the call is deleted — this branch previously shipped a gate that was wired
but could not fire, twice, so those tests are deliberate.
Generated help cannot drift from enforcement.
tools/Update-PfbContextHelp.ps1renderseach affected cmdlet's context
.NOTESblock from the samecontextScopefield the runtimegates read, and refuses to be quietly incomplete: an endpoint with no cmdlet, or a scope value
with no render arm, is reported rather than skipped. A test asserts full accounting over every
non-default-scope endpoint.
Scope decisions
contextScope'sarraydefault is deliberate and is not a guess.provenance: defaultmeans the upstream spec carries no domain override and the operation is not flagged
x-pure-incomplete-gre— upstream's own marker for where its remote-execution annotation isunfinished. A fleet-scoped operation with a complete annotation would carry an override.
Endpoints upstream flags as incomplete are recorded
unknown, which suppresses thekind-vs-scope check rather than throwing. The design doc's justification for this default was
corrected during review: mis-marking is not harmless (the gate throws, so both directions of
error can block a call); the default rests instead on coverage — nearly every endpoint accepts
an array context, so an
arraydefault leaves a working form available even where it ismistaken.
Preset operations are fleet-scoped by architecture, not by configuration: a preset is a
fleet-database template, so a non-fleet array can neither create nor hold one. This is
documented in the published FlashBlade CLI Reference; the OpenAPI spec does not carry the rule.
An unfiltered
Get-PfbPresetWorkloadstill works with no context, returning the locallyreplicated list, and the generated help says so.
Topology-group cmdlets remain out of scope (#38). This branch owes #38 exactly one thing:
the contract that
Get-PfbTopologyGroupemit a top-levelNamefor pipeline binding.#25 stays open. Phases 2 and 3 remain.
Report artifacts
Regenerated after the rebase, with the full chain in order — capability map, field-cmdlet map,
response-shape map, drift report.
Data/PfbCapabilityMap.jsoncame back byte-identical to thecommitted copy, which confirms the one-line generator change does only what it claims.
Data/PfbResponseShapeMap.jsonis unchanged.Two deltas that will look odd without explanation:
PfbFieldCmdletMapping.md: "typed but unresolved wire name" rises 40 → 51. The 11additions are the context parameters on
Connect-PfbArray,Invoke-PfbInContextandSet-PfbContext. These configure session state rather than a request body, so having nowire field is the correct classification, not a coverage gap.
PfbApiDriftReport.jsonlistsResolve-PfbAdminLocalityunderGET /admins. It doescall that endpoint, to read
is_local. Note it lives inPrivate/and is not exported, sothe drift scanner is counting a private helper as a cmdlet — arguably a generator issue
worth its own ticket rather than anything this branch should change.
Update-PfbArray'srecorded
paramBlockLinealso moves, because the generated help block sits above the paramblock.
Testing
Both PowerShell editions, per-
Describe, via the repo's scoped runner. Unit coverage ismocked; the wire behaviour behind the scope and cardinality rules was measured against a live
FlashBlade during development, and those measurements are what the capability map's
live-testedprovenance records.No version bump and no
CHANGELOG.mdchange, per project convention — those are themaintainer's call.
Follow-ups, not included here
provenance: defaultwhile the family's reads arelive-tested
fleet. Nothing reaches those endpoints yet; worth re-checking when Fusion Fleet/Realm management cmdlets (realm-connections, topology-groups, remote-realms) #38 lands.x-pure-block-remote-executionexpands sharply at fb2.28 and cannot be used as a runtimesignal without first excluding the operations flagged
x-pure-incomplete-gre.contextScopehas no version dimension.