Skip to content

Bug: ordinary dotnet clean leaves Copilot wrapper test prerequisite caches under obj\mxcw #1620

Closed

Description

@JoshuaRowePhantom

Summary

dotnet clean (project or solution) does not clean the Copilot wrapper test prerequisite cache created before Phantom.Workspaces.Install.Tests runs. Repeated test runs can leave completed directories such as Phantom.Workspaces.Install.Tests\obj\mxcw\<hash>\prepared\phantom-copilot-wrapper.exe, even after ordinary MSBuild Clean. The build/test prerequisite should be owned by the normal Clean lifecycle, not require a separate manual cleanup command. This is diagnosed from code; a surviving cache was not reproduced in the current worktree (the sample cache was no longer present), so this is not a claim of an empirical dotnet clean run.

Root Cause

  • features/Phantom.Workspaces.Install.Tests/Phantom.Workspaces.Install.Tests.csproj:32-47 attaches creation to VSTest only, with neither a Clean target nor MSBuild FileWrites/incremental-clean registration for the cache or pointer:
    <Target Name="PrepareCopilotWrapperTestPrerequisite" BeforeTargets="VSTest" ...>
      <_CopilotWrapperPrerequisiteCache>$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)mxcw</_CopilotWrapperPrerequisiteCache>
      <_CopilotWrapperPrerequisitePathFile>$(MSBuildProjectDirectory)\$(IntermediateOutputPath)copilot-wrapper-prerequisite.path</_CopilotWrapperPrerequisitePathFile>
      <Exec Command="pwsh ... -CacheRoot &quot;$(_CopilotWrapperPrerequisiteCache)&quot; -PathOutputFile &quot;$(_CopilotWrapperPrerequisitePathFile)&quot;" ... />
      <Copy SourceFiles="$(_CopilotWrapperPrerequisitePathFile)"
            DestinationFiles="$(OutDir)copilot-wrapper-prerequisite.path" ... />
    </Target>
    The ordinary clean of build outputs does not know it must remove all hashed cache entries; the BeforeTargets="VSTest" preparation is not a Clean handler.
  • features/scripts/prepare-copilot-wrapper-test-prerequisite.ps1:292-312,337-350 hashes every tracked and non-ignored untracked file (plus toolchain inputs) into the source fingerprint. Even an unrelated changed file can produce another cache key. At :374-391 only the last pointer's verified cache is reused; at :832-857 a new immutable entry is moved into the cache root and the pointer is replaced. :860-874 removes transient staging/production intermediate paths but not previous completed entries. This explains why stale copies accumulate; narrowing the fingerprint is separate from the requested Clean integration.
  • features/Phantom.Workspaces.Install.Tests/MxcIntegrationTests.cs:1436-1493 reads the copied pointer from the test output and expects the immutable obj\mxcw\<key>\prepared artifact. features/Phantom.Workspaces/Phantom.Workspaces.csproj:125-133 also validates that a production publish's prepared payload lies in the canonical Install.Tests cache root. Clean must not run preparation or alter the provenance checks.

Affected Files

File Role
features/Phantom.Workspaces.Install.Tests/Phantom.Workspaces.Install.Tests.csproj Defines cache root and pointer/output copy; needs an ordinary MSBuild Clean hook.
features/scripts/prepare-copilot-wrapper-test-prerequisite.ps1 Creates immutable hashed entries; cleanup should share its root/path validation and concurrency contract (or use a narrowly scoped companion script).
features/Phantom.Workspaces.Install.Tests/MxcIntegrationTests.cs Existing prerequisite path/manifest assertions; add build/Clean lifecycle coverage using its test naming conventions.
features/Phantom.Workspaces/Phantom.Workspaces.csproj Existing publish-time canonical cache provenance guard; retain it and reconcile any explicitly supported custom intermediate path behavior.

Design / Fix

  1. Hook cleanup into ordinary Clean of Install.Tests (therefore solution Clean), without invoking PrepareCopilotWrapperTestPrerequisite or requiring a new command. E.g. a project target BeforeTargets="Clean" (or a verified equivalent normal Clean hook) calls a dedicated pwsh -File ... -CacheRoot &quot;...&quot; -PathOutputFile &quot;...&quot; -CopiedPathFile &quot;$(OutDir)copilot-wrapper-prerequisite.path&quot; operation. Evaluate and quote the same MSBuild paths as preparation; do not depend on the pointer to discover all historical <hash> entries. Cover command ordering so SDK Clean does not leave or regenerate the pointer/copy.
  2. Validate the canonical cache root against the project's evaluated BaseIntermediateOutputPath (including a supported custom path containing spaces), and constrain deletion to the project-owned mxcw tree plus that build's pointer and copied output. Do not follow reparse points or accept a pointer to an arbitrary external path; reject unsafe roots rather than deleting elsewhere. Clear all completed hashed entries and stale staging under the validated root, not just the latest key. Repeat Clean should succeed when everything is absent.
  3. Coordinate Clean and prerequisite creation/consumption at the cache-root level (a per-fingerprint creation mutex alone is insufficient for an all-keys deletion). Prevent racing active builds/tests from losing their immutable inputs: hold an appropriate lease/lock across use or fail Clean explicitly before partial deletion if the root is in use; do not silently swallow locked-file failures. Restore/build/test after Clean must recreate and validate the prerequisite normally; production publish provenance remains fail-closed. Keep the broad fingerprint behavior out of scope for this fix except to test that multiple historical keys are removed.

Expected Tests

Test Name Class What It Verifies
CopilotWrapperPrerequisite_CleanAfterMultipleFingerprints_RemovesAllCacheEntriesAndPointers CopilotWrapperNestedPublishTests Ordinary project dotnet clean removes all completed/staging entries, intermediate pointer and copied output pointer without preparing again.
CopilotWrapperPrerequisite_SolutionClean_RemovesInstallTestCache BuildIntegrationTests Ordinary solution Clean also triggers the project cleanup.
CopilotWrapperPrerequisite_RepeatedClean_IsIdempotent CopilotWrapperNestedPublishTests A second Clean with already-absent entries succeeds.
CopilotWrapperPrerequisite_CustomBaseIntermediateWithSpaces_CleansOnlyCanonicalRoot CopilotWrapperNestedPublishTests Evaluated custom base path (including spaces) is handled without deleting anything outside the owned root.
CopilotWrapperPrerequisite_CleanWhileInUse_FailsWithoutPartialDeletion CopilotWrapperNestedPublishTests A concurrent active prerequisite build/test is protected; Clean fails clearly or waits under an explicit safe locking policy.
CopilotWrapperPrerequisite_CleanThenTest_RecreatesValidatedCache CopilotWrapperNestedPublishTests A subsequent build/test restores the pointer and verified immutable manifest/artifacts.

Activity

  1. added
    bugSomething isn't working
    diagnosedRoot cause identified
    needs-slow-testsRequires full test suite including slow Git tests at checkin
    on Oct 8, 2026
  2. JoshuaRowePhantom commented on Oct 8, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification result

    Status: ❌ 4 issue(s) found
    Commits checked: 26ac0961 Clean Copilot wrapper prerequisite cache with project Clean (against features)

    Outstanding gaps

    • Clean → prep rebuild test fails. CopilotWrapperPrerequisite_CleanThenTest_RecreatesValidatedCache exceeded MxcRepositoryTestSupport.InvokeAsync's 10-minute process timeout during real prerequisite preparation (ProcessRunner.cs:466, test line 123); no rebuilt cache/manifest was verified. The first default run also aborted its test host at the approved runner's 90-second inactivity threshold during this test. This expected test must pass under the approved runner.
    • Custom intermediate path is not end-to-end compatible. Custom-with-spaces project Clean passed, but MxcRepositoryTestSupport.LoadCopilotWrapperPrerequisite in MxcIntegrationTests.cs and _PreparedCopilotPayloadExpectedCacheRoot in Phantom.Workspaces.csproj still require Install.Tests\obj\mxcw, whereas PrepareCopilotWrapperTestPrerequisite now emits to the evaluated custom BaseIntermediateOutputPath\mxcw. The new custom-path test checks only Clean, not a normal test/publish consuming the custom cache. Reconcile this without weakening fail-closed provenance, or explicitly reject custom preparation rather than emit an unusable pointer.
    • Active test lifecycle has an unleased window. The exclusive lease in prepare-copilot-wrapper-test-prerequisite.ps1 ends before VSTest starts; the test host acquires a shared lease only in LoadCopilotWrapperPrerequisite, then releases it after each individual consumer. Ordinary Clean can acquire the root lock and remove the copied pointer/cache while a test run is active between preparation and loading, or between test cases. CleanWhileInUse manually locks the file and does not test this actual lifecycle race.
    • Unsafe-path branch tests are incomplete. The reparse-point test covers a child cache junction and the external-root test covers an unowned intermediate base, but no test exercises a reparse-point cache root/ancestor, either pointer being a symlink/directory, an unowned copied output (OutDir), or an unexpected cache-root entry. These are new fail-before-any-deletion branches in CopilotWrapperPrerequisiteCache.ps1 and the cleaner, and require verification without affecting outside files or previously existing cache entries.

    Tests

    Approved runner from worktrees\16: scripts\run-tests.ps1 -Mode fast -TestNames CopilotWrapperPrerequisite_: 3 passed, 0 failed, then test host aborted for 90-second inactivity while CleanThenTest prepared its prerequisite (exit 1; hang dumps collected). Rerun using scripts\run-tests.ps1 -Mode fast -NoBuild -PerTestHangTimeout 20m -TestNames CopilotWrapperPrerequisite_CleanAfterMultipleFingerprints,CopilotWrapperPrerequisite_RepeatedClean,CopilotWrapperPrerequisite_CleanWhileInUse,CopilotWrapperPrerequisite_ExternalIntermediateRoot,CopilotWrapperPrerequisite_LockedCacheFile,CopilotWrapperPrerequisite_CleanThenTest: Executed 6; Passed 5; Failed 1. scripts\test-results.log: System.TimeoutException: Process did not complete within the allotted 00:10:00 in CopilotWrapperPrerequisite_CleanThenTest_RecreatesValidatedCache. Previous run additionally passed SolutionClean, ReparsePoint, and CustomBaseIntermediateWithSpaces (3/3). Filesystem cleanup requires slow tests at checkin; needs-slow-tests applied. verified-locally not applied.

  3. JoshuaRowePhantom commented on Oct 9, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification result

    Status: ✅ Verified
    Commits checked: 26ac0961 and b6a49cc0 (against features; tip b6a49cc0b71fa315c9d4bb0479a6cf384c0952a5). Source and submodules synchronized with checkout-source.ps1 before inspection.

    Outstanding gaps

    None — all criteria satisfied.

    The ordinary project/solution Clean hook validates ownership and evaluated intermediate paths, preflights pointers, reparse points, unexpected cache entries and locked files before deletion, and removes historical entries without invoking preparation. The shared/exclusive cache-root lease spans preparation, MSBuild/VSTest startup, test-host consumption and prepared publish; Clean fails while in use. Custom paths with spaces are accepted within the Install.Tests project, and the prepared publish retains fail-closed cache-root and artifact validation. Cold Clean → prerequisite recreation → custom-cache publish and final Clean completed successfully.

    Tests

    From worktrees\16: scripts\run-tests.ps1 -Mode fast -NoBuild -PerTestHangTimeout 20m -TestNames CopilotWrapperPrerequisite_ — Executed: 18; Passed: 18; Failed: 0, including the previously timed-out cold rebuild, active test-host Clean rejection, project/solution Clean, custom base and unsafe-path cases. Confirmed each result in the Install.Tests TRX and scripts\test-results.log. Filesystem changes require slow tests at checkin; needs-slow-tests is already present.

  4. added a commit that references this issue on Oct 9, 2026
  5. JoshuaRowePhantom commented on Oct 9, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Checked into features via #1626 (merge 1b86a98). Hosted fast CI 37966216282 passed 8,019/8,019; full CI 37966209695 passed on Windows and Linux. #1617 and #1618 retain their separate acceptance gates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdiagnosedRoot cause identifiedverified-locallyImplementation has been verified locally

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions