Browse Source

docs: correct the paths acceptance record after its review

The independent review of the acceptance record found no Blocker or Major
issue. This commit corrects what it found: the commit that fixed the break
row, what the State test shows, the baseline failures that carry no message,
the count of results, the wording about the folders before the first run, the
truncated messages in the results file, the README row of case 10, and the
review section of the record.

The 42 messageless baseline failures are now explained by a diagnostic that
runs the bodies of those tests in a TEMP sandbox: on the base, the second item
is removed, copied, moved, re-owned, or rewritten after Select-Object -First 1
or a throw; on the candidate it stays. The diagnostic, the check of the result
files, and a read-only check of a published version are in Tests/Lab/Acceptance.

Decision 22 no longer says that Copy-Item2 is like Copy-Item for a folder: the
built-in Copy-Item creates the missing parent folders of a folder copy,
Copy-Item2 of rc6 and of the candidate doesn't. The choice stays, the reference
point is corrected, and the question is left to the maintainer. The migration
hint of item 8 is stated as it is.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/119/head
Raimund Andree 2 days ago
parent
commit
708afdf070
  1. 42
      .memory-bank/decisions/0022-phase-2-behavior-changes.md
  2. 14
      Tests/Lab/Acceptance-2026-10-09-quality-gate-paths-Results.csv
  3. 123
      Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md
  4. 224
      Tests/Lab/Acceptance/Probe-LaterCommand.ps1
  5. 111
      Tests/Lab/Acceptance/Test-PublishedRelease.ps1
  6. 68
      Tests/Lab/Acceptance/Validate-LabResults.ps1
  7. 2
      Tests/Lab/README.md

42
.memory-bank/decisions/0022-phase-2-behavior-changes.md

@ -30,7 +30,7 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review; c
| 7 | The link cmdlets stopped with terminating errors | **Breaking:** a non-terminating error per link, and the next link |
| 8 | `-Path` and `-Target` of the link cmdlets were optional | **Breaking:** required. An omitted `-Path` failed with an index error, an omitted `-Target` meant the current location |
| 9 | Entries and descriptors are equal only as the same .NET object | Kept the equality of .NET; the FAQ shows `Compare-Object -Property` |
| 10 | `Copy-Item2` doesn't create the missing destination folders (rc6) | Kept, like `Copy-Item` and `Move-Item2` |
| 10 | `Copy-Item2` doesn't create the missing destination folders (rc6) | Kept, like `Move-Item2`, and for a file like `Copy-Item`. Corrected on 2026-10-09: for a folder, `Copy-Item` creates the missing parent folders and `Copy-Item2` doesn't |
- Found on the way and fixed: every object piped to the link cmdlets
failed with `GetDefaultValueFailed` (item 8). Item 6 is not the cause of
@ -48,7 +48,9 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review; c
- Rationale: an error instead of a result that looks valid (1, 2, 6);
per-item errors, as in the other cmdlets (7); no silent default for a
path that creates something (8); no new features before the archive
(3); the conventions of .NET and PowerShell (4, 9, 10).
(3); the conventions of .NET and PowerShell (4, 9); no implicit creation
of folders (10; `Copy-Item` creates them for a folder, see the correction
below).
- Open: the maintainer accepts or reverts each choice; then this record
becomes `accepted`.
@ -58,11 +60,16 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review; c
the agent to continue with the next work and, for any decision that comes
up, to "do it and report about it later". The handoff for this record asks
for one question per item, which nobody could answer overnight. The agent
checked each choice against the source, the tests, the cmdlet pages, and
the changelog, and confirmed all ten. This is the agent's decision under
that delegation, not the maintainer's own, so the status stays `proposed`
until he confirms it or reverts an item. Nothing in the code, the tests,
or the help changed.
compared each choice with the changelog and the cmdlet pages, and checked
item 10 against the source and against the built-in `Copy-Item`; it did
not run the tests of the other items again for this record (they ran with
the suite of rc7 and of the later branches). An independent read-only
review checked the statements of the table below against the same pages
and found them accurate except two, which are corrected here (item 10, and
the migration hint of item 8). The agent confirmed all ten choices. This is
the agent's decision under that delegation, not the maintainer's own, so
the status stays `proposed` until he confirms it or reverts an item.
Nothing in the code, the tests, or the help changed.
- Impact for a caller, and where the choice is documented (the changelog
under [Unreleased], and the page of each cmdlet in `Docs\Cmdlets`):
@ -77,15 +84,22 @@ source: agent choices in autopilot on 2026-10-08, for the maintainer's review; c
| 7 | **Breaking:** the link cmdlets write a non-terminating error per link and go on; a script that relies on the stop needs `-ErrorAction Stop` | both link pages, notes; the changelog, **Breaking** |
| 8 | **Breaking:** `-Path` and `-Target` are required; a script that omitted one must pass it | both link pages, notes; the changelog, **Breaking** |
| 9 | None: entries and descriptors are equal only as the same .NET object, as in .NET; `Compare-Object -Property` compares values | `Docs\FAQ.md` |
| 10 | Only against the earlier 5.0.0 prereleases: `Copy-Item2` no longer creates the missing folders of the destination of a folder copy, like `Copy-Item` and `Move-Item2` | `Copy-Item2` page; the changelog |
| 10 | Only against the earlier 5.0.0 prereleases, which created the missing parent folders of a folder copy: `Copy-Item2` writes an error that names the missing folder, as `Move-Item2` does. It differs from `Copy-Item`, which creates the missing parents of a folder copy (checked in both editions on 2026-10-09; for a file, `Copy-Item` writes an error as well). A script that relied on the prerelease behavior creates the folder first. Published rc6 and the candidate both write a `CopyError` and create nothing (checked in both editions on 2026-10-09) | `Copy-Item2` page; the changelog |
- Why all ten stand: 5.0.0 is a major version, so documented breaking
changes are allowed (7 and 8 have a **Breaking:** entry and a migration
hint); 1, 2, and 6 replace a result that looked valid with an error or a
complete result; 3, 4, 9, and 10 follow the conventions of .NET and
PowerShell and add no feature before the archive; 5 is a clearer message.
Reverting item 8 would bring back the failure for every object piped to
the link cmdlets (found on the way, above).
changes are allowed (7 has a **Breaking:** entry with a migration hint,
`-ErrorAction Stop`; the **Breaking:** entry of 8 names the old behavior,
and the migration is to pass both parameters); 1, 2, and 6 replace a
result that looked valid with an error or a complete result; 3, 4, and 9
follow the conventions of .NET and PowerShell and add no feature before
the archive; 5 is a clearer message. Item 10 adds no feature either, but
its reference point was wrong: it isn't like `Copy-Item` for a folder.
The stricter behavior is the safer default and matches `Move-Item2`, and
creating missing parents would flip the behavior of rc6 and rc7 again, so
the agent keeps it and leaves the question, whether `Copy-Item2` should
create the missing parents of a folder copy like `Copy-Item`, to the
maintainer. Reverting item 8 would bring back the failure for every object
piped to the link cmdlets (found on the way, above).
- To revert an item: revert its commit (the range in Context), regenerate
the help from `Docs`, adjust the changelog and the cmdlet page, and run
the four test configurations again; a later commit on the same page or

14
Tests/Lab/Acceptance-2026-10-09-quality-gate-paths-Results.csv

@ -16,7 +16,7 @@
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","Admin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Admin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Admin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
@ -45,7 +45,7 @@
"Core","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Clear.txt]'."
"Core","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Descriptor.txt]'."
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","Delegate","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Delegate","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry","Failed","Passed","Expected strings to be the same, but they were different."
@ -73,7 +73,7 @@
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Core","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Core","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Core","ServerAdmin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Core","ServerAdmin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
@ -94,7 +94,7 @@
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Admin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","Admin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Admin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
@ -121,9 +121,9 @@
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","Delegate","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Clear.txt]'."
"Desktop","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]'"
"Desktop","Delegate","An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError","Failed","Passed","Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]'."
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","Delegate","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Delegate","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","Delegate","InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry","Failed","Passed","Expected strings to be the same, but they were different."
@ -151,7 +151,7 @@
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else","Failed","Passed",
"Desktop","ServerAdmin","A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else","Failed","Passed","Expected like wildcard '*Downstream failure*' to match $null, but it did not match."
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter","Failed","Passed","Expected a collection with size 1, but got an empty collection."
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of"
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter","Failed","Passed","Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument ""pattern"" is null. Change the value of argument ""pattern"" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11"
"Desktop","ServerAdmin","Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","ServerAdmin","InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."
"Desktop","ServerAdmin","InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone","Failed","Passed","Expected strings to be the same, but they were different."

1 Edition Role Test Baseline Candidate BaselineFailure
16 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
17 Core Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
18 Core Admin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
19 Core Admin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
20 Core Admin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
21 Core Admin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
22 Core Admin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
45 Core Delegate An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Clear.txt]'.
46 Core Delegate An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Core-20261009213136\Case10\Delegate\Owner\Descriptor.txt]'.
47 Core Delegate Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
48 Core Delegate Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
49 Core Delegate Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
50 Core Delegate InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
51 Core Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry Failed Passed Expected strings to be the same, but they were different.
73 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
74 Core ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
75 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
76 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
77 Core ServerAdmin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
78 Core ServerAdmin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
79 Core ServerAdmin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
94 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
95 Desktop Admin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
96 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
97 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
98 Desktop Admin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
99 Desktop Admin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
100 Desktop Admin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
121 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
122 Desktop Delegate A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
123 Desktop Delegate An item that the account owns and whose owner may not change its permissions on a share.Clear-NTFSAccess -DisableInheritance should take ownership, clear and protect the DACL, and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.ClearAccess: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Clear.txt]'.
124 Desktop Delegate An item that the account owns and whose owner may not change its permissions on a share.Set-NTFSSecurityDescriptor should write the cleared DACL and report no RestoreOwnerError Failed Passed Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]' Expected $null or empty, but got 'RestoreOwnerError,NTFSSecurity.SetSecurityDescriptor: (5) Access is denied: [\\F1AFile2.a.forest1.net\NTFSSecurityLive\local-Desktop-20261009212312\Case10\Delegate\Owner\Descriptor.txt]'.
125 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
126 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
127 Desktop Delegate Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
128 Desktop Delegate InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
129 Desktop Delegate InheritedFrom of an item below a folder on a share whose permissions the account cannot read.Get-NTFSAccess should name an unknown parent for an inherited entry and no source for an explicit entry Failed Passed Expected strings to be the same, but they were different.
151 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for Select-Object -First 1 and change nothing else Failed Passed
152 Desktop ServerAdmin A later command that ends the pipeline or throws, for the item cmdlets on a share.With the messages of the verbose and debug streams.Set-NTFSSecurityDescriptor should stop at the verbose message for throw and change nothing else Failed Passed Expected like wildcard '*Downstream failure*' to match $null, but it did not match.
153 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should find a file whose name contains brackets by that name with -Filter Failed Passed Expected a collection with size 1, but got an empty collection.
154 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should reject a null -Filter Failed Passed Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of Expected an exception with message like '*'Filter'*' and FullyQualifiedErrorId 'ParameterArgumentValidationError,NTFSSecurity.GetChildItem2' to be thrown, but the message was 'Cannot process argument because the value of argument "pattern" is null. Change the value of argument "pattern" to a non-null value.' and the FullyQualifiedErrorId was 'ArgumentNull,NTFSSecurity.GetChildItem2'. from C:\NTFSSecurityLab\Tests\NTFSSecurity.Live.Tests.ps1:1333 char:11
155 Desktop ServerAdmin Get-ChildItem2 -Filter on a share folder.Should return every item for -Filter *.*, also the ones without a dot in their names Failed Passed Expected strings to be the same, but they were different.
156 Desktop ServerAdmin InheritedFrom of access entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.
157 Desktop ServerAdmin InheritedFrom of audit entries that Windows cannot resolve on a share.Should name an unknown parent for an inherited audit entry and no source for an explicit entry when the file is gone Failed Passed Expected strings to be the same, but they were different.

123
Tests/Lab/Acceptance-2026-10-09-quality-gate-paths.md

@ -31,10 +31,16 @@ is a control.
of its commit, packaged by `.github/scripts/New-ModulePackage.ps1`. Both
carry the label `5.0.0-rc7` and one assembly version, so every run used a
new process. All 11 files of each tested module folder equal the extracted
`NTFSSecurity.zip` byte for byte (SHA-256).
`NTFSSecurity.zip` byte for byte (SHA-256). The first packaging attempt, at
20:41 UTC, stopped in both builds at the check of the build script that
compares the package folder with the extracted zip ("The extracted ZIP
differs from the module folder"); the logs of that attempt are kept. I
changed the script (20:43) and built both again; the files that the lab
tested are those of the second attempt, and the cause of the first
mismatch wasn't recorded.
- Test source, identical in both runs (last written 20:56 and 20:54 UTC,
before the first run started): `NTFSSecurity.Live.Tests.ps1` (Git blob
`67b85efeb45af67070538f241c203c4afa38b6f4`) and
before the candidate run started at 21:02): `NTFSSecurity.Live.Tests.ps1`
(Git blob `67b85efeb45af67070538f241c203c4afa38b6f4`) and
`Invoke-NTFSSecurityLabTest.ps1` (blob
`0b46427bc32b0b15449e283a2a6cf67879937541`). Both are in the commit that
adds this record.
@ -54,7 +60,7 @@ Administrators own for the cases of `Set-NTFSOwner`.
| InheritedFrom of audit entries that Windows cannot resolve (ServerAdmin, Admin) | 2 | 2 | 0 | `2909a1c` |
| InheritedFrom of an item below a folder whose permissions the account cannot read (Delegate) | 2 | 1 | 0 | `2909a1c` |
| A later command that ends the pipeline or throws, for the item cmdlets (3 roles; 16 each) | 48 | 48 | 0 | `c77ecbf`, `40bf6a8` |
| A later command and the error of a folder that Get-ChildItem2 cannot read (Delegate) | 3 | 2 | 0 | `d44a200` |
| A later command and the error of a folder that Get-ChildItem2 cannot read (Delegate) | 3 | 2 | 0 | `c77ecbf` (break), `d44a200` (throw) |
| Get-ChildItem2 -Filter (3 roles; brackets, `*.*`, null) | 9 | 9 | 0 | `ee7c105`, `40bf6a8`, `ae3078f` |
| Privileges when a later command takes the debug messages (Delegate, Admin) | 6 | 4 | 0 | `d44a200` |
| State of the file server: only the first item changed (Server; one per role) | 3 | 3 | 0 | `c77ecbf`, `40bf6a8` |
@ -85,15 +91,18 @@ check covers.
`WindowsAccessControlLab`: F1ADC1, F1BDC1, F2DC1, F3DC1, F1AFile1 (client),
and F1AFile2 (file server), all Windows Server 2025 (10.0.26100). At
20:41 UTC, authenticated WinRM, LDAP RootDSE, Kerberos tickets, member secure
channels, and clocks (skew at most 7 s) passed on all six machines. No
`NTFSSecurityLive` OU or `NtfsLive*` account existed before the run. No VM,
operating system, or network changed, and no other session or controller
process used the lab.
channels, and clocks (skew at most 7 s) passed on all six machines. At 20:43
UTC, before the first test run, no `NTFSSecurityLive` OU or `NtfsLive*`
account existed. The runs changed no VM, operating system, or network
setting. A process listing at the start showed no other controller of these
tests on the host; it wasn't kept as a log.
Six checkpoints named `ntfs-qg-paths-83149ee-before-acceptance` were taken
at 20:45 to 20:46 UTC, one per machine. The policy of each machine is
Production, but Hyper-V reports the type Standard. As before, Production
classification is unverified, and no checkpoint was restored.
at 20:45 to 20:46 UTC, one per machine; the Hyper-V listing that shows the
names is kept with the evidence. The policy of each machine is Production,
but Hyper-V reports the type Standard. As before, Production classification
is unverified, and no checkpoint was restored or deleted. Every machine now
carries seven checkpoints of the acceptances since 2026-10-08, F1AFile1 eight.
## Live results
@ -117,27 +126,61 @@ the test that needs the module in the Server role, which doesn't import it.
Baseline, both editions: 338 passed, 148 failed, two skipped. Every role
exited 0 on the candidate. A joined verification of the result files (not of
the counts) found the same 488 tests in both builds, no duplicate, and every
one of the 148 baseline failures passed on the candidate. All 148 failures are
among the 156 tests of case 10 and the state test, which
[the results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv)
lists with both results and the first line of the baseline message.
one of the 148 baseline failures passed on the candidate. The 148 failures
are 74 tests in each edition, all among the 78 new tests of each edition
(case 10 and the state test); the four that pass on both builds are
preconditions. [The results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv)
lists the 156 results (78 tests in two editions) with both outcomes and the
first line of the baseline message.
What the baseline shows, from its messages:
- Owner: `RestoreOwnerError ... (5) Access is denied` for the unchanged owner.
- `InheritedFrom`: a text of 13 characters instead of the 14 of
`unknown parent`.
- Later command: the second item changed after `Select-Object -First 1`; the
`Downstream failure` of a `throw` never reached the caller; and a `break`
of a later command didn't leave the caller's loop.
- Later command: the `Downstream failure` of a `throw` never reached the
caller (the messages read `Expected like wildcard '*Downstream failure*' to
match $null`), and a `break` of a later command didn't leave the caller's
loop. For `Select-Object -First 1`, see the next section.
- `-Filter`: no result for a name with brackets; `*.*` returned only the
three names with a dot and dropped `NoExtension` and `NoExtensionFolder`;
`$null` gave `ArgumentNull` instead of the parameter validation error.
- Privileges: `TakeOwnership` still enabled after the pipeline stopped.
The 21 `Select-Object -First 1` tests per edition carry no message on the
baseline and no line in the Pester log. The State test shows independently
that the baseline changed the second item for each role.
### Baseline failures without a message
Seven tests of each role, 21 per edition and 42 in all, fail on the baseline
with an empty message, and Pester prints no line for them: `Select-Object
-First 1` for the five item cmdlets, the verbose stop of
`Set-NTFSSecurityDescriptor`, and the debug stop of `Set-NTFSOwner`. This lab
run doesn't show what the baseline did in them. The State test of the Server
role shows it only for `Remove-Item2`: in each role, the second item was
removed after `Select-Object -First 1`. That test stops at its first failed
assertion, so it says nothing about the other cmdlets, and its assertions for
the debug and verbose stops check only that the second item is as it was,
which is also true when the client test never ran.
To close the gap, the bodies of these tests ran afterwards on this host, in a
sandbox below TEMP, with the settings of the runner (Pester 5.7.1,
`ErrorActionPreference` Stop), one build in one edition per process
(`Acceptance\Probe-LaterCommand.ps1`; it isn't part of the acceptance, and
it didn't run on a share). The result is the same in Windows PowerShell 5.1
and PowerShell 7:
| Cmdlet | Baseline `f11ff41`, after `Select-Object -First 1` and after `throw` | Candidate `83149ee` |
| --- | --- | --- |
| `Remove-Item2` | both items removed | the second item stays |
| `Copy-Item2` | both items copied | only the first is copied |
| `Move-Item2` | both items moved | the second item stays |
| `Set-NTFSOwner` | both owners changed, also at the debug stop | the second owner stays Administrators |
| `Set-NTFSSecurityDescriptor` | both descriptors written | only the first is written |
All 12 tests of the probe (seven stop rows, five `throw` rows) fail on the
baseline, the seven stop rows with no error record, as in the lab, and the
`throw` rows with the message of the lab; all 12 pass on the candidate. At the
verbose stop of `Set-NTFSSecurityDescriptor`, neither build writes a
descriptor, because the stop comes before the first write, so that failure on
the baseline isn't a change of state.
## Cleanup and review
@ -152,6 +195,26 @@ F1AFile2 no share, no `C:\NTFSSecurityLive` or `C:\NTFSSecurityLab`, no
Assistance Operators, or Remote Management Users, and no profile of the ten
SIDs. No checkpoint was restored.
One independent, read-only, static review of the finished change (tests,
fixture, README, this record and its results file, and Decision 22) ran
before the first commit. The custom `security-reviewer` can't start because
its configured model is unavailable, so the built-in code-review agent did
it. Verdict: approve with Minor; no Blocker and no Major. It confirmed that
the new tests can't pass vacuously (every precondition is asserted, the data
rows are not empty, nothing is shared between rows), that the fixture stays
below the guarded folders and throws when Administrators don't own the
files, and that the counts, the hashes, the 156 results, and the cleanup
facts of this record match the evidence. Its findings, all corrected in the
commit that follows the first: the fix that this record credited for the
`break` row, the claims about the State test and the 42 messageless
failures (now the section above, with the diagnostic), this heading, the
count of results, the wording about the folders before the run, the
truncated messages in the results file, the README row of case 10, and the
migration hint of item 8 and the comparison with `Copy-Item` in Decision 22.
It could not run anything, so the run state and the lab-wide claims rest on
the logs; the diagnostic above and the checkpoint listing close two of its
open points.
## Limits
- `Get-NTFSAudit` below an unreadable parent folder can't be built here: an
@ -166,11 +229,23 @@ SIDs. No checkpoint was restored.
other gates. The stable version remains 4.2.6.
- The candidate and the baseline differ only by the 28 commits; the test and
controller files are the same.
- What the baseline did in the 42 failures without a message is shown by a
local diagnostic, not by this lab run. A State test split per cmdlet and
stop style would show it on the share too, and would need both lab runs
again.
## Evidence
The result files, logs, hashes, readiness, checkpoint, snapshot, and cleanup
logs of both runs are in the session artifact
`4b12e2f4-d4c7-4a5d-883a-ddb7421c4848\files\lab-qg-paths` (local, not in Git).
The per-test results of case 10 are in
[the results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv).
`4b12e2f4-d4c7-4a5d-883a-ddb7421c4848\files\lab-qg-paths` (local, not in Git);
so are the Hyper-V listing of the checkpoint names
(`checkpoints-83149ee-names.csv`) and the outputs of the diagnostic
(`runs\diagnostic-mute`, and `runs\diagnostic-mute-first-run` from before the
probe listed owners and entries). The per-test results of case 10 are in
[the results file](Acceptance-2026-10-09-quality-gate-paths-Results.csv). The
scripts that build, package, run, and clean up in this acceptance contain
paths of the session folder and stay in the session artifact; the generic
ones that it used, `Validate-LabResults.ps1` (the check of the result files)
and `Probe-LaterCommand.ps1` (the diagnostic), are in the folder
[Acceptance](Acceptance).

224
Tests/Lab/Acceptance/Probe-LaterCommand.ps1

@ -0,0 +1,224 @@
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Pester passes the data to the blocks of the container.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'The tests read the variables that BeforeAll sets.')]
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ModulePath,
[Parameter(Mandatory)] [string] $OutFile,
[string] $PesterPath = 'V:\Git\WindowsAccessControl\output\RequiredModules\Pester\5.7.1'
)
# Diagnostic of the acceptance in Acceptance-2026-10-09-quality-gate-paths.md, not part of it: why do the Select-Object rows of case 10
# fail on the base of the branch without a message? It runs the bodies of those tests (Assert-LabPipelineStop and
# Assert-LabDownstreamFailure of NTFSSecurity.Live.Tests.ps1) against files in a new folder below TEMP, with the settings of the
# runner (Pester 5.7.1, ErrorActionPreference Stop, detailed plain text), and lists for each test its result and error records, and
# the state of the items afterwards. One module build in one edition per process, never imported into another session; the script
# removes its own folder at the end after it has checked the path. Windows only. For example:
# powershell.exe -NoProfile -File Probe-LaterCommand.ps1 -ModulePath <folder with NTFSSecurity.psd1> -OutFile <result.txt>
$ErrorActionPreference = 'Stop'
Import-Module -Name (Join-Path -Path $PesterPath -ChildPath 'Pester.psd1') -Force
$root = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('mute-probe-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $root
$account = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$lines = New-Object -TypeName 'System.Collections.Generic.List[string]'
try {
$container = New-PesterContainer -ScriptBlock {
param ($ModulePath, $Root, $Account)
BeforeAll {
Import-Module -Name (Join-Path -Path $ModulePath -ChildPath 'NTFSSecurity.psd1') -Force -ErrorAction Stop
$everyone = 'S-1-1-0'
$administrators = 'S-1-5-32-544'
$privateData = (Get-Module -Name NTFSSecurity).PrivateData
$privateData['EnablePrivileges'] = $false
$account = $Account
function Get-ProbeOwner {
param ([string] $Path)
(Get-Acl -LiteralPath $Path).GetOwner([System.Security.Principal.SecurityIdentifier]).Value
}
function New-ProbeFolder {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$path = Join-Path -Path $Root -ChildPath $Name
$null = New-Item -ItemType Directory -Path $path -Force
$path
}
function New-ProbePair {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Helper that writes only to the folder of this run.'
)]
param ([string] $Name)
$directory = New-ProbeFolder -Name $Name
foreach ($item in 'First', 'Second') {
Set-Content -LiteralPath (Join-Path -Path $directory -ChildPath "$item.txt") -Value $item -NoNewline
}
@{ Directory = $directory; First = (Join-Path -Path $directory -ChildPath 'First.txt'); Second = (Join-Path -Path $directory -ChildPath 'Second.txt') }
}
$cases = @{
'Remove-Item2' = @{
Prepare = { param ($Slug) New-ProbePair -Name "RemoveItem2-$Slug" }
Run = { param ($Context) Remove-Item2 -Path $Context.First, $Context.Second -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Copy-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "CopyItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "CopyItem2-$Slug-To"; $c }
Run = { param ($Context) Copy-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (Test-Path -LiteralPath (Join-Path -Path $Context.Destination -ChildPath 'Second.txt')) }
}
'Move-Item2' = @{
Prepare = { param ($Slug) $c = New-ProbePair -Name "MoveItem2-$Slug"; $c.Destination = New-ProbeFolder -Name "MoveItem2-$Slug-To"; $c }
Run = { param ($Context) Move-Item2 -Path $Context.First, $Context.Second -Destination $Context.Destination -PassThru $true -ErrorAction SilentlyContinue }
Untouched = { param ($Context) Test-Path -LiteralPath $Context.Second }
}
'Set-NTFSOwner' = @{
Prepare = { param ($Slug) New-ProbePair -Name "SetOwner-$Slug" }
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) (Get-ProbeOwner -Path $Context.Second) -eq $administrators }
}
'Set-NTFSSecurityDescriptor' = @{
Prepare = {
param ($Slug)
$c = New-ProbePair -Name "SetDescriptor-$Slug"
$c.Descriptors = @(Get-NTFSSecurityDescriptor -Path $c.First, $c.Second -ErrorAction Stop)
Add-NTFSAccess -SecurityDescriptor $c.Descriptors -Account $everyone -AccessRights ReadData -ErrorAction Stop
$c
}
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -PassThru -ErrorAction SilentlyContinue }
Untouched = { param ($Context) -not (@((Get-Acl -LiteralPath $Context.Second).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq $everyone }).Count) }
}
}
$streamCases = @{
'Set-NTFSSecurityDescriptor/verbose' = @{
Prepare = $cases['Set-NTFSSecurityDescriptor'].Prepare
Run = { param ($Context) Set-NTFSSecurityDescriptor -SecurityDescriptor $Context.Descriptors -Verbose -ErrorAction SilentlyContinue 4>&1 }
Untouched = $cases['Set-NTFSSecurityDescriptor'].Untouched
RecordType = [System.Management.Automation.VerboseRecord]
}
'Set-NTFSOwner/debug' = @{
Prepare = $cases['Set-NTFSOwner'].Prepare
Run = { param ($Context) Set-NTFSOwner -Path $Context.First, $Context.Second -Account $account -ErrorAction SilentlyContinue 5>&1 }
Untouched = $cases['Set-NTFSOwner'].Untouched
RecordType = [System.Management.Automation.DebugRecord]
}
}
function Assert-ProbePipelineStop {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$Error.Clear()
$result = @(& $Case.Run $context | Select-Object -First 1)
$result | Should -HaveCount 1
if ($Case.RecordType) {
$result[0] | Should -BeOfType $Case.RecordType
}
$Error.Count | Should -Be 0
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
function Assert-ProbeDownstreamFailure {
param ([hashtable] $Case, [string] $Slug, [string] $Stream)
if ($Stream -eq 'debug') { $DebugPreference = 'Continue' }
$context = & $Case.Prepare $Slug
$emitted = 0
$caught = $null
$Error.Clear()
try {
& $Case.Run $context | ForEach-Object -Process {
$emitted++
throw 'Downstream failure'
}
}
catch {
$caught = $_
}
$caught.Exception.Message | Should -BeLike '*Downstream failure*'
$emitted | Should -Be 1
@($Error | Where-Object -FilterScript { $_.Exception.Message -notlike '*Downstream failure*' }) | Should -BeNullOrEmpty
if ($Case.Untouched) {
(& $Case.Untouched $context) | Should -BeTrue
}
}
}
Describe 'Mirror of the later-command tests' {
It '<Name> should stop after the first object for Select-Object -First 1' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbePipelineStop -Case $cases[$Name] -Slug 'Select'
}
It '<Name> should stop after the first object for throw' -ForEach @(
@{ Name = 'Remove-Item2' }, @{ Name = 'Copy-Item2' }, @{ Name = 'Move-Item2' }, @{ Name = 'Set-NTFSOwner' }, @{ Name = 'Set-NTFSSecurityDescriptor' }
) {
Assert-ProbeDownstreamFailure -Case $cases[$Name] -Slug 'Throw'
}
It '<Key> should stop at the message for Select-Object -First 1' -ForEach @(
@{ Key = 'Set-NTFSSecurityDescriptor/verbose'; Stream = 'verbose' }, @{ Key = 'Set-NTFSOwner/debug'; Stream = 'debug' }
) {
Assert-ProbePipelineStop -Case $streamCases[$Key] -Slug ('{0}Select' -f $Stream) -Stream $Stream
}
}
} -Data @{ ModulePath = $ModulePath; Root = $root; Account = $account }
$configuration = New-PesterConfiguration
$configuration.Run.Container = $container
$configuration.Run.PassThru = $true
$configuration.Output.Verbosity = 'Detailed'
$configuration.Output.RenderMode = 'Plaintext'
$lines.Add(('Edition {0} {1}; module {2}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion, $ModulePath))
$lines.Add('--- Pester output')
$output = & { Invoke-Pester -Configuration $configuration } *>&1
$result = @($output | Where-Object -FilterScript { $_ -is [Pester.Run] }) | Select-Object -First 1
foreach ($entry in @($output | Where-Object -FilterScript { $_ -isnot [Pester.Run] })) { $lines.Add('{0}' -f $entry) }
$lines.Add('--- Results')
foreach ($test in $result.Tests) {
$messages = @(@($test.ErrorRecord) | Where-Object -FilterScript { $_ } | ForEach-Object -Process { ($_.ToString() -split '\r?\n')[0] })
$lines.Add(('{0} | {1} | error records: {2} | {3}' -f $test.Result, $test.ExpandedName, @($test.ErrorRecord).Count, ($messages -join ' // ')))
}
$lines.Add(('Totals: passed {0}, failed {1}, not run {2}; result {3}' -f $result.PassedCount, $result.FailedCount, $result.NotRunCount, $result.Result))
$lines.Add('--- State of the items after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Sort-Object -Property Name) {
$files = @(Get-ChildItem -LiteralPath $folder.FullName -File | ForEach-Object -Process { $_.Name })
$lines.Add(('{0}: {1}' -f $folder.Name, ($files -join ', ')))
}
$lines.Add('--- Owner (SetOwner folders) and explicit entry for Everyone (SetDescriptor folders) after the run')
foreach ($folder in Get-ChildItem -LiteralPath $root -Directory | Where-Object -FilterScript { $_.Name -like 'SetOwner-*' -or $_.Name -like 'SetDescriptor-*' } | Sort-Object -Property Name) {
foreach ($name in 'First.txt', 'Second.txt') {
$path = Join-Path -Path $folder.FullName -ChildPath $name
$acl = Get-Acl -LiteralPath $path
if ($folder.Name -like 'SetOwner-*') {
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
$lines.Add(('{0}\{1}: owner {2}' -f $folder.Name, $name, $(if ($owner -eq 'S-1-5-32-544') { 'Administrators (as created)' } elseif ($owner -eq $account) { 'the account of the run (changed)' } else { $owner })))
}
else {
$entries = @($acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object -FilterScript { $_.IdentityReference.Value -eq 'S-1-1-0' })
$lines.Add(('{0}\{1}: explicit entry for Everyone: {2}' -f $folder.Name, $name, $(if ($entries.Count) { 'yes (changed)' } else { 'no (as created)' })))
}
}
}
}
finally {
$full = [System.IO.Path]::GetFullPath($root)
if ($full.StartsWith([System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()), [System.StringComparison]::OrdinalIgnoreCase) -and (Split-Path -Path $full -Leaf) -like 'mute-probe-*') {
Remove-Item -LiteralPath $full -Recurse -Force -ErrorAction SilentlyContinue
}
Set-Content -LiteralPath $OutFile -Value $lines -Encoding utf8
}

111
Tests/Lab/Acceptance/Test-PublishedRelease.ps1

@ -0,0 +1,111 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [ValidatePattern('^\d+\.\d+\.\d+(-[0-9A-Za-z]+)?$')] [string] $Version,
[Parameter(Mandatory)] [string] $OutputPath,
[string] $Repository = 'raandree/NTFSSecurity'
)
# Read-only identity check of a published NTFSSecurity version (acceptance of a published candidate): the tag, its commit on master, the CI run of the
# tag, the GitHub release asset, and the PowerShell Gallery package. It downloads the nupkg and the zip into OutputPath, checks the
# SHA-512 that the Gallery publishes (ordinal, case-sensitive base64), extracts both with System.IO.Compression, and compares the
# module files byte for byte. It writes Identity.json and prints a table; it changes nothing on GitHub or in the Gallery, and
# it never imports the module. Exit code 1 for any mismatch.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Add-Type -AssemblyName System.IO.Compression.FileSystem
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
$headers = @{ 'User-Agent' = 'ntfssecurity-published-identity-check'; Accept = 'application/vnd.github+json' }
$api = "https://api.github.com/repos/$Repository"
$problems = New-Object -TypeName 'System.Collections.Generic.List[string]'
$result = [ordered]@{ Version = $Version; CheckedUtc = [DateTime]::UtcNow.ToString('o') }
# 1. The tag and its commit
$ref = Invoke-RestMethod -Uri "$api/git/ref/tags/$Version" -Headers $headers
$sha = $ref.object.sha
if ($ref.object.type -eq 'tag') { $sha = (Invoke-RestMethod -Uri "$api/git/tags/$sha" -Headers $headers).object.sha }
$result.TagCommit = $sha
$compare = Invoke-RestMethod -Uri "$api/compare/master...$sha" -Headers $headers
$result.CommitOnMaster = ($compare.status -in 'identical', 'behind')
$result.CompareStatus = $compare.status
if (-not $result.CommitOnMaster) { $problems.Add("The commit $sha of the tag isn't on master (compare status: $($compare.status)).") }
# 2. The CI run of the tag: the tag push has the tag as its branch name
$runs = @((Invoke-RestMethod -Uri "$api/actions/runs?head_sha=$sha&per_page=30" -Headers $headers).workflow_runs | Where-Object -FilterScript { $_.event -eq 'push' -and $_.head_branch -eq $Version })
if ($runs.Count -eq 0) { $problems.Add("No CI run of the tag push for $Version.") }
$jobs = @()
foreach ($run in ($runs | Sort-Object -Property run_number)) {
$jobs += @((Invoke-RestMethod -Uri "$api/actions/runs/$($run.id)/jobs?per_page=50" -Headers $headers).jobs | ForEach-Object -Process {
[pscustomobject]@{ Run = $run.id; Attempt = $run.run_attempt; Job = $_.name; Status = $_.status; Conclusion = $_.conclusion }
})
}
$result.CiJobs = $jobs
$latestRelease = @($jobs | Where-Object -FilterScript { $_.Job -match 'Release' } | Sort-Object -Property Attempt | Select-Object -Last 1)
if ($latestRelease.Count -eq 0 -or $latestRelease[0].Conclusion -ne 'success') { $problems.Add('The latest Release job of the tag did not succeed.') }
# 3. The GitHub release and its zip
$release = Invoke-RestMethod -Uri "$api/releases/tags/$Version" -Headers $headers
$asset = @($release.assets | Where-Object -FilterScript { $_.name -eq 'NTFSSecurity.zip' }) | Select-Object -First 1
if (-not $asset) { throw "The release $Version has no NTFSSecurity.zip." }
$zipPath = Join-Path -Path $OutputPath -ChildPath "NTFSSecurity-$Version.zip"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing
$zipSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $zipPath).Hash
$result.Release = [ordered]@{ Prerelease = $release.prerelease; Published = $release.published_at; AssetSize = $asset.size; AssetDigest = $asset.digest; ZipSha256 = $zipSha256 }
if ($asset.digest -and $asset.digest -like 'sha256:*' -and ($asset.digest.Substring(7) -ne $zipSha256.ToLowerInvariant())) { $problems.Add('The SHA-256 of the downloaded zip differs from the digest of the release asset.') }
# 4. The PowerShell Gallery package; the published hash is base64 of SHA-512
$entry = Invoke-RestMethod -Uri ("https://www.powershellgallery.com/api/v2/Packages(Id='NTFSSecurity',Version='{0}')" -f $Version)
$published = $entry.entry.properties.PackageHash.'#text'
if (-not $published) { $published = [string] $entry.entry.properties.PackageHash }
$algorithm = $entry.entry.properties.PackageHashAlgorithm
if (-not $published -or $algorithm -ne 'SHA512') { throw "The Gallery has no SHA512 hash for NTFSSecurity $Version (algorithm '$algorithm')." }
$nupkgPath = Join-Path -Path $OutputPath -ChildPath "NTFSSecurity.$Version.nupkg"
Invoke-WebRequest -Uri "https://www.powershellgallery.com/api/v2/package/NTFSSecurity/$Version" -OutFile $nupkgPath -UseBasicParsing
$sha512 = [System.Security.Cryptography.SHA512]::Create()
$stream = [System.IO.File]::OpenRead($nupkgPath)
try { $actual = [Convert]::ToBase64String($sha512.ComputeHash($stream)) } finally { $stream.Dispose(); $sha512.Dispose() }
$hashMatches = [string]::Equals($actual, $published, [StringComparison]::Ordinal)
$result.Gallery = [ordered]@{ Published = $entry.entry.properties.Published.'#text'; IsPrerelease = $entry.entry.properties.IsPrerelease.'#text'; PackageHashAlgorithm = $algorithm; PackageHash = $published; DownloadedSha512 = $actual; HashMatches = $hashMatches; NupkgSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $nupkgPath).Hash }
if (-not $hashMatches) { $problems.Add('The downloaded nupkg does not have the SHA-512 that the Gallery publishes.') }
# 5. The module files of both packages
$nupkgFolder = Join-Path -Path $OutputPath -ChildPath "nupkg-$Version"
$zipFolder = Join-Path -Path $OutputPath -ChildPath "zip-$Version"
foreach ($folder in $nupkgFolder, $zipFolder) { if (Test-Path -LiteralPath $folder) { Remove-Item -LiteralPath $folder -Recurse -Force } }
[System.IO.Compression.ZipFile]::ExtractToDirectory($nupkgPath, $nupkgFolder)
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $zipFolder)
function Get-ModuleRoot { param ([string] $Folder) (Get-ChildItem -LiteralPath $Folder -Filter 'NTFSSecurity.psd1' -Recurse -File | Select-Object -First 1).DirectoryName }
$nupkgRoot = Get-ModuleRoot -Folder $nupkgFolder
$zipRoot = Get-ModuleRoot -Folder $zipFolder
$files = foreach ($file in Get-ChildItem -LiteralPath $zipRoot -Recurse -File) {
$relative = $file.FullName.Substring($zipRoot.Length).TrimStart('\')
$other = Join-Path -Path $nupkgRoot -ChildPath $relative
$zipHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
$nupkgHash = if (Test-Path -LiteralPath $other) { (Get-FileHash -Algorithm SHA256 -LiteralPath $other).Hash } else { '' }
[pscustomobject]@{ File = $relative; ZipSha256 = $zipHash; NupkgSha256 = $nupkgHash; Equal = ($zipHash -eq $nupkgHash) }
}
$files | Export-Csv -LiteralPath (Join-Path -Path $OutputPath -ChildPath "ModuleFiles-$Version.csv") -NoTypeInformation -Encoding utf8
$result.ModuleFiles = @($files).Count
$result.ModuleFilesEqual = (@($files | Where-Object -FilterScript { -not $_.Equal }).Count -eq 0)
$result.ModuleDllSha256 = ($files | Where-Object -FilterScript { $_.File -eq 'NTFSSecurity.dll' }).ZipSha256
if (-not $result.ModuleFilesEqual) { $problems.Add('The module files of the nupkg and of the zip differ.') }
# 6. The identity that the manifest claims
$manifest = Import-PowerShellDataFile -LiteralPath (Join-Path -Path $zipRoot -ChildPath 'NTFSSecurity.psd1')
$label = $manifest.PrivateData.PSData.Prerelease
$claimed = if ($label) { '{0}-{1}' -f $manifest.ModuleVersion, $label } else { [string] $manifest.ModuleVersion }
$result.ManifestVersion = $claimed
if ($claimed -ne $Version) { $problems.Add("The manifest says $claimed, not $Version.") }
$result.Problems = @($problems)
$result.Verified = ($problems.Count -eq 0)
$result | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path -Path $OutputPath -ChildPath "Identity-$Version.json") -Encoding utf8
'Version {0}: tag commit {1}; on master: {2} ({3})' -f $Version, $sha, $result.CommitOnMaster, $compare.status
$jobs | Format-Table -AutoSize | Out-String -Width 200
'GitHub zip SHA-256 {0}' -f $zipSha256
'Gallery SHA-512 matches: {0}; nupkg SHA-256 {1}' -f $hashMatches, $result.Gallery.NupkgSha256
'Module files: {0}; equal in nupkg and zip: {1}; NTFSSecurity.dll SHA-256 {2}' -f $result.ModuleFiles, $result.ModuleFilesEqual, $result.ModuleDllSha256
'Manifest identity: {0}' -f $claimed
if ($problems.Count -gt 0) { $problems | ForEach-Object -Process { 'PROBLEM: ' + $_ }; 'PUBLISHED_IDENTITY_NOT_VERIFIED'; exit 1 }
'PUBLISHED_IDENTITY_VERIFIED'

68
Tests/Lab/Acceptance/Validate-LabResults.ps1

@ -0,0 +1,68 @@
[CmdletBinding()]
param (
[Parameter(Mandatory)] [string] $ResultsFolder,
[Parameter(Mandatory)] [string] $OutputPrefix,
[string[]] $Edition = @('Desktop', 'Core'),
[ValidateSet('Candidate', 'Baseline')] [string] $Expect = 'Candidate'
)
# Validates one controller result folder: every edition and role has exactly one result, and for a candidate no test failed
# and every exit code is 0. It writes the counts per role, every test with its result (from the result files of the roles,
# not from the counts), and the failures with their full names and messages. A Desktop ConvertFrom-Json wraps an array in
# one object, so each JSON array is enumerated explicitly. -File passes an array as one string.
$ErrorActionPreference = 'Stop'
$Edition = @($Edition | ForEach-Object -Process { $_ -split ',' } | Where-Object -FilterScript { $_ })
$summary = @(Get-Content -LiteralPath (Join-Path -Path $ResultsFolder -ChildPath 'Summary.json') -Raw | ConvertFrom-Json | ForEach-Object -Process { $_ })
$roles = 'Delegate', 'ServerAdmin', 'Admin', 'Server'
$expected = @(foreach ($name in $Edition) { foreach ($role in $roles) { '{0}:{1}' -f $name, $role } })
$actual = @($summary | ForEach-Object -Process { '{0}:{1}' -f $_.Edition, $_.Role })
$problems = New-Object -TypeName 'System.Collections.Generic.List[string]'
foreach ($identity in $expected) {
$count = @($actual | Where-Object -FilterScript { $_ -eq $identity }).Count
if ($count -ne 1) { $problems.Add("$identity has $count results instead of 1") }
}
if ($summary.Count -ne $expected.Count) { $problems.Add("The summary has $($summary.Count) results instead of $($expected.Count)") }
$counts = foreach ($entry in $summary) {
[pscustomobject]@{
Version = $entry.Version; Edition = $entry.Edition; Role = $entry.Role; Account = $entry.Account; ExitCode = $entry.ExitCode
Passed = $entry.Passed; Failed = $entry.Failed; Skipped = $entry.Skipped
}
}
$tests = New-Object -TypeName 'System.Collections.Generic.List[object]'
foreach ($file in Get-ChildItem -LiteralPath $ResultsFolder -Filter '*.result.json') {
$baseName = $file.Name -replace '\.result\.json$', ''
$role = ($baseName -split '-')[-1]
$resultEdition = if ($baseName -match '-(Desktop|Core)-') { $Matches[1] } else { '' }
foreach ($case in @(Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json | ForEach-Object -Process { $_ })) {
$tests.Add([pscustomobject]@{ Edition = $resultEdition; Role = $role; Test = $case.Name; Result = $case.Result; Message = (($case.Message -split '\r?\n')[0]) })
}
}
$failures = @($tests | Where-Object -FilterScript { $_.Result -eq 'Failed' })
if ($Expect -eq 'Candidate') {
foreach ($row in $counts) {
if ($row.ExitCode -ne 0 -or $row.Failed -ne 0 -or $row.Passed -eq 0) { $problems.Add("$($row.Edition) $($row.Role): exit code $($row.ExitCode), $($row.Passed) passed, $($row.Failed) failed") }
}
if ($failures.Count -gt 0) { $problems.Add("$($failures.Count) failed tests in the result files") }
}
$counts | Export-Csv -LiteralPath ($OutputPrefix + '-counts.csv') -NoTypeInformation -Encoding utf8
$tests | Export-Csv -LiteralPath ($OutputPrefix + '-tests.csv') -NoTypeInformation -Encoding utf8
$failures | Export-Csv -LiteralPath ($OutputPrefix + '-failures.csv') -NoTypeInformation -Encoding utf8
foreach ($editionName in $Edition) {
$selected = @($counts | Where-Object -FilterScript { $_.Edition -eq $editionName })
'{0}: passed={1}, failed={2}, skipped={3}' -f $editionName, ($selected.Passed | Measure-Object -Sum).Sum, ($selected.Failed | Measure-Object -Sum).Sum, ($selected.Skipped | Measure-Object -Sum).Sum
}
$counts | Format-Table -AutoSize | Out-String -Width 200
'tests in the result files: {0}; failed: {1}; skipped: {2}' -f $tests.Count, $failures.Count, @($tests | Where-Object -FilterScript { $_.Result -eq 'Skipped' }).Count
if ($problems.Count -gt 0) {
$problems | ForEach-Object -Process { 'PROBLEM: ' + $_ }
'LIVE_RESULT_NOT_ACCEPTED'
exit 1
}
'LIVE_RESULT_VERIFIED ({0})' -f $Expect

2
Tests/Lab/README.md

@ -22,7 +22,7 @@ without a lab they skip every test.
| 9 | Delegate, Admin | `Get-NTFSSimpleAccess` compares a share folder with its parent. For the accounts of another domain and of other forests, `Get-NTFSAccess` returns their names, `Get-NTFSOrphanedAccess` doesn't report them, `Add-NTFSAccess` and `Remove-NTFSAccess` find them by name, and `Get-NTFSEffectiveAccess -ServerName` returns the rights that the file server's own token of each account gets. |
| Long paths | Admin | `Get-ChildItem2` and `Get-NTFSAccess` with a share path longer than 260 characters. |
| [#108][issue-108] | Admin | `Copy-Item2` and `Move-Item2` with `-WhatIf` onto an existing file on the share write no error. |
| 10 | Delegate, ServerAdmin, Admin | The behavior that the quality-gate fixes before 5.0.0 changed, and that the lab can observe. The delegated account, which owns the items it creates, clears and protects the DACL of an item whose OWNER RIGHTS entry denies it the right to change the DACL, and the cmdlets report no `RestoreOwnerError` for the unchanged owner. `InheritedFrom` names an `unknown parent` for entries that Windows can't resolve, for a deleted file and below a folder whose permissions the account can't read, and no source for an explicit entry. A later command that stops the pipeline with `Select-Object -First 1` or throws leaves the second item of `Remove-Item2`, `Copy-Item2`, `Move-Item2`, `Set-NTFSOwner`, and `Set-NTFSSecurityDescriptor` as it was, also at a verbose message, and `Get-ChildItem2` passes on what a later command throws for the error of a folder it can't read. `Get-ChildItem2 -Filter` finds a name with brackets and returns every item for `*.*`. The privileges that the cmdlets enable are disabled again when a later command stops the pipeline or throws at a debug message. |
| 10 | Delegate, ServerAdmin, Admin | The behavior that the quality-gate fixes before 5.0.0 changed, and that the lab can observe. The delegated account, which owns the items it creates, clears and protects the DACL of an item whose OWNER RIGHTS entry denies it the right to change the DACL, and the cmdlets report no `RestoreOwnerError` for the unchanged owner. `InheritedFrom` names an `unknown parent` for entries that Windows can't resolve, for a deleted file and below a folder whose permissions the account can't read, and no source for an explicit entry. A later command that stops the pipeline with `Select-Object -First 1` or throws leaves the second item of `Remove-Item2`, `Copy-Item2`, `Move-Item2`, `Set-NTFSOwner`, and `Set-NTFSSecurityDescriptor` as it was, also when it takes the verbose messages of `Set-NTFSSecurityDescriptor` or the debug messages of `Set-NTFSOwner`, and `Get-FileHash2` writes no error when it takes its verbose messages. `Get-ChildItem2` passes on what a later command throws for the error of a folder it can't read, and a `break` of that command leaves the caller's loop. `Get-ChildItem2 -Filter` finds a name with brackets, returns every item for `*.*`, and rejects `$null`. The privileges that the cmdlets enable are disabled again when a later command stops the pipeline or throws at a debug message. |
| State | Server | After the runs on the client, the file server checks the owners, the audit entries, the items, the links, the entries of the foreign accounts, and which items a later command changed, itself, without the module. |
Case 1 uses two kinds of folders. Before 5.0.0-rc3, the cmdlets wrote back the

Loading…
Cancel
Save