@@ -604,6 +604,228 @@ Invoke-TestPackageDescribe -Name 'Eigenverft.Manifested.Sandbox Package - acquis
604604 $result.InstallOrigin | Should - Be ' PackageReused'
605605 }
606606
607+ It ' discovers an older OpenCode package-owned install from inventory and replaces it for a new selected version' {
608+ $rootPath = Join-Path $TestDrive ' opencode-inventory-update'
609+ $preferredInstallRoot = Join-Path $rootPath ' installs'
610+ $oldInstallRoot = Join-Path $preferredInstallRoot ' opencode-runtime\stable\1.14.46\win32-x64'
611+ $packageStateIndexPath = Join-Path $rootPath ' PackageAssignmentInventory.json'
612+ $null = New-Item - ItemType Directory - Path (Join-Path $oldInstallRoot ' bin' ) - Force
613+ Write-TestTextFile - Path (Join-Path $oldInstallRoot ' Code.exe' ) - Content ' fake'
614+ Write-TestTextFile - Path (Join-Path $oldInstallRoot ' bin\code.cmd' ) - Content " @echo off`r`n echo 1.14.46`r`n "
615+ Write-TestJsonDocument - Path $packageStateIndexPath - Document @ {
616+ records = @ (
617+ @ {
618+ installSlotId = ' OpenCodeCli:stable:win32-x64'
619+ definitionId = ' OpenCodeCli'
620+ releaseTrack = ' stable'
621+ artifactDistributionVariant = ' win32-x64'
622+ currentReleaseId = ' opencode-runtime-win32-x64-stable'
623+ currentVersion = ' 1.14.46'
624+ installDirectory = $oldInstallRoot
625+ ownershipKind = ' PackageInstalled'
626+ updatedAtUtc = [DateTime ]::UtcNow.ToString(' o' )
627+ }
628+ )
629+ }
630+
631+ $sharedInstall = @ {
632+ install = @ {
633+ kind = ' expandArchive'
634+ installDirectory = ' opencode-runtime/{releaseTrack}/{version}/{artifactDistributionVariant}'
635+ expandedRoot = ' auto'
636+ }
637+ versionUpdatePolicy = @ {
638+ whenAssigned = ' trackSelectedVersion'
639+ onSameSelectedVersion = ' reuseOrRepair'
640+ onNewSelectedVersion = ' replacePackageOwnedInstall'
641+ }
642+ }
643+ $readiness = New-TestReadiness - Version ' 1.15.7' - Directories @ ()
644+ $release = New-TestPackageRelease - Id ' opencode-runtime-win32-x64-stable' - Version ' 1.15.7' - Architecture ' x64' - ArtifactDistributionVariant ' win32-x64' - FileName ' opencode-1.15.7.zip' - Readiness $readiness
645+ $documents = Write-TestPackageDocuments - RootPath $rootPath - GlobalDocument (New-TestPackageGlobalDocument - PreferredTargetInstallDirectory $preferredInstallRoot - PackageAssignmentInventoryFilePath $packageStateIndexPath ) - DefinitionDocument (New-TestVSCodeDefinitionDocument - DefinitionId ' OpenCodeCli' - Releases @ ($release ) - SharedInstall $sharedInstall - SharedReadiness $readiness - SharedExistingInstallDiscovery (New-TestExistingInstallDiscovery - Enabled $false ))
646+
647+ [Environment ]::SetEnvironmentVariable($script :SourceInventoryEnvVarName , (Join-Path $TestDrive ' missing-inventory.json' ), ' Process' )
648+ Mock Get-PackageConfigPath { $documents.GlobalConfigPath }
649+ Mock Get-PackageDefinitionPath { param ($DefinitionId ) $documents.DefinitionPath }
650+
651+ $config = Get-PackageConfig - DefinitionId ' OpenCodeCli'
652+ $result = New-PackageResult - PackageConfig $config
653+ $result = Resolve-PackagePackage - PackageResult $result
654+ $result = Resolve-PackagePaths - PackageResult $result
655+ $newInstallRoot = $result.InstallDirectory
656+ Test-Path - LiteralPath $newInstallRoot - PathType Container | Should - BeFalse
657+ $result = Find-PackageExistingPackage - PackageResult $result
658+ $result = Set-PackageExistingPackage - PackageResult $result
659+ $result = Resolve-PackageExistingPackageDecision - PackageResult $result
660+
661+ $result.ExistingPackage.SearchKind | Should - Be ' packageInventoryInstallSlot'
662+ $result.ExistingPackage.InstallDirectory | Should - Be ([System.IO.Path ]::GetFullPath($oldInstallRoot ))
663+ $result.ExistingPackage.Decision | Should - Be ' ReplacePackageOwnedInstall'
664+ $result.InstallDirectory | Should - Be $newInstallRoot
665+ $result.Readiness | Should - BeNullOrEmpty
666+ }
667+
668+ It ' uses an exact PackageVersion selector to downgrade a package-owned install when replacement is allowed' {
669+ $rootPath = Join-Path $TestDrive ' opencode-inventory-downgrade'
670+ $preferredInstallRoot = Join-Path $rootPath ' installs'
671+ $newerInstallRoot = Join-Path $preferredInstallRoot ' opencode-runtime\stable\1.15.7\win32-x64'
672+ $packageStateIndexPath = Join-Path $rootPath ' PackageAssignmentInventory.json'
673+ $null = New-Item - ItemType Directory - Path (Join-Path $newerInstallRoot ' bin' ) - Force
674+ Write-TestTextFile - Path (Join-Path $newerInstallRoot ' Code.exe' ) - Content ' fake'
675+ Write-TestTextFile - Path (Join-Path $newerInstallRoot ' bin\code.cmd' ) - Content " @echo off`r`n echo 1.15.7`r`n "
676+ Write-TestJsonDocument - Path $packageStateIndexPath - Document @ {
677+ records = @ (
678+ @ {
679+ installSlotId = ' OpenCodeCli:stable:win32-x64'
680+ definitionId = ' OpenCodeCli'
681+ releaseTrack = ' stable'
682+ artifactDistributionVariant = ' win32-x64'
683+ currentReleaseId = ' opencode-runtime-win32-x64-stable'
684+ currentVersion = ' 1.15.7'
685+ installDirectory = $newerInstallRoot
686+ ownershipKind = ' PackageInstalled'
687+ updatedAtUtc = [DateTime ]::UtcNow.ToString(' o' )
688+ }
689+ )
690+ }
691+
692+ $sharedInstall = @ {
693+ install = @ {
694+ kind = ' expandArchive'
695+ installDirectory = ' opencode-runtime/{releaseTrack}/{version}/{artifactDistributionVariant}'
696+ expandedRoot = ' auto'
697+ }
698+ versionUpdatePolicy = @ {
699+ whenAssigned = ' trackSelectedVersion'
700+ onSameSelectedVersion = ' reuseOrRepair'
701+ onNewSelectedVersion = ' replacePackageOwnedInstall'
702+ }
703+ }
704+ $readiness = New-TestReadiness - Version ' 1.14.46' - Directories @ ()
705+ $releaseLatest = New-TestPackageRelease - Id ' opencode-runtime-win32-x64-stable' - Version ' 1.15.7' - Architecture ' x64' - ArtifactDistributionVariant ' win32-x64' - FileName ' opencode-1.15.7.zip' - Readiness (New-TestReadiness - Version ' 1.15.7' - Directories @ ())
706+ $definitionDocument = New-TestVSCodeDefinitionDocument - DefinitionId ' OpenCodeCli' - Releases @ ($releaseLatest ) - SharedInstall $sharedInstall - SharedReadiness $readiness - SharedExistingInstallDiscovery (New-TestExistingInstallDiscovery - Enabled $false )
707+ $definitionDocument.artifacts.releases += [ordered ]@ {
708+ version = ' 1.14.46'
709+ releaseTracks = @ (' stable' )
710+ targetArtifacts = @ {
711+ ' opencode-runtime-win32-x64-stable' = @ {
712+ artifactId = ' opencode-runtime-win32-x64-stable'
713+ fileName = ' opencode-1.14.46.zip'
714+ }
715+ }
716+ }
717+ $documents = Write-TestPackageDocuments - RootPath $rootPath - GlobalDocument (New-TestPackageGlobalDocument - PreferredTargetInstallDirectory $preferredInstallRoot - PackageAssignmentInventoryFilePath $packageStateIndexPath ) - DefinitionDocument $definitionDocument
718+
719+ [Environment ]::SetEnvironmentVariable($script :SourceInventoryEnvVarName , (Join-Path $TestDrive ' missing-inventory.json' ), ' Process' )
720+ Mock Get-PackageConfigPath { $documents.GlobalConfigPath }
721+ Mock Get-PackageDefinitionPath { param ($DefinitionId ) $documents.DefinitionPath }
722+
723+ $config = Get-PackageConfig - DefinitionId ' OpenCodeCli'
724+ $result = New-PackageResult - PackageConfig $config - PackageVersionSelector ' 1.14.46'
725+ $result = Resolve-PackagePackage - PackageResult $result
726+ $result = Resolve-PackagePaths - PackageResult $result
727+ $downgradeInstallRoot = $result.InstallDirectory
728+ $result = Find-PackageExistingPackage - PackageResult $result
729+ $result = Set-PackageExistingPackage - PackageResult $result
730+ $result = Resolve-PackageExistingPackageDecision - PackageResult $result
731+
732+ $result.Package.version | Should - Be ' 1.14.46'
733+ $result.ExistingPackage.SearchKind | Should - Be ' packageInventoryInstallSlot'
734+ $result.ExistingPackage.InstallDirectory | Should - Be ([System.IO.Path ]::GetFullPath($newerInstallRoot ))
735+ $result.ExistingPackage.Decision | Should - Be ' ReplacePackageOwnedInstall'
736+ $result.InstallDirectory | Should - Be $downgradeInstallRoot
737+ }
738+
739+ It ' fails an assigned version change when versionUpdatePolicy.onNewSelectedVersion is fail' {
740+ $rootPath = Join-Path $TestDrive ' opencode-inventory-update-fail'
741+ $preferredInstallRoot = Join-Path $rootPath ' installs'
742+ $oldInstallRoot = Join-Path $preferredInstallRoot ' opencode-runtime\stable\1.14.46\win32-x64'
743+ $packageStateIndexPath = Join-Path $rootPath ' PackageAssignmentInventory.json'
744+ $null = New-Item - ItemType Directory - Path (Join-Path $oldInstallRoot ' bin' ) - Force
745+ Write-TestTextFile - Path (Join-Path $oldInstallRoot ' Code.exe' ) - Content ' fake'
746+ Write-TestTextFile - Path (Join-Path $oldInstallRoot ' bin\code.cmd' ) - Content " @echo off`r`n echo 1.14.46`r`n "
747+ Write-TestJsonDocument - Path $packageStateIndexPath - Document @ {
748+ records = @ (
749+ @ {
750+ installSlotId = ' OpenCodeCli:stable:win32-x64'
751+ definitionId = ' OpenCodeCli'
752+ releaseTrack = ' stable'
753+ artifactDistributionVariant = ' win32-x64'
754+ currentReleaseId = ' opencode-runtime-win32-x64-stable'
755+ currentVersion = ' 1.14.46'
756+ installDirectory = $oldInstallRoot
757+ ownershipKind = ' PackageInstalled'
758+ updatedAtUtc = [DateTime ]::UtcNow.ToString(' o' )
759+ }
760+ )
761+ }
762+
763+ $sharedInstall = @ {
764+ install = @ {
765+ kind = ' expandArchive'
766+ installDirectory = ' opencode-runtime/{releaseTrack}/{version}/{artifactDistributionVariant}'
767+ expandedRoot = ' auto'
768+ }
769+ versionUpdatePolicy = @ {
770+ whenAssigned = ' trackSelectedVersion'
771+ onSameSelectedVersion = ' reuseOrRepair'
772+ onNewSelectedVersion = ' fail'
773+ }
774+ }
775+ $readiness = New-TestReadiness - Version ' 1.15.7' - Directories @ ()
776+ $release = New-TestPackageRelease - Id ' opencode-runtime-win32-x64-stable' - Version ' 1.15.7' - Architecture ' x64' - ArtifactDistributionVariant ' win32-x64' - FileName ' opencode-1.15.7.zip' - Readiness $readiness
777+ $documents = Write-TestPackageDocuments - RootPath $rootPath - GlobalDocument (New-TestPackageGlobalDocument - PreferredTargetInstallDirectory $preferredInstallRoot - PackageAssignmentInventoryFilePath $packageStateIndexPath ) - DefinitionDocument (New-TestVSCodeDefinitionDocument - DefinitionId ' OpenCodeCli' - Releases @ ($release ) - SharedInstall $sharedInstall - SharedReadiness $readiness - SharedExistingInstallDiscovery (New-TestExistingInstallDiscovery - Enabled $false ))
778+
779+ [Environment ]::SetEnvironmentVariable($script :SourceInventoryEnvVarName , (Join-Path $TestDrive ' missing-inventory.json' ), ' Process' )
780+ Mock Get-PackageConfigPath { $documents.GlobalConfigPath }
781+ Mock Get-PackageDefinitionPath { param ($DefinitionId ) $documents.DefinitionPath }
782+
783+ $config = Get-PackageConfig - DefinitionId ' OpenCodeCli'
784+ $result = New-PackageResult - PackageConfig $config
785+ $result = Resolve-PackagePackage - PackageResult $result
786+ $result = Resolve-PackagePaths - PackageResult $result
787+ $result = Find-PackageExistingPackage - PackageResult $result
788+ $result = Set-PackageExistingPackage - PackageResult $result
789+
790+ { Resolve-PackageExistingPackageDecision - PackageResult $result } | Should - Throw - ExpectedMessage " *versionUpdatePolicy.onNewSelectedVersion='fail'*"
791+ }
792+
793+ It ' removes the replaced package-owned install directory after the new install is ready' {
794+ $rootPath = Join-Path $TestDrive ' opencode-replacement-cleanup'
795+ $preferredInstallRoot = Join-Path $rootPath ' installs'
796+ $oldInstallRoot = Join-Path $preferredInstallRoot ' opencode-runtime\stable\1.14.46\win32-x64'
797+ $newInstallRoot = Join-Path $preferredInstallRoot ' opencode-runtime\stable\1.15.7\win32-x64'
798+ $null = New-Item - ItemType Directory - Path $oldInstallRoot - Force
799+ $null = New-Item - ItemType Directory - Path $newInstallRoot - Force
800+ Write-TestTextFile - Path (Join-Path $oldInstallRoot ' old.txt' ) - Content ' old'
801+ Write-TestTextFile - Path (Join-Path $newInstallRoot ' new.txt' ) - Content ' new'
802+
803+ $result = [pscustomobject ]@ {
804+ ExistingPackage = [pscustomobject ]@ {
805+ Decision = ' ReplacePackageOwnedInstall'
806+ InstallDirectory = $oldInstallRoot
807+ }
808+ InstallDirectory = $newInstallRoot
809+ Readiness = [pscustomobject ]@ {
810+ Accepted = $true
811+ }
812+ Ownership = [pscustomobject ]@ {
813+ OwnershipRecord = [pscustomobject ]@ {
814+ ownershipKind = ' PackageInstalled'
815+ }
816+ }
817+ PackageConfig = [pscustomobject ]@ {
818+ PreferredTargetInstallRootDirectory = $preferredInstallRoot
819+ }
820+ }
821+
822+ $result = Remove-PackageReplacedPackageOwnedInstallDirectory - PackageResult $result
823+
824+ $result.ReplacementCleanup.Status | Should - Be ' Removed'
825+ Test-Path - LiteralPath $oldInstallRoot - PathType Container | Should - BeFalse
826+ Test-Path - LiteralPath $newInstallRoot - PathType Container | Should - BeTrue
827+ }
828+
607829 It ' discovers and reuses the current package target install path even when inventory is missing' {
608830 $rootPath = Join-Path $TestDrive ' reuse-managed-untracked'
609831 $installRoot = Join-Path $rootPath ' managed-install'
0 commit comments