real-credential-switch-acceptance.ps1 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. param(
  2. [string]$TrainingDir = 'D:\qiwei-training',
  3. [string]$TrialCredentialEnv = 'E:\workspace\QIWEI-skill\.env.local',
  4. [int]$Port = 4435
  5. )
  6. $ErrorActionPreference = 'Stop'
  7. function Read-EnvValue([string]$path, [string]$key) {
  8. $line = Get-Content -LiteralPath $path | Where-Object { $_ -match "^$([regex]::Escape($key))\s*=" } | Select-Object -Last 1
  9. if (-not $line) { return '' }
  10. return ($line -replace "^$([regex]::Escape($key))\s*=\s*", '').Trim().Trim('"').Trim("'")
  11. }
  12. function Wait-Http([string]$url, [int]$seconds = 30) {
  13. $deadline = (Get-Date).AddSeconds($seconds)
  14. do {
  15. try { return Invoke-RestMethod -Uri $url -TimeoutSec 5 }
  16. catch { Start-Sleep -Milliseconds 750 }
  17. } while ((Get-Date) -lt $deadline)
  18. throw "Timed out waiting for $url"
  19. }
  20. $trialToken = Read-EnvValue $TrialCredentialEnv 'QIWEI_AUTH_TOKEN'
  21. if (-not $trialToken.StartsWith('r:')) { throw 'Trial credential must be an r: session credential' }
  22. $root = Join-Path $env:TEMP ("qiwei-real-switch-" + [guid]::NewGuid().ToString('N'))
  23. $exe = Join-Path $root 'qiwei-workbench.exe'
  24. $envPath = Join-Path $root '.env.local'
  25. $process = $null
  26. New-Item -ItemType Directory -Path $root | Out-Null
  27. try {
  28. Copy-Item (Join-Path $TrainingDir 'qiwei-workbench.exe') $exe
  29. Copy-Item (Join-Path $TrainingDir 'qiwei.runtime.config.mjs') (Join-Path $root 'qiwei.runtime.config.mjs')
  30. Copy-Item (Join-Path $TrainingDir '.env.local') $envPath
  31. New-Item -ItemType Directory -Path (Join-Path $root 'web') | Out-Null
  32. Copy-Item (Join-Path $TrainingDir 'web\*') (Join-Path $root 'web')
  33. New-Item -ItemType Directory -Path (Join-Path $root 'knowledge') | Out-Null
  34. Copy-Item (Join-Path $TrainingDir 'knowledge\*') (Join-Path $root 'knowledge') -Recurse
  35. $oldUid = Read-EnvValue $envPath 'QIWEI_UID'
  36. $oldGuid = Read-EnvValue $envPath 'QIWEI_GUID'
  37. if (-not $oldUid) { throw 'Formal fixture is missing uid' }
  38. $process = Start-Process -FilePath $exe -ArgumentList 'dashboard','--port',"$Port" -WorkingDirectory $root -PassThru -WindowStyle Hidden
  39. $before = Wait-Http "http://127.0.0.1:$Port/api/status"
  40. $body = @{ token = $trialToken } | ConvertTo-Json -Compress
  41. $switch = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/auth/token" -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 45
  42. Start-Sleep -Seconds 2
  43. $health = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/health" -TimeoutSec 10
  44. $after = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/status" -TimeoutSec 30
  45. $healthAfterStatus = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/api/health" -TimeoutSec 10
  46. $newUid = Read-EnvValue $envPath 'QIWEI_UID'
  47. $newGuid = Read-EnvValue $envPath 'QIWEI_GUID'
  48. $savedToken = Read-EnvValue $envPath 'QIWEI_AUTH_TOKEN'
  49. $diagnostic = [ordered]@{
  50. endpointStatus = $switch.status
  51. accountReset = [bool]$switch.summary.accountReset
  52. oldUidPresent = [bool]$oldUid
  53. oldGuidPresent = [bool]$oldGuid
  54. uidRotated = [bool]$newUid -and $newUid -ne $oldUid
  55. newGuidPresent = [bool]$newGuid
  56. activeUidPresent = [bool]$health.data.activeAccountUid
  57. switchedSource = $after.data.subscription.summary.source
  58. credentialPersisted = $savedToken -eq $trialToken
  59. }
  60. if ($switch.status -ne 'ok') { throw "Credential switch endpoint did not succeed: $($diagnostic | ConvertTo-Json -Compress)" }
  61. if ($switch.summary.accountReset -ne $true) { throw "Cross-account switch did not reset the old device: $($diagnostic | ConvertTo-Json -Compress)" }
  62. if (-not $newUid -or $newUid -eq $oldUid -or $newGuid) { throw "Old uid/guid remain in package env: $($diagnostic | ConvertTo-Json -Compress)" }
  63. if ($health.data.activeAccountUid) { throw "Old uid remains active in the long-lived dashboard: $($diagnostic | ConvertTo-Json -Compress)" }
  64. if ($savedToken -ne $trialToken) { throw "Trial credential was not persisted: $($diagnostic | ConvertTo-Json -Compress)" }
  65. if ($after.data.subscription.summary.source -ne 'trial') { throw "Switched credential is not classified as trial: $($diagnostic | ConvertTo-Json -Compress)" }
  66. [ordered]@{
  67. initialSource = $before.data.subscription.summary.source
  68. initialUidPresent = [bool]$oldUid
  69. initialGuidPresent = [bool]$oldGuid
  70. endpointStatus = $switch.status
  71. accountReset = [bool]$switch.summary.accountReset
  72. switchedSource = $after.data.subscription.summary.source
  73. trialActive = [bool]$after.data.subscription.summary.trialActive
  74. seats = $after.data.subscription.summary.seats
  75. usedSeats = $after.data.subscription.summary.usedSeats
  76. persistedCredentialKind = if ($savedToken.StartsWith('r:')) { 'session' } else { 'other' }
  77. uidRotated = [bool]$newUid -and $newUid -ne $oldUid
  78. guidCleared = -not [bool]$newGuid
  79. activeUidCleared = -not [bool]$health.data.activeAccountUid
  80. freshUidActivatedAfterStatus = $healthAfterStatus.data.activeAccountUid -eq $newUid
  81. upstreamDeviceConfigured = [bool]$after.data.login.summary.configured
  82. loginStarted = $false
  83. } | ConvertTo-Json -Depth 6
  84. } finally {
  85. if ($process -and -not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue }
  86. Get-CimInstance Win32_Process |
  87. Where-Object { $_.ExecutablePath -eq $exe } |
  88. ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
  89. Start-Sleep -Milliseconds 500
  90. $resolvedRoot = [IO.Path]::GetFullPath($root)
  91. $resolvedTemp = [IO.Path]::GetFullPath($env:TEMP)
  92. if ($resolvedRoot.StartsWith($resolvedTemp, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path $resolvedRoot)) {
  93. Remove-Item -LiteralPath $resolvedRoot -Recurse -Force
  94. }
  95. }