[CmdletBinding()] param() Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repositoryRoot = Split-Path -Parent $PSScriptRoot $modulePath = Join-Path $repositoryRoot 'scripts\XasSkills.psm1' if (-not (Test-Path -LiteralPath $modulePath -PathType Leaf)) { throw "Installation module does not exist: $modulePath" } Import-Module $modulePath -Force $installationModule = Get-Module | Where-Object { $_.Path -eq $modulePath } | Select-Object -First 1 $testRoot = Join-Path ([IO.Path]::GetTempPath()) ('xas-install-tests-' + [guid]::NewGuid().ToString('N')) $null = New-Item -ItemType Directory -Path $testRoot $script:TestCount = 0 $script:FailureCount = 0 $script:SkipCount = 0 $script:CaseCount = 0 $script:TestLinks = New-Object 'System.Collections.Generic.List[string]' $utf8WithoutBom = New-Object Text.UTF8Encoding($false) function Write-TestFile { param([string]$Path, [string]$Content) $parent = Split-Path -Parent $Path if (-not (Test-Path -LiteralPath $parent)) { $null = New-Item -ItemType Directory -Path $parent -Force } [IO.File]::WriteAllText($Path, $Content, $utf8WithoutBom) } function New-TestContext { $script:CaseCount++ $unicodeName = ([string][char]0x4E2D) + ([string][char]0x6587) + ' space' $root = Join-Path $testRoot (('{0:D2}-' -f $script:CaseCount) + $unicodeName) $source = Join-Path $root 'source checkout' $null = New-Item -ItemType Directory -Path (Join-Path $source 'skills') -Force [pscustomobject]@{ Root = $root RepositoryRoot = $source SkillsRoot = Join-Path $root 'personal skills' } } function New-TestSkill { param($Context, [string]$Name = 'xas-one') $skillPath = Join-Path (Join-Path $Context.RepositoryRoot 'skills') $Name $markdown = "---`nname: $Name`ndescription: A self-contained installation test skill.`n---`n`n# Test skill`n`nRead the bundled references before proceeding.`n" Write-TestFile (Join-Path $skillPath 'SKILL.md') $markdown Write-TestFile (Join-Path $skillPath 'references\guide.md') 'original guide' Write-TestFile (Join-Path $skillPath 'scripts\example.ps1') "Write-Output 'fixture only'" Write-TestFile (Join-Path $skillPath 'assets\.hidden-fixture') 'hidden resource' $hiddenPath = Join-Path $skillPath 'assets\.hidden-fixture' [IO.File]::SetAttributes($hiddenPath, [IO.FileAttributes]::Hidden) $null = New-Item -ItemType Directory -Path (Join-Path $skillPath 'assets\empty directory') -Force $skillPath } function Assert-True { param([bool]$Condition, [string]$Message) if (-not $Condition) { throw $Message } } function Assert-Equal { param($Actual, $Expected, [string]$Message) if ($Actual -cne $Expected) { throw ($Message + ". Expected: [$Expected]; actual: [$Actual].") } } function Assert-Throws { param([scriptblock]$Action, [string]$Message) $didThrow = $false try { $null = & $Action } catch { $didThrow = $true } Assert-True $didThrow $Message } function Get-TestSnapshot { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return '' } $base = [IO.Path]::GetFullPath($Path).TrimEnd('\', '/') $lines = foreach ($item in (Get-ChildItem -LiteralPath $Path -Recurse -Force | Sort-Object FullName)) { $relative = $item.FullName.Substring($base.Length + 1).Replace('\', '/') if ($item.PSIsContainer) { 'D:' + $relative } else { 'F:' + $relative + ':' + (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash } } (@($lines) -join "`n") } function Invoke-Test { param([string]$Name, [scriptblock]$Action) $script:TestCount++ try { & $Action Write-Host ('PASS ' + $Name) } catch { $script:FailureCount++ Write-Host ('FAIL ' + $Name + ': ' + $_.Exception.Message) -ForegroundColor Red Write-Host $_.ScriptStackTrace } } function Invoke-TestEntryPoint { param([string]$ScriptPath, [string]$Arguments, [string]$ProfilePath) $executableName = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' } $literalPath = $ScriptPath.Replace("'", "''") $command = "`$ErrorActionPreference = 'Stop'; & '$literalPath' $Arguments" $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) $startInfo = New-Object Diagnostics.ProcessStartInfo $startInfo.FileName = Join-Path $PSHOME $executableName $startInfo.Arguments = '-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ' + $encoded $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $true $startInfo.RedirectStandardOutput = $true $startInfo.RedirectStandardError = $true $startInfo.EnvironmentVariables['USERPROFILE'] = $ProfilePath $process = New-Object Diagnostics.Process $process.StartInfo = $startInfo $null = $process.Start() $outputTask = $process.StandardOutput.ReadToEndAsync() $errorTask = $process.StandardError.ReadToEndAsync() $process.WaitForExit() $output = $outputTask.GetAwaiter().GetResult() $errorOutput = $errorTask.GetAwaiter().GetResult() $result = [pscustomobject]@{ ExitCode = $process.ExitCode; Output = $output; Error = $errorOutput } $process.Dispose() $result } try { Invoke-Test 'catalog uses direct children and excludes templates' { $context = New-TestContext $one = New-TestSkill $context 'xas-one' $null = New-TestSkill $context 'xas-two' Write-TestFile (Join-Path $context.RepositoryRoot 'templates\skill-template\SKILL.md') "---`nname: xas-template`ndescription: Template only.`n---`n" Write-TestFile (Join-Path $context.RepositoryRoot 'skills\nested-holder\nested-skill\SKILL.md') "---`nname: nested-skill`ndescription: Nested fixture.`n---`n" $catalog = @(Get-XasSkillCatalog -RepositoryRoot $context.RepositoryRoot) Assert-Equal $catalog.Count 2 'Only the two direct skills should be discovered' Assert-Equal ((@($catalog.Name | Sort-Object)) -join ',') 'xas-one,xas-two' 'Catalog names should match' $entry = $catalog | Where-Object Name -eq 'xas-one' Assert-Equal $entry.Path $one 'Catalog should return the full source path' Assert-True (-not [string]::IsNullOrWhiteSpace($entry.Description)) 'Catalog description should be populated' } Invoke-Test 'catalog accepts comments, quoted strings and nonempty description blocks' { $examples = @( @{ Body = "name: xas-one # name comment`ndescription: A plain description # description comment"; Expected = 'A plain description' }, @{ Body = "name: 'xas-one' # comment`ndescription: 'It''s a # quoted description' # comment"; Expected = "It's a # quoted description" }, @{ Body = ('name: "xas-one" # comment' + "`n" + 'description: "Use \"quotes\", # markers and C:\\work\\notes" # comment'); Expected = 'Use "quotes", # markers and C:\work\notes' }, @{ Body = "name: xas-one`ndescription: Document#fragment"; Expected = 'Document#fragment' }, @{ Body = "name: xas-one`ndescription: '123'"; Expected = '123' }, @{ Body = "name: xas-one`ndescription: | # literal block`n First line`n Second line"; Expected = "First line`nSecond line`n" }, @{ Body = "name: xas-one`ndescription: |-`n First line`n Second line"; Expected = "First line`nSecond line" }, @{ Body = "name: xas-one`ndescription: >`n First line`n continues here"; Expected = "First line continues here`n" }, @{ Body = "name: xas-one`ndescription: >- # folded block`n First line`n continues here`n`n Next paragraph"; Expected = "First line continues here`nNext paragraph" } ) foreach ($example in $examples) { $context = New-TestContext $source = New-TestSkill $context Write-TestFile (Join-Path $source 'SKILL.md') ("---`n" + $example.Body + "`n---`n`n# Fixture`n") $catalog = @(Get-XasSkillCatalog -RepositoryRoot $context.RepositoryRoot) Assert-Equal $catalog.Count 1 'Valid scalar syntax should remain discoverable' Assert-Equal $catalog[0].Name 'xas-one' 'YAML comments and quotes should not become part of the skill name' Assert-Equal $catalog[0].Description $example.Expected ('Description should be parsed correctly: ' + $example.Body) } } Invoke-Test 'catalog rejects empty, non-string, duplicate and unsupported metadata' { $bodies = @( "name: xas-one`ndescription: null", "name: xas-one`ndescription: ~", "name: xas-one`ndescription: true", "name: xas-one`ndescription: False", "name: xas-one`ndescription: yes", "name: xas-one`ndescription: 123", "name: xas-one`ndescription: -1.5", "name: xas-one`ndescription: 1e3", "name: xas-one`ndescription: 0xFF", "name: xas-one`ndescription: |", "name: xas-one`ndescription: >- # empty block`n ", "name: xas-one`nname: xas-one`ndescription: Description", "name: xas-one`ndescription: First`ndescription: Second", "name: xas-one`ndescription: [one, two]", "name: xas-one`ndescription: &anchor Description", "name: xas-one`ndescription: *alias", "name: xas-one`ndescription: !!str Description", "name: xas-one`ndescription: |2`n Description", ('name: xas-one' + "`n" + 'description: "\x41"'), "name: xas-one`ndescription: First line`n unsupported continuation" ) foreach ($body in $bodies) { $context = New-TestContext $source = New-TestSkill $context Write-TestFile (Join-Path $source 'SKILL.md') ("---`n" + $body + "`n---`n`n# Fixture`n") Assert-Throws { Get-XasSkillCatalog -RepositoryRoot $context.RepositoryRoot } ('Invalid or unsupported YAML metadata should be rejected: ' + $body) } } Invoke-Test 'complete resource copy, managed state and repeated install' { $context = New-TestContext $source = New-TestSkill $context $result = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one')) Assert-Equal $result.Count 1 'Installation should return one result' Assert-Equal $result[0].Status 'Installed' 'First install should report Installed' $destination = Join-Path $context.SkillsRoot 'xas-one' foreach ($file in @('SKILL.md', 'references\guide.md', 'scripts\example.ps1', 'assets\.hidden-fixture')) { Assert-Equal (Get-FileHash -LiteralPath (Join-Path $destination $file)).Hash (Get-FileHash -LiteralPath (Join-Path $source $file)).Hash "Resource should be copied: $file" } Assert-True (Test-Path -LiteralPath (Join-Path $destination 'assets\empty directory') -PathType Container) 'Empty resource directory should be copied' $state = Get-Content -LiteralPath (Join-Path $destination '.xas-install.json') -Raw | ConvertFrom-Json Assert-True ($null -ne $state.schemaVersion) 'Managed state should have a schema version' Assert-Equal $state.repository 'https://code.it.aisuan.cn/wenhuadian/xas-skills.git' 'Managed state should use the company repository' Assert-Equal $state.skill 'xas-one' 'Managed state should identify the skill' Assert-True (@($state.files).Count -ge 4) 'Managed state should inventory resource files' Assert-True (@($state.directories).Count -ge 3) 'Managed state should inventory directories' foreach ($file in $state.files) { Assert-True (-not [string]::IsNullOrWhiteSpace($file.path)) 'Inventory file should have a path' Assert-True ($file.sha256 -match '^[a-fA-F0-9]{64}$') 'Inventory file should have a SHA-256 hash' } $before = Get-TestSnapshot $context.SkillsRoot $again = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one')) Assert-Equal $again[0].Status 'Skipped' 'Identical install should be skipped' Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before 'Skipped installation should not change files' Assert-Equal ((@(Get-XasInstalledSkillNames -SkillsRoot $context.SkillsRoot)) -join ',') 'xas-one' 'Installed skill discovery should find managed skill' } Invoke-Test 'selection and arbitrary current directory' { $context = New-TestContext $null = New-TestSkill $context 'xas-one' $null = New-TestSkill $context 'xas-two' Push-Location ([IO.Path]::GetTempPath()) try { $result = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-two')) } finally { Pop-Location } Assert-Equal $result[0].Name 'xas-two' 'Selected skill should be installed' Assert-True (-not (Test-Path -LiteralPath (Join-Path $context.SkillsRoot 'xas-one'))) 'Unselected skill should remain absent' Assert-True (Test-Path -LiteralPath (Join-Path $context.SkillsRoot 'xas-two\SKILL.md')) 'Selected skill should be present' } Invoke-Test 'unmanaged destination collision is preserved' { $context = New-TestContext $null = New-TestSkill $context Write-TestFile (Join-Path $context.SkillsRoot 'xas-one\keep.txt') 'belongs to somebody else' $before = Get-TestSnapshot $context.SkillsRoot Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') } 'Unmanaged destination must be rejected' Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before 'Unmanaged destination must be preserved' } Invoke-Test 'foreign repository state and malformed state both block replacement' { foreach ($change in @('foreign-source', 'malformed-json')) { $context = New-TestContext $null = New-TestSkill $context $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') $recordPath = Join-Path $context.SkillsRoot 'xas-one\.xas-install.json' if ($change -eq 'foreign-source') { $state = Get-Content -LiteralPath $recordPath -Raw | ConvertFrom-Json $state.repository = 'https://example.invalid/another-team/skills.git' Write-TestFile $recordPath ($state | ConvertTo-Json -Depth 8) } else { Write-TestFile $recordPath '{ invalid json' } $before = Get-TestSnapshot $context.SkillsRoot Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') } "Installation should reject $change state" Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before "Rejected $change state and installed files must remain unchanged" } } foreach ($localChange in @('modified', 'added', 'deleted', 'empty-directory-added', 'empty-directory-deleted')) { Invoke-Test ("local $localChange resources block update") { $context = New-TestContext $source = New-TestSkill $context $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') $destination = Join-Path $context.SkillsRoot 'xas-one' switch ($localChange) { 'modified' { Write-TestFile (Join-Path $destination 'references\guide.md') 'user edit' } 'added' { Write-TestFile (Join-Path $destination 'user-added.txt') 'user note' } 'deleted' { Remove-Item -LiteralPath (Join-Path $destination 'references\guide.md') } 'empty-directory-added' { $null = New-Item -ItemType Directory -Path (Join-Path $destination 'user empty folder') } 'empty-directory-deleted' { Remove-Item -LiteralPath (Join-Path $destination 'assets\empty directory') } } Write-TestFile (Join-Path $source 'references\guide.md') 'upstream update' $before = Get-TestSnapshot $context.SkillsRoot Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') } "Local $localChange resources must block update" Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before 'Local state must survive the failed update' } } Invoke-Test 'update replaces changed content and removes deleted upstream resources' { $context = New-TestContext $source = New-TestSkill $context $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') Remove-Item -LiteralPath (Join-Path $source 'scripts\example.ps1') Remove-Item -LiteralPath (Join-Path $source 'assets\empty directory') Write-TestFile (Join-Path $source 'references\guide.md') 'updated guide' Write-TestFile (Join-Path $source 'assets\new-resource.txt') 'new upstream resource' $result = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one')) Assert-Equal $result[0].Status 'Updated' 'Changed source should report Updated' $destination = Join-Path $context.SkillsRoot 'xas-one' Assert-Equal ([IO.File]::ReadAllText((Join-Path $destination 'references\guide.md'))) 'updated guide' 'Changed resource should be replaced' Assert-True (-not (Test-Path -LiteralPath (Join-Path $destination 'scripts\example.ps1'))) 'Upstream deleted file should be removed' Assert-True (-not (Test-Path -LiteralPath (Join-Path $destination 'assets\empty directory'))) 'Upstream deleted empty directory should be removed' Assert-True (Test-Path -LiteralPath (Join-Path $destination 'assets\new-resource.txt')) 'New upstream file should be copied' $again = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one')) Assert-Equal $again[0].Status 'Skipped' 'Updated installation state should validate on the next run' } Invoke-Test 'batch preflight prevents earlier skill writes when a later skill conflicts' { $context = New-TestContext $null = New-TestSkill $context 'xas-one' $null = New-TestSkill $context 'xas-two' $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') Write-TestFile (Join-Path $context.SkillsRoot 'xas-one\references\guide.md') 'user edit' $before = Get-TestSnapshot $context.SkillsRoot Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-two', 'xas-one') } 'Batch should fail preflight on conflicting second skill' Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before 'Batch preflight failure should perform no writes' } Invoke-Test 'preview install and preview update do not write' { $context = New-TestContext $source = New-TestSkill $context $before = Get-TestSnapshot $context.Root $result = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') -Preview) Assert-Equal $result[0].Status 'WouldInstall' 'Preview should report WouldInstall' Assert-Equal (Get-TestSnapshot $context.Root) $before 'Preview install should not create even the target root' $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') Write-TestFile (Join-Path $source 'references\guide.md') 'updated guide' $before = Get-TestSnapshot $context.Root $result = @(Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') -Preview) Assert-Equal $result[0].Status 'WouldUpdate' 'Preview should report WouldUpdate' Assert-Equal (Get-TestSnapshot $context.Root) $before 'Preview update should not change files' } Invoke-Test 'invalid names and overlapping roots are rejected' { $context = New-TestContext $source = New-TestSkill $context foreach ($name in @('../xas-one', '..\xas-one', 'xas-one/child', '.', 'missing-skill', $source)) { Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @($name) } "Invalid or unknown skill name must be rejected: $name" } foreach ($target in @((Join-Path $context.RepositoryRoot 'skills'), (Join-Path $source 'nested target'), $context.Root)) { Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $target -Skill @('xas-one') } "Overlapping roots must be rejected: $target" } Assert-True (-not (Test-Path -LiteralPath $context.SkillsRoot)) 'Invalid requests should not create target root' } Invoke-Test 'source directory link is rejected' { $context = New-TestContext $source = New-TestSkill $context $external = Join-Path $context.Root 'external fixture' $null = New-Item -ItemType Directory -Path $external Write-TestFile (Join-Path $external 'outside.txt') 'not a bundled resource' $link = Join-Path $source 'assets\linked directory' try { $null = New-Item -ItemType Junction -Path $link -Target $external -ErrorAction Stop $script:TestLinks.Add($link) } catch { $script:SkipCount++ Write-Host ('SKIP Junction fixture unavailable: ' + $_.Exception.Message) return } Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') } 'Linked source resource must be rejected' Assert-True (-not (Test-Path -LiteralPath $context.SkillsRoot)) 'Rejected source link should not create target root' Assert-Equal ([IO.File]::ReadAllText((Join-Path $external 'outside.txt'))) 'not a bundled resource' 'Link target must remain unchanged' } Invoke-Test 'replacement failure restores the old installed skill' { $context = New-TestContext $source = New-TestSkill $context $null = Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') $before = Get-TestSnapshot $context.SkillsRoot Write-TestFile (Join-Path $source 'references\guide.md') 'new upstream version' & $installationModule { $script:XasTestOriginalMove = (Get-Item Function:Move-XasDirectory).ScriptBlock $script:XasTestMoveCount = 0 Set-Item -Path Function:script:Move-XasDirectory -Value { param([string]$Source, [string]$Destination) $script:XasTestMoveCount++ if ($script:XasTestMoveCount -eq 2) { throw 'Injected failure while replacing installed skill.' } & $script:XasTestOriginalMove -Source $Source -Destination $Destination } } try { Assert-Throws { Invoke-XasInstall -RepositoryRoot $context.RepositoryRoot -SkillsRoot $context.SkillsRoot -Skill @('xas-one') } 'Injected replacement failure should be reported' } finally { & $installationModule { Set-Item -Path Function:script:Move-XasDirectory -Value $script:XasTestOriginalMove Remove-Variable -Scope Script -Name XasTestOriginalMove, XasTestMoveCount } } Assert-Equal (Get-TestSnapshot $context.SkillsRoot) $before 'Replacement failure should restore old files and state without leftovers' } Invoke-Test 'install entry point lists and previews with an isolated user profile' { $context = New-TestContext $null = New-TestSkill $context 'xas-one' $installerPath = Join-Path $repositoryRoot 'install.ps1' Assert-True (Test-Path -LiteralPath $installerPath -PathType Leaf) 'Root install.ps1 should exist' $null = New-Item -ItemType Directory -Path (Join-Path $context.RepositoryRoot 'scripts') -Force Copy-Item -LiteralPath $modulePath -Destination (Join-Path $context.RepositoryRoot 'scripts\XasSkills.psm1') Copy-Item -LiteralPath $installerPath -Destination (Join-Path $context.RepositoryRoot 'install.ps1') $profilePath = Join-Path $context.Root 'isolated profile' $null = New-Item -ItemType Directory -Path $profilePath $before = Get-TestSnapshot $context.RepositoryRoot $listed = Invoke-TestEntryPoint -ScriptPath (Join-Path $context.RepositoryRoot 'install.ps1') -Arguments '-List' -ProfilePath $profilePath Assert-Equal $listed.ExitCode 0 ('List entry point failed: ' + $listed.Error) Assert-True ($listed.Output -match 'xas-one') 'List entry point should include fixture skill' $previewed = Invoke-TestEntryPoint -ScriptPath (Join-Path $context.RepositoryRoot 'install.ps1') -Arguments '-Skill xas-one -WhatIf' -ProfilePath $profilePath Assert-Equal $previewed.ExitCode 0 ('WhatIf entry point failed: ' + $previewed.Error) Assert-True ($previewed.Output -match 'xas-one') 'WhatIf entry point should identify the planned skill' Assert-Equal (Get-TestSnapshot $context.RepositoryRoot) $before 'List and WhatIf entry points should not change the source checkout' Assert-True (-not (Test-Path -LiteralPath (Join-Path $profilePath '.agents'))) 'List and WhatIf entry points should not create the skills destination' } } finally { foreach ($link in $script:TestLinks) { if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } } $resolvedRoot = [IO.Path]::GetFullPath($testRoot) $resolvedTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar if (-not $resolvedRoot.StartsWith($resolvedTemp, [StringComparison]::OrdinalIgnoreCase) -or -not ([IO.Path]::GetFileName($resolvedRoot)).StartsWith('xas-install-tests-', [StringComparison]::Ordinal)) { throw "Refusing to clean unexpected test path: $resolvedRoot" } if (Test-Path -LiteralPath $resolvedRoot) { Remove-Item -LiteralPath $resolvedRoot -Recurse -Force } } Write-Host ("Tests: $script:TestCount; failures: $script:FailureCount; skipped link checks: $script:SkipCount") if ($script:FailureCount -gt 0) { exit 1 } exit 0