diff --git a/CHANGELOG.md b/CHANGELOG.md index 97df854..271aaee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ Following the `0.0.15-beta` prerelease, Windows client/server, IIS, Exchange, pr ### Changed +- Complete issue #28's public-command directory-identity regressions for empty and populated replacements, retained-handle prevention, defensive identity refusal, deepest-first pruning, preservation boundaries, WhatIf and terminating errors. The fixture note distinguishes reachable production behavior from injected handle-loss tests; executable runtime behavior and lab gates are unchanged. + - Complete the bounded issue #30 retention contract with deterministic acquisition-barrier fixtures for both temp commands. Cover recent/old replacements, renamed paths, directory/junction substitution, same-object relinking, current length and timestamp observations, writer failures and handle recovery. Document that attribute-only timestamp updates are not excluded by read-only sharing and retention validation is not atomic with deletion; runtime behavior and deferred acceptance gates remain unchanged. -- Implement the issue #26 Zensical migration in PR #41: replace the active MkDocs/Material configuration and direct dependencies with pinned Zensical 0.0.62, preserve the classic navigation and generated command references, and retain the single-build manifest-to-`gh-pages` byte-verification controls. Review, merge, deployment, and lowercase-path acceptance remain open. +- Complete the issue #26 Zensical migration in merged PR #41: replace the active MkDocs/Material configuration and direct dependencies with pinned Zensical 0.0.62, preserve the classic navigation and generated command references, and retain the single-build manifest-to-`gh-pages` byte-verification controls. Exact merged-source deployment and live canonical bytes are verified in the release ledger. The merged account-root companion supplies a JavaScript fallback for lowercase paths from its 404 page; it is not an HTTP redirect. ## 0.0.15-beta - 2026-09-17 @@ -32,7 +34,7 @@ Version `0.0.15-beta` was published from exact source `345f06c861b6d4074e5896e0b ### Changed - Prioritize non-lab documentation/delivery readiness and an optional approved prerelease; Windows, IIS, Exchange, and profile lab validation is deferred future work, not underway. Require succinct Completed / Remaining summaries in every successor prompt. -- Use the working title-case `https://day3bits.com/TheCleaners/` URL consistently in configuration, manifest, source help, and documentation. Track support for both URL cases in Zensical issue #26. +- Use the working title-case `https://day3bits.com/TheCleaners/` URL consistently in configuration, manifest, source help, and documentation. At this beta checkpoint, support for both URL cases was tracked in Zensical issue #26; the later completion is recorded in Unreleased above. - Configure the GitHub `powershell-gallery` environment with SamErde review, disabled administrator bypass and `v*` tag restrictions. Update the publishing workflow to use the dedicated `PSGALLERY_PUBLISH_API_KEY` environment secret, fail clearly when absent, and stop passing repository secrets to the build matrix. The protected `0.0.15-beta` run exercised the environment approval and credential through the exact-artifact publisher. - Add a closed local-rehearsal publisher mode that uses an existing local filesystem repository and an internal placeholder key. Preserve the exact-artifact guards shared with the fixed PSGallery production path and require an explicit valid source commit in both modes. - Bind publication to the maintainer-approved archive digest and verify the published version's installed payload, metadata, import, help, aliases and preview locks on fresh supported-runtime runners. @@ -70,4 +72,4 @@ Protected publication run [35268852573](https://github.com/SamErde/TheCleaners/a ### Still pending -Windows client/server, broader elevated/non-elevated, real-system-root, ReFS and adversarial/concurrency acceptance; IIS and Exchange product/build preview labs; and Windows profile-inventory acceptance are deferred future work, not underway. Final 1.0 release acceptance remains open. The bounded baseline uncovered-command review merged in PR #35; its remaining native/platform/product cases remain lab gates. The published beta does not close them. The Zensical framework migration is open in PR #41 under issue #26; review, merge, deployment, and the account-root lowercase redirect remain open. +Windows client/server, broader elevated/non-elevated, real-system-root, ReFS and adversarial/concurrency acceptance; IIS and Exchange product/build preview labs; and Windows profile-inventory acceptance are deferred future work, not underway. Final 1.0 release acceptance remains open. The bounded baseline uncovered-command review merged in PR #35; its remaining native/platform/product cases remain lab gates. The published beta does not close them. The Zensical migration and account-root compatibility work were still pending at this beta checkpoint; their subsequent completion in PR #41 and the account-root companion is recorded in Unreleased above and the current release ledger. diff --git a/docs/issue-28-directory-identity.md b/docs/issue-28-directory-identity.md new file mode 100644 index 0000000..914974b --- /dev/null +++ b/docs/issue-28-directory-identity.md @@ -0,0 +1,47 @@ +# Issue #28 directory-identity closure + +## Scope and result + +This packet closes the remaining deterministic fixture gap in [issue #28](https://github.com/SamErde/TheCleaners/issues/28). The runtime already retains native Windows directory handles through candidate mutation and compares volume/file identity before non-recursive pruning. No runtime defect was found, so this packet adds a dedicated public-command regression suite without changing production behavior. + +The initial implementation checkpoint used base commit `af330858b41335366476f0b845e3f73a3c1497d1` plus uncommitted test/documentation changes. It is retained below as historical evidence. The clean metadata-correction checkpoint records local committed fixture validation; final-head hosted CI across all supported runtimes and post-merge evidence remain separate checks. + +## Deterministic fixture design + +Every test redirects the current-user and Windows temp roots to a unique Pester fixture. `Clear-WindowsTemp` uses a controlled root resolver mock; the original environment is restored after each case. No cleaner targets actual user, Windows, IIS, Exchange, or other product data. + +The replacement regressions distinguish two safety layers: + +1. The production-reachable test deletes the old candidate, then attempts to move the touched directory aside and install a staged empty or populated replacement at the same path. The fixture requires the first move to throw native sharing violation 32; only a successful first move permits the staged replacement attempt. The retained DELETE-capable handle denies the move, so the replacement cannot occur while the plan remains active. The staged directory and any new file remain intact outside the cleanup root. +2. The defensive identity test uses an explicitly labeled isolated fault injection at the same pre-prune boundary. It releases only the touched directory's retained handle, moves the now-empty original to a displaced path, and installs a real empty or populated directory at the original path. The displaced original stays alive to prevent file-ID reuse. Before reading the replacement identity, the fixture sets its creation time, last-write time, and attributes to the values captured for the planned original and asserts those mutable fields match. The command still observes that the native volume/file identity differs, skips the replacement and its ancestor, and reports no directory-removal failure. + +The second case demonstrates defense in depth when the primary handle barrier is unavailable. It is not a claim that production planning normally releases that handle or permits the physical replacement. + +## Acceptance mapping + +| Issue #28 criterion | Deterministic evidence | +| --- | --- | +| Preserve a touched directory removed and recreated at the same path before pruning | For both public commands, the intact production plan blocks the replacement attempt. The isolated fallback fixture then performs the replacement, verifies different native identities, and preserves the replacement and its ancestor. | +| Verify both temp commands | Every case is parameterized across `Clear-CurrentUserTemp` and `Clear-WindowsTemp`. | +| Cover recreated directories with and without new contents | The fallback fixture installs one empty replacement and one replacement containing `new.tmp` for each command; all four replacements remain. The primary handle test also stages and attempts both payloads. | +| Preserve normal deepest-first pruning | Each command removes an old file, its child directory, and its parent in that order-sensitive plan, with two directory candidates and two removals. | +| Preserve root, unrelated empty branches, recent files, and reparse points | The end-to-end boundary case retains the fixture root, an unrelated empty directory, a recent-file branch, a junction, and the junction target outside the cleanup root. | +| Avoid path/timestamp-only identity proof | Tests use `FILE_ID_INFO` identities from the native interop. The fallback fixture explicitly matches creation time, last-write time, and attributes, then records unequal original and replacement identities while confirming the displaced original retains its planned identity. | +| Keep `ShouldProcess` and error behavior | `-WhatIf` reports candidates and performs no mutation; mutation cases use explicit `-Confirm:$false`; a locked candidate with `-ErrorAction Stop` terminates with `TempFileRemovalFailed` and preserves directory ancestry. | +| Validate the supported runtime matrix | Local coverage passed on PowerShell 7.6.6 and Windows PowerShell 5.1.26100.9444 with Pester 5.7.1. Full-matrix acceptance remains pending inspection of exact final-head hosted results, including PowerShell 7.4.20 and 7.5.11. | + +## Validation evidence + +Local fixture runs used Windows **10.0.26200.0**, PowerShell **7.6.6** and Windows PowerShell **5.1.26100.9444**, with pinned Pester **5.7.1**. Native interop was initialized before Pester under Windows PowerShell 5.1, matching the hosted workflow's runspace requirement. Each result below belongs only to its stated checkpoint; an older run does not validate a later correction. + +| Checkpoint | Result on each local runtime | Scope | +| --- | --- | --- | +| Initial base `af330858b41335366476f0b845e3f73a3c1497d1` plus uncommitted implementation | 14 passed, zero failed/skipped/not-run | Historical initial fixture suite. Reports: `%TEMP%\TheCleaners-issue28-evidence\pester-ps766.xml` and `pester-ps51.xml`. | +| Clean integration `171b4cc558b875f72ffedd68aa051d6aeac6e43e` | 18/18 passed, zero failed/skipped/not-run | Original 14 fixtures plus four documentation contracts; predates the metadata-matching refinement. Strict Zensical 0.0.62 and PR-range whitespace checks also passed. | +| Review base `581907e7d83c283de423c0f36d02b735c7153876` plus uncommitted refinement | 14 passed, zero failed/skipped/not-run | Historical first run with matching replacement metadata; reports `pester-review-followup-ps766.xml` and `pester-review-followup-ps51.xml` in the same temporary evidence directory. | +| Clean correction `2372a218d04354df100c2d9188bc30c419764151` | 18/18 passed, zero failed/skipped/not-run | Includes the metadata-matching assertions and four documentation contracts. Retained reports: `issue28-final2372-ps7.json/.xml` and `issue28-final2372-ps51.json/.xml`. Test-file SHA-256: `d6f2c31c5625cec86cabee0fb5d9f856a40ed2f43ff3e479dbe326a2a8f8a6fa`. Strict Zensical 0.0.62 passed. | + +The clean correction's hosted build failed test-source analysis because two display-only `Article` parameters were unused; it did not pass the full build. Commit `da99d4ebdd0e8c76cba332eb506e06d2eda43691` removed those unused parameters and simplified the test descriptions without changing fixture logic. Its PowerShell parsers and PR-range whitespace check passed. Final-head hosted runtime reports, artifact inspection and post-merge results remain separate gates and are recorded in the PR and release ledger when verified. + +## Limitations +This is deterministic local NTFS fixture evidence, not Windows client/server, ReFS, real-system-root, elevated/non-elevated, hostile-filter, or product lab acceptance. Issue #28 does not require ReFS validation, and the production help already states that the checks cannot provide an atomic defense when a filesystem or filter does not provide stable file IDs. Deferred lab gates and final 1.0 acceptance remain open. diff --git a/docs/next-stage-prompts.md b/docs/next-stage-prompts.md index 8a68a82..de36e75 100644 --- a/docs/next-stage-prompts.md +++ b/docs/next-stage-prompts.md @@ -1,23 +1,17 @@ -# Next-stage prompt +**Completed:** Non-lab delivery and protected 0.0.15-beta publication are verified. Zensical PR #41 and retention PR #42 are merged; issues #26 and #30 are closed. PR #43's 14 directory-identity cases and PR #44's deletion-rights fixtures are implemented and locally validated. Canonical Zensical deployment from af330858b41335366476f0b845e3f73a3c1497d1 and PR #42's exact reviewed-head and merged-source runtime/artifact results have been verified separately; its merged documentation deployment and live bytes also passed. -## Next: complete issue #26 review and deployment acceptance +**Remaining:** Finish independent and hosted review, exact-head validation and merge for #28 PR #43 and #29 PR #44; verify #28/#29 merged-source results; then complete the final documentation sweep. TC-001 product acceptance, TC-003 through TC-007 lab acceptance and final TC-008/009 release acceptance remain open. Lab validation is deferred future work, not underway. -**Completed:** Non-lab delivery and protected `0.0.15-beta` publication are verified. The Zensical 0.0.62 migration is implemented and locally validated with classic navigation, generated references, strict link checks, and the existing single-build exact-byte deployment controls. +# Recovery: finish the authorized temp-issue sequence -**Remaining:** Review and merge issue #26 [PR #41](https://github.com/SamErde/TheCleaners/pull/41), verify its exact deployed bytes, and resolve the hosting-level lowercase route. Narrow deterministic fixture/contract criteria in [#28](https://github.com/SamErde/TheCleaners/issues/28), [#29](https://github.com/SamErde/TheCleaners/issues/29), and [#30](https://github.com/SamErde/TheCleaners/issues/30) remain unexecuted; product labs stay paused. +Continue SamErde/TheCleaners from the current release ledger. Read AGENTS.md, .github/copilot-instructions.md, docs/release-plan-1.0.md, the three issue evidence notes, and the issue/PR discussions. Refresh exact Git state, worktree ownership, open PRs, reviews, checks and paginated reviewThreads before acting; do not redo merged #41/#42 or select already implemented #28/#29 as new implementation work. -Recommended model: GPT-5.6 Sol (`gpt-5.6-sol`), reasoning effort high. Use a different model for independent review when practical; escalate to Astra only for a concrete unresolved deployment or URL-routing risk. +Use the existing isolated worktrees and preserve unrelated local changes. Sol high authored #28/#29; Terra high independently reviews #43, and Astra high reviews #44's ACL/native-identity scope. Sol high independently reviewed Astra high's #42 implementation. Keep one integrator for shared runtime, ledger and changelog writes. Use a different model for each independent review and reassess only if concrete risk warrants escalation. -Continue TheCleaners from `docs/release-plan-1.0.md`. Read `AGENTS.md`, `.github/copilot-instructions.md`, the complete release plan, issue #26, and `docs/deployment-validation.md`. Verify repository identity, the exact candidate head, worktrees, uncommitted changes, and current pull-request state before writing. +For each remaining PR, address every material finding and reply to its thread; record quota-skipped bot reviews as incomplete, not approvals. Require posted independent review, completed available hosted reviews, zero unresolved paginated review threads, passing exact-head checks and inspected source/runtime-bound reports, artifact digests and archives before a SHA-guarded merge. Rebase the later packet onto merged main and revalidate any resolution. Verify merge-triggered runs separately from PR-head evidence. -Review the migration without changing runtime/package behavior or the published `0.0.15-beta` tag and source. Confirm that `zensical.toml` preserves the documented navigation and classic presentation, Zensical 0.0.62 is the only direct documentation dependency, strict validation passes, generated command references remain readable, and Read the Docs plus GitHub Pages use the supported build interface. +Keep the documented observation contract: a deletion handle supplies identity, type, timestamp and logical length before disposition. Read-only sharing excludes ordinary data writers and renames but permits attribute-only timestamp updates; retention checking and deletion are not atomic. The #28 lost-handle test is explicit fault injection, not a reachable intact-plan race. For #29, distinguish documented 128-bit ReFS semantics and fail-closed behavior from unexecuted ReFS runtime acceptance. -Preserve the delivery chain: one strict clean build into configured `site_dir`, add `.nojekyll`, create and retain the source/run-bound manifest, recheck the downloaded site, copy the exact checked bytes to `gh-pages`, and verify every public file plus representative navigation with bounded whole-attempt retries. Do not replace the existing hosting backend or weaken permissions, immutable action pins, manifest checks, or fail-closed behavior. +The maintainer already authorized continuous execution through all three PR merges and the final documentation sweep. Continue without repeated merge approval. Update relevant help/docs, changelog and exact evidence in each packet, then sweep current status, command contracts, support boundaries and historical wording after all three close. Every future successor prompt must begin with succinct Completed / Remaining paragraphs refreshed from the release ledger and carry this requirement forward. Save updated recovery prompts while continuing the authorized work. -Resolve all material review threads and require green exact-head checks. After merge, verify the exact merged-source build and retained artifacts before claiming deployment acceptance. Test `https://day3bits.com/TheCleaners/` and representative deep links. Lowercase `/thecleaners/` currently resolves at the account-site host before the project site and cannot be fixed by Zensical's docs-relative redirect maps. Do not emit case-only alias directory pairs on Windows. Evaluate the separately owned account-root redirect as a companion hosting change, preserving suffixes and the title-case canonical URL without loops. - -Treat the prior MkDocs deployment for source `345f06c861b6d4074e5896e0b27a19f869dfa7e3` as historical evidence only. A local build or PR-head run does not prove the migrated public site. Record exact source, workflow run and attempt, artifact-wrapper digests, file count/bytes/tree hash, `gh-pages` commit, navigation results, URL-case results, and browser observations for the merged deployment. - -Lab validation remains deferred future work and is not underway. Do not provision a host or begin TC-003 through TC-007 automatically. Keep IIS and Exchange structurally preview-only and preserve final TC-001/008/009 product and 1.0 acceptance as open gates. - -Save the next copy-ready prompt without executing it automatically. Begin it with concise **Completed / Remaining** paragraphs refreshed from the ledger. After issue #26 and its hosting follow-up are resolved, refresh the open-issue audit and select one bounded #28/#29/#30 deterministic regression/contract packet. Product labs still require new maintainer direction and approved disposable hosts. +Use isolated fixtures only. Do not provision hosts/volumes, run actual-root cleanup or IIS/Exchange/profile labs, enable product deletion, create release tags, or publish to the Gallery. Final publication and credentials retain their separate approval boundaries. If no authorized non-lab work remains after the sweep, say so and leave labs deferred rather than starting them automatically. diff --git a/docs/release-plan-1.0.md b/docs/release-plan-1.0.md index 4679b3b..7c175b6 100644 --- a/docs/release-plan-1.0.md +++ b/docs/release-plan-1.0.md @@ -1,6 +1,6 @@ # TheCleaners 1.0 implementation plan -Updated September 17, 2026. Non-lab delivery readiness and the approved `0.0.15-beta` prerelease publication are complete. A separate Zensical migration is open in PR #41 under issue #26; review, merge, fresh deployment, and URL-case acceptance remain open. Windows client/server, IIS, Exchange, and profile lab validation is **deferred future work, not underway**. The published prerelease remains separate from final 1.0 product and release acceptance. Exact source, artifact, publication, and installed-package evidence is recorded below. +Updated September 18, 2026. Non-lab delivery readiness and the approved `0.0.15-beta` prerelease publication are complete. Zensical PR #41 and retention PR #42 are merged; issues #26 and #30 are closed. Directory-identity PR #43 and deletion-rights PR #44 are implemented in draft and undergoing review and hosted validation. Windows client/server, IIS, Exchange, and profile lab validation is **deferred future work, not underway**. The published prerelease and bounded issue closures remain separate from final 1.0 product and release acceptance. ## Status conventions @@ -17,7 +17,7 @@ The manifest stays on the current prerelease version during this packet. No comm - Keep Exchange discoverable in 1.0, but structurally incapable of deletion. Require explicit `-WhatIf`, fail before discovery when it is absent/false, and do not silently force a preference or run IIS cleanup. - A later minor release may add a per-invocation Exchange `-AllowRemoval` switch after acceptance. This is **provisional**: the maintainer accepted it for now, not as an irreversible permanent design. There is no persistent unlock and no removal flag in the 1.0 implementation. - IIS may ship as stable only after its gates pass. Until then, keep it structurally preview-only rather than delaying ready commands. -- Zensical migration and support for both `/TheCleaners/` and `/thecleaners/` are [issue #26](https://github.com/SamErde/TheCleaners/issues/26), outside the 1.0 critical path. The framework migration is open in [PR #41](https://github.com/SamErde/TheCleaners/pull/41) with pinned Zensical 0.0.62; review, merged-source deployment, exact-byte verification, and URL-case acceptance remain separate gates. The lowercase project prefix is routed by the account-root Pages site before this project, so native docs-relative redirects cannot fix it. Do not add case-only alias directories that can collide on Windows; evaluate the account-root redirect separately. +- Zensical [PR #41](https://github.com/SamErde/TheCleaners/pull/41) merged with pinned Zensical 0.0.62 and [issue #26](https://github.com/SamErde/TheCleaners/issues/26) is closed. Its canonical merged-source deployment is verified below. The lowercase project prefix is handled by the separately owned account-root Pages site; companion [SamErde/samerde.github.io#13](https://github.com/SamErde/samerde.github.io/pull/13) also merged. That compatibility path uses a JavaScript fallback from a 404 page, not an HTTP redirect. Do not add case-only alias directories that collide on Windows or describe project-relative redirects as account-root routing. - Use a GitHub environment for publishing: required maintainer review, restricted release tags, and an environment-scoped credential. Configuration, credential readiness, actual publication, and published-install evidence are separate gates. - Every successor prompt begins with a very succinct **Completed / Remaining** summary from this ledger and carries that requirement forward. Save prompts without automatically executing them. @@ -33,28 +33,40 @@ The manifest stays on the current prerelease version during this packet. No comm ## Current non-lab delivery sequence -**Completed:** Non-lab delivery readiness; exact documentation deployment; supported runtime/archive validation; local-feed rehearsal; protected `0.0.15-beta` publication from source `345f06c861b6d4074e5896e0b27a19f869dfa7e3`; fresh Gallery-install verification; and exact local validation of PR #41's initial Zensical migration head. +**Completed:** Non-lab delivery and protected `0.0.15-beta` publication with fresh Gallery-install verification; merged Zensical PR #41 and verified canonical deployment; and issue #30's retention contract in merged PR #42 with independently reviewed exact-head runtime/artifact evidence. Issues #26 and #30 are closed. -**Remaining:** Review, merge, deploy, and verify issue #26, including its hosting work. Narrow deterministic fixture/contract criteria in [#28](https://github.com/SamErde/TheCleaners/issues/28), [#29](https://github.com/SamErde/TheCleaners/issues/29), and [#30](https://github.com/SamErde/TheCleaners/issues/30) remain unexecuted alongside deferred labs and final 1.0 acceptance. +**Remaining:** Complete the independent/hosted review, exact-head checks and merge of issue #28 [PR #43](https://github.com/SamErde/TheCleaners/pull/43) and issue #29 [PR #44](https://github.com/SamErde/TheCleaners/pull/44); verify merged-source evidence for #28/#29; then perform the final documentation sweep. PR #42 merged-source runtime, artifact and documentation evidence is verified below. The fixtures in both open PRs are implemented and locally validated, not unexecuted backlog. TC-001 product acceptance, TC-003 through TC-007 labs and final TC-008/009 release acceptance remain deferred/separate. | Bundle | Bounded work and exit evidence | Current status | | --- | --- | --- | | 1. Documentation and delivery readiness | Use title-case URLs throughout; deploy the exact strict-build output once; retain and compare site files/digests and verify navigation at the deployed URL. Configure publishing controls, validate publisher refusal paths and rehearse publish/install against an isolated local repository. | Validated for exact merge `345f06c861b6d4074e5896e0b27a19f869dfa7e3`: deployed bytes/navigation, runtime/archive matrix, publisher refusal paths, local-feed rehearsal, environment rules, credential presence, actual approval, and protected publication all have retained evidence. | | 2. Prerelease preparation | Select an unused alpha/preview or beta version, reconcile release history, align manifest/version/tag/release notes, and validate the exact candidate artifact across required hosted runtimes. Describe labs as deferred and preserve preview locks. Prepare a concrete release for maintainer approval. | Validated and approved. Exact source `345f06c861b6d4074e5896e0b27a19f869dfa7e3`, version `0.0.15-beta`, tag `v0.0.15-beta`, [release notes/history](releases/0.0.15-beta.md), and archive SHA-256 `1c8e061278e68c2e1537186709f79606735c6dda2e48b5cf65a4a877699e3383` are aligned. | | 3. Optional prerelease publication and installed-artifact verification | After explicit approval, publish the exact tested artifact through the protected environment; install that exact Gallery version in fresh Windows CI environments, verify package payload and metadata, quiet import, help, aliases, exports and preview locks, and retain version/commit-bound evidence. | Validated for `0.0.15-beta` by protected run [35268852573](https://github.com/SamErde/TheCleaners/actions/runs/35268852573) and its retained fresh-install reports. This is prerelease/process evidence; final 1.0 needs its own artifact, approval, publication, installed-package evidence, and product acceptance. | -| 4. Zensical migration follow-up (issue #26) | Replace the active documentation framework and dependencies while preserving navigation and exact-byte delivery controls. Review the PR, then verify the merged strict build and canonical deployment. Resolve the lowercase project prefix at its account-root hosting boundary. | Open in PR #41. Initial head `1063c8400609c68c9fe1348d24a191a25905a321` was locally validated with Zensical 0.0.62. Review, exact-head hosted CI, merge, deployment, exact public-byte evidence, and the separately owned account-root redirect remain open. | +| 4. Zensical migration follow-up (issue #26) | Preserve navigation and exact-byte delivery controls; keep lowercase compatibility at the account-root boundary. | PR #41 merged as `af330858b41335366476f0b845e3f73a3c1497d1`; issue #26 and companion account-root PR #13 are closed/merged. Run [35282935445](https://github.com/SamErde/TheCleaners/actions/runs/35282935445) and an independent live recheck verified all 36 canonical files and required navigation; the 404/JavaScript fallback is not an HTTP redirect. | +| 5. Bounded temp issue closure | Complete explicit deterministic acceptance criteria without substituting fixture evidence for broad OS/product labs. | Issue #30 merged in PR #42. PR #43 (#28) and PR #44 (#29) are implemented in draft; independent reviews, hosted checks, merges and the final documentation sweep are in progress under maintainer authorization. | -The prerelease delivery sequence is complete. Finish the separately authorized issue #26 follow-up through reviewed PR and merged deployment acceptance, then refresh the open-issue audit before choosing later work. Issues #28, #29, and #30 retain bounded deterministic fixture/contract criteria that are not lab work and were not executed in this migration. Do not start labs without a new maintainer direction and approved disposable-host prerequisites. +The prerelease delivery sequence and issue #26 migration are complete. Continue the explicitly authorized PR #43/#44 review and merge cycle, retaining PR #42's completed status, then reconcile all three packets in the documentation sweep. Do not select these implemented packets again as fresh work or start labs without new maintainer direction and approved disposable-host prerequisites. The previous next stage (DELETE-PENDING, ROOT-RACE, HANDLE-RECOVERY fixture drivers and evidence-schema regressions) is deferred with the lab work. Do not resume provisioning, actual-root acceptance, IIS/Exchange labs, or profile acceptance automatically. Preserve the runbook and case inventory for a later maintainer-directed resumption. TC-001 product acceptance, TC-003 through TC-007 acceptance, and final stable-release acceptance remain open. No IIS/Exchange removal or 1.0 Exchange `-AllowRemoval` is introduced. -See [publishing controls](publishing.md) and the [copy-ready next-stage prompts](next-stage-prompts.md). Supporting both URL cases remains an open issue #26 deployment gate and does not alter the completed beta publication evidence. +See [publishing controls](publishing.md) and the [copy-ready recovery prompt](next-stage-prompts.md). The historical issue #26 checkpoints below describe their original pre-merge state; the current summary and verified merged-source evidence supersede those pending instructions. + +## September 18 issue #28 directory-identity packet + +**Implemented in draft:** [Issue #28's directory-identity packet](issue-28-directory-identity.md) adds 14 cases across both public temp commands. They verify retained-handle prevention, native identity refusal after explicitly injected handle loss with empty/populated replacements, normal deepest-first pruning, preservation boundaries, WhatIf and terminating errors. No executable runtime changes were required. + +Historical clean integration `171b4cc558b875f72ffedd68aa051d6aeac6e43e` passed 18/18 local tests before the metadata-matching refinement. Clean correction `2372a218d04354df100c2d9188bc30c419764151` then passed **18/18** (14 fixtures plus four documentation contracts) with matching replacement metadata on PowerShell **7.6.6** and Windows PowerShell **5.1.26100.9444**, Pester **5.7.1**, Windows **10.0.26200.0**, zero failures/skips/not-run; strict Zensical **0.0.62** also passed. That correction's hosted build failed on two unused test-display parameters, which `da99d4ebdd0e8c76cba332eb506e06d2eda43691` removed. The packet also tightens the retained-handle fixture to require native sharing violation 32 from the first move. These are bounded local checkpoints, not final-head or merged-source acceptance; the PR's exact final-head hosted checks and artifact inspection remain required before merge. The evidence note distinguishes each historical scope. Issue #30 is merged in [PR #42](https://github.com/SamErde/TheCleaners/pull/42); issue #29 remains in progress. Broader TC-003/004 and product/release gates remain open. ## September 18 issue #30 retention packet -**Implemented in draft:** [Issue #30's deterministic contract packet](issue-30-retention-handle.md) adds 28 native/public-command regression cases for both temp commands and updates source help, command references and safety guidance. The same-handle runtime behavior is unchanged. The documented retention decision uses the timestamp observed from the deletion handle; attribute-only updates after that observation are outside an atomic guarantee. +**Merged implementation:** [PR #42](https://github.com/SamErde/TheCleaners/pull/42) merged as `3d07e9ccbfd441c388b1a3a69edc92fc326a4a66` and closed issue #30. Its [deterministic contract packet](issue-30-retention-handle.md) adds 28 native/public-command regression cases and updates source help, command references and safety guidance. The same-handle runtime behavior is unchanged. The documented retention decision uses the timestamp observed from the deletion handle; attribute-only updates after that observation are outside an atomic guarantee. + +Exact reviewed head `719750ce8c7fd3142b24261b8b75d09499e9f840` passed [build 35330950539](https://github.com/SamErde/TheCleaners/actions/runs/35330950539): PS7.4.20/7.5.11/7.6.6 each passed 333 unit plus four integration tests; PS5.1.26100.33296 passed 337 combined tests, all with zero failures/skips/not-run. PS7 coverage was 87.73%. All eleven artifact-wrapper digests, runtime/source-bound reports, repeated/cross-runtime archives, ACL and local-feed rehearsal evidence were independently verified. The 19-file, 224,558-byte archive SHA-256 was `5153966aaef9f194fdf60fe8793989f4693dcd3310b10c4223d6208865d0135f`. Sol high independently approved the Astra high implementation; Copilot and CodeRabbit completed reviews and the final paginated audit found zero review threads. This is PR-head evidence; the distinct merged-source result follows. + +Merged-source build [35332020558](https://github.com/SamErde/TheCleaners/actions/runs/35332020558) passed at `3d07e9ccbfd441c388b1a3a69edc92fc326a4a66`: PS7.4.20/7.5.11/7.6.6 each passed 333 unit and four integration tests, and Windows PS5.1.26100.33296 passed 337 combined tests, with zero failures/skips/not-run. Independent inspection verified all eleven artifact wrappers, exact source/runtime reports, 87.73% PS7 coverage, ACL evidence, local-feed rehearsal and repeated/cross-runtime archive equality with the same package digest above. Merged-source analyzer [35332020450](https://github.com/SamErde/TheCleaners/actions/runs/35332020450) passed. Documentation deployment [35332020512](https://github.com/SamErde/TheCleaners/actions/runs/35332020512) and an independent live check verified all 37 canonical files and required navigation against the retained site manifest. -Exact clean integration commit `1f6a28e1da328440f1942c66648e9991ba3c5522` passed 28 new fixture cases and four documentation contracts (**32/32**) under PowerShell **7.6.6** and Windows PowerShell **5.1.26100.9444**, with Pester **5.7.1** on Windows **10.0.26200.0**. Both runs had zero failures/skips/not-run tests; strict Zensical **0.0.62** and configuration validation passed. The [issue evidence](issue-30-retention-handle.md) records the test hash and scope. Final-head hosted runtime evidence must be verified before merge; this checkpoint is not merged evidence. Issues #28 and #29 remain in progress. TC-001 product acceptance, TC-003 through TC-007 lab acceptance, and final TC-008/009 release acceptance remain open. +The earlier local integration checkpoint below is historical pre-merge evidence. +Exact clean integration commit `1f6a28e1da328440f1942c66648e9991ba3c5522` passed 28 new fixture cases and four documentation contracts (**32/32**) under PowerShell **7.6.6** and Windows PowerShell **5.1.26100.9444**, with Pester **5.7.1** on Windows **10.0.26200.0**. Both runs had zero failures/skips/not-run tests; strict Zensical **0.0.62** and configuration validation passed. The [issue evidence](issue-30-retention-handle.md) records the test hash and scope. This local checkpoint alone was not merge evidence; the exact final-head and merged-source results above supersede its then-outstanding hosted checks. Issues #28 and #29 remain in progress. TC-001 product acceptance, TC-003 through TC-007 lab acceptance, and final TC-008/009 release acceptance remain open. ## Issue #26 Zensical migration checkpoint diff --git a/docs/safety-and-confirmation.md b/docs/safety-and-confirmation.md index 7df7dca..d322477 100644 --- a/docs/safety-and-confirmation.md +++ b/docs/safety-and-confirmation.md @@ -14,6 +14,8 @@ Cleanup roots and literal filesystem paths must use fully qualified Windows synt By default, no directories are removed. `-RemoveEmptyDirectory` permits only directories emptied by this invocation and now-empty ancestors. It does not authorize the cleanup root, an unrelated pre-existing empty branch, or a directory containing a retained file. Pruning is deepest-first. Empty directories are deleted using a non-recursive API inside the owning command's approved `ShouldProcess` branch; a file appearing after the emptiness check causes deletion to fail, not become recursive. +The plan retains directory handles through pruning, preventing an ordinary rename/replacement of the touched directory while those handles remain open. Pruning also compares native volume/file identity, so a different directory at the same path is not authorized by path equality alone. The [directory-identity regression evidence](issue-28-directory-identity.md) separately records production handle-lock tests and an injected handle-loss case that verifies this defensive identity check. + ```powershell Clear-CurrentUserTemp -Days 30 -RemoveEmptyDirectory -WhatIf -PassThru -Verbose Clear-WindowsTemp -Days 60 -WhatIf -PassThru diff --git a/src/Tests/Unit/TempDirectoryIdentity.Tests.ps1 b/src/Tests/Unit/TempDirectoryIdentity.Tests.ps1 new file mode 100644 index 0000000..a75fe5a --- /dev/null +++ b/src/Tests/Unit/TempDirectoryIdentity.Tests.ps1 @@ -0,0 +1,286 @@ +BeforeDiscovery { + $WindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT + $TempCases = @( + @{ CommandName = 'Clear-CurrentUserTemp' } + @{ CommandName = 'Clear-WindowsTemp' } + ) + $ReplacementCases = @( + @{ ReplacementKind = 'empty'; HasReplacementContent = $false } + @{ ReplacementKind = 'populated'; HasReplacementContent = $true } + ) +} + +BeforeAll { + $ModuleRoot = (Resolve-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath '../../TheCleaners')).Path + foreach ($RelativePath in @( + 'Private/ResultContracts.ps1' + 'Private/Initialize-TheCleanersNativeFileInterop.ps1' + 'Private/Get-TheCleanersWindowsTempRoot.ps1' + 'Private/Get-TheCleanersTempPlan.ps1' + 'Private/Resolve-TheCleanersFileSystemPath.ps1' + 'Public/Clear-CurrentUserTemp.ps1' + 'Public/Clear-WindowsTemp.ps1' + )) { + . (Join-Path -Path $ModuleRoot -ChildPath $RelativePath) + } + Initialize-TheCleanersNativeFileInterop + $OriginalTempPlan = ${function:Get-TheCleanersTempPlan} +} + +Describe 'Temp directory identity closure: ' -ForEach $TempCases -Skip:(-not $WindowsHost) -Tag Unit { + BeforeEach { + $PreviousTemp = $env:TEMP + $PreviousTmp = $env:TMP + $PreviousSystemRoot = $env:SystemRoot + $FixtureRoot = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().Guid) + $FakeWindows = Join-Path -Path $FixtureRoot -ChildPath 'Windows' + $TempRoot = Join-Path -Path $FakeWindows -ChildPath 'Temp' + $ParentPath = Join-Path -Path $TempRoot -ChildPath 'Touched' + $ChildPath = Join-Path -Path $ParentPath -ChildPath 'Child' + $UnrelatedPath = Join-Path -Path $TempRoot -ChildPath 'UnrelatedEmpty' + $RecentPath = Join-Path -Path $TempRoot -ChildPath 'RecentBranch' + $OutsidePath = Join-Path -Path $FixtureRoot -ChildPath 'Outside' + $ReparsePath = Join-Path -Path $TempRoot -ChildPath 'ReparseBranch' + foreach ($Path in @($ChildPath, $UnrelatedPath, $RecentPath, $OutsidePath)) { + $null = New-Item -Path $Path -ItemType Directory -Force + } + $CandidatePath = Join-Path -Path $ChildPath -ChildPath 'old.tmp' + [System.IO.File]::WriteAllText($CandidatePath, 'old') + $RecentFilePath = Join-Path -Path $RecentPath -ChildPath 'recent.tmp' + [System.IO.File]::WriteAllText($RecentFilePath, 'recent') + $OutsideFilePath = Join-Path -Path $OutsidePath -ChildPath 'outside.tmp' + [System.IO.File]::WriteAllText($OutsideFilePath, 'outside') + $Now = [DateTime]::UtcNow + [System.IO.File]::SetLastWriteTimeUtc($CandidatePath, $Now.AddDays(-31)) + [System.IO.File]::SetLastWriteTimeUtc($RecentFilePath, $Now) + [System.IO.File]::SetLastWriteTimeUtc($OutsideFilePath, $Now.AddDays(-31)) + $null = New-Item -Path $ReparsePath -ItemType Junction -Target $OutsidePath -ErrorAction Stop + + $env:TEMP = $TempRoot + $env:TMP = $TempRoot + $env:SystemRoot = $FakeWindows + $State = [pscustomobject]@{ + Handles = @() + } + Mock Get-TheCleanersWindowsTempRoot { Resolve-TheCleanersFileSystemPath -LiteralPath $TempRoot } + Mock Get-Date { $Now } + } + + AfterEach { + foreach ($Handle in @($State.Handles)) { + if ($null -ne $Handle -and -not $Handle.IsClosed) { + $Handle.Dispose() + } + } + if ([System.IO.Directory]::Exists($ReparsePath)) { + [System.IO.Directory]::Delete($ReparsePath) + } + $env:TEMP = $PreviousTemp + $env:TMP = $PreviousTmp + $env:SystemRoot = $PreviousSystemRoot + } + + It 'prevents a same-path replacement attempt with a payload of type while the planned handle is retained' -TestCases $ReplacementCases { + param($ReplacementKind, $HasReplacementContent) + + $ReplacementSource = Join-Path -Path $FixtureRoot -ChildPath "Replacement-$ReplacementKind" + $DisplacedPath = Join-Path -Path $FixtureRoot -ChildPath "Displaced-$ReplacementKind" + $null = New-Item -Path $ReplacementSource -ItemType Directory + if ($HasReplacementContent) { + [System.IO.File]::WriteAllText((Join-Path -Path $ReplacementSource -ChildPath 'new.tmp'), 'new') + } + $script:ReplacementAttempted = $false + $script:ReplacementInstalled = $false + $script:ReplacementFailure = $null + + Mock Get-TheCleanersTempPlan { + $Plan = & $OriginalTempPlan -Root $Root -TraversalRootPath $TraversalRootPath -CutoffUtc $CutoffUtc -RemoveEmptyDirectory:$RemoveEmptyDirectory -CaptureIdentity:$CaptureIdentity -ValidatedRootIdentity $ValidatedRootIdentity + $State.Handles = @($Plan.HeldDirectoryHandles | ForEach-Object { $_.Handle }) + $Plan + } + Mock Get-Item { + if ($LiteralPath -eq $ChildPath -and + -not [System.IO.File]::Exists($CandidatePath) -and + -not $script:ReplacementAttempted) { + $script:ReplacementAttempted = $true + try { + [System.IO.Directory]::Move($ChildPath, $DisplacedPath) + } catch { + $script:ReplacementFailure = $_.Exception.GetBaseException() + } + if ($null -eq $script:ReplacementFailure) { + [System.IO.Directory]::Move($ReplacementSource, $ChildPath) + $script:ReplacementInstalled = $true + } + } + + if ([System.IO.Directory]::Exists($LiteralPath)) { + $Item = [System.IO.DirectoryInfo]::new($LiteralPath) + $Item | Add-Member -MemberType NoteProperty -Name PSIsContainer -Value $true -Force + return $Item + } + if ([System.IO.File]::Exists($LiteralPath)) { + $Item = [System.IO.FileInfo]::new($LiteralPath) + $Item | Add-Member -MemberType NoteProperty -Name PSIsContainer -Value $false -Force + return $Item + } + throw [System.IO.FileNotFoundException]::new("Fixture path was not found: '$LiteralPath'.") + } + + $Result = & $CommandName -Days 30 -RemoveEmptyDirectory -Confirm:$false -PassThru -ErrorAction Stop + + $script:ReplacementAttempted | Should -BeTrue + $script:ReplacementInstalled | Should -BeFalse + $script:ReplacementFailure | Should -Not -BeNullOrEmpty + $script:ReplacementFailure | Should -BeOfType ([System.IO.IOException]) + ($script:ReplacementFailure.HResult -band 0xFFFF) | Should -Be 32 + $DisplacedPath | Should -Not -Exist + $ReplacementSource | Should -Exist + if ($HasReplacementContent) { + (Join-Path -Path $ReplacementSource -ChildPath 'new.tmp') | Should -Exist + } else { + @(Get-ChildItem -LiteralPath $ReplacementSource -Force) | Should -HaveCount 0 + } + $Result.FilesRemoved | Should -Be 1 + $Result.DirectoriesRemoved | Should -Be 2 + $Result.DirectoryFailureCount | Should -Be 0 + $Result.Status | Should -Be 'Completed' + $ChildPath | Should -Not -Exist + $ParentPath | Should -Not -Exist + @($State.Handles | Where-Object { -not $_.IsClosed }) | Should -HaveCount 0 + } + + It 'rejects and preserves an injected same-path replacement with a payload of type by native identity' -TestCases $ReplacementCases { + param($ReplacementKind, $HasReplacementContent) + + $ReplacementSource = Join-Path -Path $FixtureRoot -ChildPath "InjectedReplacement-$ReplacementKind" + $DisplacedPath = Join-Path -Path $FixtureRoot -ChildPath "OriginalDirectory-$ReplacementKind" + $null = New-Item -Path $ReplacementSource -ItemType Directory + if ($HasReplacementContent) { + [System.IO.File]::WriteAllText((Join-Path -Path $ReplacementSource -ChildPath 'new.tmp'), 'new') + } + $script:ReplacementInjected = $false + $script:OriginalCreationTimeUtc = $null + $script:OriginalAttributes = $null + $script:OriginalIdentity = $null + $script:ReplacementCreationTimeUtc = $null + $script:ReplacementAttributes = $null + $script:ReplacementIdentity = $null + $script:ChildPlan = $null + + Mock Get-TheCleanersTempPlan { + $Plan = & $OriginalTempPlan -Root $Root -TraversalRootPath $TraversalRootPath -CutoffUtc $CutoffUtc -RemoveEmptyDirectory:$RemoveEmptyDirectory -CaptureIdentity:$CaptureIdentity -ValidatedRootIdentity $ValidatedRootIdentity + $State.Handles = @($Plan.HeldDirectoryHandles | ForEach-Object { $_.Handle }) + $script:ChildPlan = @($Plan.Directories | Where-Object Path -EQ $ChildPath)[0] + $script:OriginalIdentity = $script:ChildPlan.Identity + $script:OriginalCreationTimeUtc = [System.IO.Directory]::GetCreationTimeUtc($ChildPath) + $script:OriginalAttributes = [System.IO.File]::GetAttributes($ChildPath) + $Plan + } + Mock Get-Item { + if ($LiteralPath -eq $ChildPath -and + -not [System.IO.File]::Exists($CandidatePath) -and + -not $script:ReplacementInjected) { + # Production retains this handle and therefore prevents the move. + # This isolated fault injection removes only that primary barrier + # to exercise the public command's independent native-ID refusal. + $script:ChildPlan.Handle.Dispose() + $script:ChildPlan.Handle = $null + [System.IO.Directory]::Move($ChildPath, $DisplacedPath) + [System.IO.Directory]::Move($ReplacementSource, $ChildPath) + # Match mutable metadata before the identity read so path, + # timestamps and attributes cannot explain the rejection. + [System.IO.Directory]::SetCreationTimeUtc($ChildPath, $script:OriginalCreationTimeUtc) + [System.IO.Directory]::SetLastWriteTimeUtc($ChildPath, $script:OriginalIdentity.LastWriteTimeUtc) + [System.IO.File]::SetAttributes($ChildPath, $script:OriginalAttributes) + $script:ReplacementCreationTimeUtc = [System.IO.Directory]::GetCreationTimeUtc($ChildPath) + $script:ReplacementAttributes = [System.IO.File]::GetAttributes($ChildPath) + $script:ReplacementIdentity = Get-TheCleanersFileIdentity -LiteralPath $ChildPath -Directory + $script:ReplacementInjected = $true + } + + if ([System.IO.Directory]::Exists($LiteralPath)) { + $Item = [System.IO.DirectoryInfo]::new($LiteralPath) + $Item | Add-Member -MemberType NoteProperty -Name PSIsContainer -Value $true -Force + return $Item + } + if ([System.IO.File]::Exists($LiteralPath)) { + $Item = [System.IO.FileInfo]::new($LiteralPath) + $Item | Add-Member -MemberType NoteProperty -Name PSIsContainer -Value $false -Force + return $Item + } + throw [System.IO.FileNotFoundException]::new("Fixture path was not found: '$LiteralPath'.") + } + + $Result = & $CommandName -Days 30 -RemoveEmptyDirectory -Confirm:$false -PassThru -ErrorAction Stop + + $script:ReplacementInjected | Should -BeTrue + $script:ReplacementCreationTimeUtc.ToFileTimeUtc() | Should -Be $script:OriginalCreationTimeUtc.ToFileTimeUtc() + $script:ReplacementIdentity.LastWriteTimeUtc.ToFileTimeUtc() | Should -Be $script:OriginalIdentity.LastWriteTimeUtc.ToFileTimeUtc() + [uint32]$script:ReplacementAttributes | Should -Be ([uint32]$script:OriginalAttributes) + [uint32]$script:ReplacementIdentity.Attributes | Should -Be ([uint32]$script:OriginalIdentity.Attributes) + $script:OriginalIdentity.Equals($script:ReplacementIdentity) | Should -BeFalse + $script:OriginalIdentity.Equals((Get-TheCleanersFileIdentity -LiteralPath $DisplacedPath -Directory)) | Should -BeTrue + $ChildPath | Should -Exist + $DisplacedPath | Should -Exist + if ($HasReplacementContent) { + (Join-Path -Path $ChildPath -ChildPath 'new.tmp') | Should -Exist + } else { + @(Get-ChildItem -LiteralPath $ChildPath -Force) | Should -HaveCount 0 + } + $Result.FilesRemoved | Should -Be 1 + $Result.DirectoriesRemoved | Should -Be 0 + $Result.DirectoriesSkipped | Should -Be 2 + $Result.DirectoryFailureCount | Should -Be 0 + $Result.Status | Should -Be 'CompletedWithSkips' + @($State.Handles | Where-Object { -not $_.IsClosed }) | Should -HaveCount 0 + } + + It 'prunes genuine directories deepest-first while preserving root, unrelated, recent, and reparse branches' { + $Result = & $CommandName -Days 30 -RemoveEmptyDirectory -Confirm:$false -PassThru -ErrorAction Stop + + $Result.FilesRemoved | Should -Be 1 + $Result.DirectoryCandidateCount | Should -Be 2 + $Result.DirectoriesRemoved | Should -Be 2 + $Result.Status | Should -Be 'Completed' + $ChildPath | Should -Not -Exist + $ParentPath | Should -Not -Exist + $TempRoot | Should -Exist + $UnrelatedPath | Should -Exist + $RecentFilePath | Should -Exist + $ReparsePath | Should -Exist + $OutsideFilePath | Should -Exist + } + + It 'keeps every fixture intact under WhatIf' { + $Result = & $CommandName -Days 30 -RemoveEmptyDirectory -WhatIf -PassThru -ErrorAction Stop + + $Result.Status | Should -Be 'WhatIf' + $Result.FileCandidateCount | Should -Be 1 + $Result.DirectoryCandidateCount | Should -Be 2 + $Result.FilesRemoved | Should -Be 0 + $Result.DirectoriesRemoved | Should -Be 0 + $CandidatePath | Should -Exist + $ChildPath | Should -Exist + $ParentPath | Should -Exist + $TempRoot | Should -Exist + $UnrelatedPath | Should -Exist + $RecentFilePath | Should -Exist + $ReparsePath | Should -Exist + $OutsideFilePath | Should -Exist + } + + It 'honors ErrorAction Stop and preserves directory ancestry after candidate deletion fails' { + $CandidateLock = [System.IO.File]::Open($CandidatePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) + try { + { & $CommandName -Days 30 -RemoveEmptyDirectory -Confirm:$false -ErrorAction Stop } | Should -Throw -ErrorId 'TempFileRemovalFailed,*' + } finally { + $CandidateLock.Dispose() + } + + $CandidatePath | Should -Exist + $ChildPath | Should -Exist + $ParentPath | Should -Exist + $TempRoot | Should -Exist + } +}