#requires -Version 7.0
param([ValidateSet('Debug','Release')][string]$Configuration = 'Release', [string]$Package, [switch]$UseLaunchProfiles, [switch]$NoBuild, [switch]$KeepOpen, [switch]$VerifyDialogs, [ValidateSet('zh','en','de','es','fr','pl','ru','uk')][string]$UiLanguage = 'zh', [string]$NativeCardManifest)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$taskRun = Join-Path $taskRoot ('artifacts/desktop-smoke-' + [Guid]::NewGuid().ToString('N'))
$taskIntermediate = Join-Path $taskRoot ('artifacts/test-obj/' + [Guid]::NewGuid().ToString('N').Substring(0, 12))
$taskRuntime = Join-Path $taskRun 'Server/runtime'
New-Item -ItemType Directory -Path $taskRuntime -Force | Out-Null
$taskLaunchProfiles = @{}
$taskVisibleProcesses = @()
$taskCompleted = $false
if ($UseLaunchProfiles -and $Package) { throw 'Choose source launch profiles or a published package, not both.' }
if ($NoBuild -and -not $UseLaunchProfiles) { throw '-NoBuild requires -UseLaunchProfiles.' }
if ($Package) {
$taskServerExe = Join-Path $Package 'Server/XFEWarframe.Server.exe'
$taskAdminExe = Join-Path $Package 'Admin/XFEWarframe.Admin.exe'
$taskPlayerExe = Join-Path $Package 'Client/XFEWarframe.Player.exe'
} elseif ($UseLaunchProfiles) {
if (-not $NoBuild) {
dotnet build (Join-Path $taskRoot 'XFEWarframe.slnx') -c $Configuration -p:Platform=x64 --nologo -v quiet
if ($LASTEXITCODE -ne 0) { throw "Solution $Configuration/x64 build failed" }
}
foreach ($taskSpec in @(
@{Role='Server';Project='Server/XFEWarframe.Server';Platform='AnyCPU'},
@{Role='Admin';Project='Admin/XFEWarframe.Admin';Platform='x64'},
@{Role='Player';Project='Client/XFEWarframe.Player';Platform='x64'}
)) {
$taskProjectDirectory = Join-Path $taskRoot $taskSpec.Project
$taskName = Split-Path $taskSpec.Project -Leaf
$taskPropertiesText = dotnet msbuild (Join-Path $taskProjectDirectory "$taskName.csproj") "-p:Configuration=$Configuration" "-p:Platform=$($taskSpec.Platform)" -getProperty:RunCommand,RunWorkingDirectory,ActiveDebugProfile,PlatformTarget,WindowsPackageType -nologo
if ($LASTEXITCODE -ne 0) { throw "Cannot evaluate $taskName debug target" }
$taskProperties = ($taskPropertiesText | ConvertFrom-Json).Properties
$taskProfiles = (Get-Content (Join-Path $taskProjectDirectory 'Properties/launchSettings.json') -Raw | ConvertFrom-Json -AsHashtable).profiles
$taskProfileName = if ($taskProperties.ActiveDebugProfile) { $taskProperties.ActiveDebugProfile } else { @($taskProfiles.Keys)[0] }
$taskProfile = $taskProfiles[$taskProfileName]
if ($taskProfile.commandName -ne 'Project') { throw "$taskName is not configured to launch its executable" }
if (-not (Test-Path -LiteralPath $taskProperties.RunCommand -PathType Leaf)) { throw "Debug executable missing: $($taskProperties.RunCommand)" }
if ($taskSpec.Role -ne 'Server' -and ($taskProperties.PlatformTarget -ne 'x64' -or $taskProperties.WindowsPackageType -ne 'None')) { throw 'WinUI must launch unpackaged on x64' }
if ($taskSpec.Role -in @('Server','Admin')) {
if ($taskProfile.commandLineArgs -notmatch '--root=([^ ]+)') { throw 'Missing server runtime argument' }
$taskConfiguredRoot = [IO.Path]::GetFullPath((Join-Path $taskProperties.RunWorkingDirectory $Matches[1]))
if ($taskConfiguredRoot -ne (Join-Path $taskRoot 'Server/runtime')) { throw 'Admin and Server debug profiles must use the same runtime directory' }
} else {
if ($taskProfile.commandLineArgs -notmatch '--payload=([^ ]+)') { throw 'Missing player payload argument' }
$taskPayloadRoot = [IO.Path]::GetFullPath((Join-Path $taskProperties.RunWorkingDirectory $Matches[1]))
if ($taskPayloadRoot -ne (Join-Path $taskRoot 'artifacts/bridge')) { throw 'Incorrect developer bridge path' }
}
$taskLaunchProfiles[$taskSpec.Role] = @{Executable=$taskProperties.RunCommand; WorkingDirectory=$taskProperties.RunWorkingDirectory; Arguments=$taskProfile.commandLineArgs; Profile=$taskProfileName}
}
$taskServerExe = $taskLaunchProfiles.Server.Executable
$taskAdminExe = $taskLaunchProfiles.Admin.Executable
$taskPlayerExe = $taskLaunchProfiles.Player.Executable
$taskLaunchProfiles | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $taskRun 'launch-profiles.json') -Encoding utf8
} else {
foreach ($taskProject in @('Server/XFEWarframe.Server','Admin/XFEWarframe.Admin','Client/XFEWarframe.Player')) {
$taskName = Split-Path $taskProject -Leaf
$taskBuildOutput = Join-Path $taskRun "build/$taskName"
dotnet build (Join-Path $taskRoot "$taskProject/$taskName.csproj") -c $Configuration ("-p:XFEIntermediateRoot=" + $taskIntermediate) ("-p:OutputPath=" + $taskBuildOutput + [IO.Path]::DirectorySeparatorChar) --nologo -v quiet
if ($LASTEXITCODE -ne 0) { throw "Build failed: $taskName" }
}
$taskServerExe = Join-Path $taskRun 'build/XFEWarframe.Server/XFEWarframe.Server.exe'
$taskAdminExe = Join-Path $taskRun 'build/XFEWarframe.Admin/XFEWarframe.Admin.exe'
$taskPlayerExe = Join-Path $taskRun 'build/XFEWarframe.Player/XFEWarframe.Player.exe'
}
$taskProbe = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$taskProbe.Start()
$taskPort = $taskProbe.LocalEndpoint.Port
$taskProbe.Stop()
$taskProbe.Start()
$taskGamePort = $taskProbe.LocalEndpoint.Port
$taskProbe.Stop()
$taskOrigin = "http://127.0.0.1:$taskPort"
$taskServerArguments = @('"--root=' + $taskRuntime + '"', "--port=$taskPort", "--game-port=$taskGamePort", '--no-irc')
$taskServerDirectory = Split-Path $taskServerExe
if ($UseLaunchProfiles) {
# The isolated root comes first so the profile cannot touch production state.
$taskServerArguments += $taskLaunchProfiles.Server.Arguments
$taskServerDirectory = $taskLaunchProfiles.Server.WorkingDirectory
}
$taskServerProcess = Start-Process -FilePath $taskServerExe -WorkingDirectory $taskServerDirectory -ArgumentList $taskServerArguments -WindowStyle Hidden -PassThru -RedirectStandardOutput (Join-Path $taskRun 'server-stdout.log') -RedirectStandardError (Join-Path $taskRun 'server-stderr.log')
try {
$taskReady = $false
for ($i = 0; $i -lt 60; $i++) {
if ($taskServerProcess.HasExited) { throw 'Isolated server exited before readiness' }
try { $null = Invoke-RestMethod "$taskOrigin/health" -TimeoutSec 1; $taskReady = $true; break } catch { Start-Sleep -Milliseconds 250 }
}
if (-not $taskReady) { throw 'Isolated server was not ready' }
$taskConfigPath = Join-Path $taskRuntime 'server.json'
$taskConfig = Get-Content -LiteralPath $taskConfigPath -Raw | ConvertFrom-Json
# --port is an in-memory override; write the test copy so Admin can discover this ephemeral listener.
$taskConfig.ListenUrls = @("$taskOrigin/")
$taskConfig.PublicUrl = $taskOrigin
$taskConfig | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $taskConfigPath -Encoding utf8
$taskHeaders = @{ Authorization = 'Bearer ' + $taskConfig.AdminToken }
$taskBody = @{ displayName = 'UI Verification'; email = 'ui@example.invalid'; password = [Guid]::NewGuid().ToString('N') } | ConvertTo-Json
$null = Invoke-RestMethod "$taskOrigin/control/v1/accounts" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskBody
$taskPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskAllowed = @{ experience=$true; 'mod.rank'=$true; riven=$true; batch=$true; quest=$true; 'create.quest'=$true; 'archive.export'=$true; 'archive.import'=$true; 'inbox.read'=$true; syndicate=$true }
foreach ($taskField in $taskPermissions.fields | Where-Object { $_.key.StartsWith('unlock.', [StringComparison]::Ordinal) }) { $taskAllowed[$taskField.key] = $true }
$taskEditorBody = @{ version = $taskPermissions.version; values = $taskAllowed } | ConvertTo-Json -Depth 6
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskEditorBody
foreach ($taskRole in @('Admin','Player')) {
$taskExe = if ($taskRole -eq 'Admin') { $taskAdminExe } else { $taskPlayerExe }
$taskReport = Join-Path $taskRun "$taskRole.json"
$taskProfile = Join-Path $taskRun "$taskRole/ProfilesRoot"
$taskArguments = @('"--verify-ui=' + $taskReport + '"', '"--profile-root=' + $taskProfile + '"', '"--root=' + $taskRuntime + '"')
$taskArguments += "--verify-api=$taskOrigin", "--ui-language=$UiLanguage"
if ($NativeCardManifest) { $taskArguments += ('"--verify-native-card-manifest=' + [IO.Path]::GetFullPath($NativeCardManifest) + '"') }
if ($KeepOpen) { $taskArguments += '--verify-visible=true' }
if ($VerifyDialogs) { $taskArguments += '--verify-dialogs=true' }
$taskWorkingDirectory = Split-Path $taskExe
if ($UseLaunchProfiles) {
$taskArguments += $taskLaunchProfiles[$taskRole].Arguments
$taskWorkingDirectory = $taskLaunchProfiles[$taskRole].WorkingDirectory
}
$taskUiProcess = Start-Process -FilePath $taskExe -WorkingDirectory $taskWorkingDirectory -ArgumentList $taskArguments -WindowStyle Hidden -PassThru
$taskUiAccepted = $false
try {
if ($KeepOpen) {
$taskDeadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-Path -LiteralPath $taskReport) -and -not $taskUiProcess.HasExited -and [DateTime]::UtcNow -lt $taskDeadline) { Start-Sleep -Milliseconds 200 }
} elseif (-not $taskUiProcess.WaitForExit(90000)) { throw "$taskRole UI verification timed out" }
if (-not (Test-Path -LiteralPath $taskReport)) { throw "$taskRole did not create a verification report (exit $($taskUiProcess.ExitCode))" }
$taskResult = Get-Content -LiteralPath $taskReport -Raw | ConvertFrom-Json
if (-not $taskResult.passed) { throw "$taskRole verification failed: $($taskResult.error)" }
if ($KeepOpen) { $taskVisibleProcesses += @{ role = $taskRole; pid = $taskUiProcess.Id; path = $taskExe; profile = $taskProfile } }
$taskUiAccepted = $true
Write-Output "PASS $taskRole : $($taskResult.checks.Count) integration checks"
} finally {
if ((-not $KeepOpen -or -not $taskUiAccepted) -and -not $taskUiProcess.HasExited) { Stop-Process -Id $taskUiProcess.Id }
$taskUiProcess.Dispose()
}
}
# Exercise the same native inventory view with genuinely revoked server permissions.
$taskSavedPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskDisabledValues = @{}
$taskSavedValues = @{}
foreach ($taskValue in $taskSavedPermissions.fields) { $taskDisabledValues[$taskValue.key] = $false; $taskSavedValues[$taskValue.key] = [bool]$taskValue.value }
$taskDisableBody = @{version=$taskSavedPermissions.version; values=$taskDisabledValues} | ConvertTo-Json -Depth 8
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskDisableBody
$null = Invoke-RestMethod "$taskOrigin/control/v1/config" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body '{"allowPlayerCurrencyGrant":false,"allowPlayerItemGrant":false}'
$taskReaderReport = Join-Path $taskRun 'PlayerReadOnly.json'
$taskReaderProfile = Join-Path $taskRun 'PlayerReadOnly/ProfilesRoot'
$taskReader = Start-Process -FilePath $taskPlayerExe -WorkingDirectory (Split-Path $taskPlayerExe) -ArgumentList @('"--verify-ui=' + $taskReaderReport + '"', '"--profile-root=' + $taskReaderProfile + '"', "--verify-api=$taskOrigin", '--verify-readonly=true', "--ui-language=$UiLanguage") -WindowStyle Hidden -PassThru
try {
if (-not $taskReader.WaitForExit(90000)) { throw 'Read-only inventory verification timed out' }
$taskReaderResult = Get-Content -LiteralPath $taskReaderReport -Raw | ConvertFrom-Json
if (-not $taskReaderResult.passed) { throw "Read-only inventory verification failed: $($taskReaderResult.error)" }
Write-Output 'PASS PlayerReadOnly : disabled permissions and inventory category navigation'
} finally {
if (-not $taskReader.HasExited) { Stop-Process -Id $taskReader.Id }
$taskReader.Dispose()
$taskCurrentPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskRestoreBody = @{version=$taskCurrentPermissions.version; values=$taskSavedValues} | ConvertTo-Json -Depth 8
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskRestoreBody
$null = Invoke-RestMethod "$taskOrigin/control/v1/config" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body '{"allowPlayerCurrencyGrant":true,"allowPlayerItemGrant":true}'
}
$taskCompleted = $true
if ($KeepOpen) {
@{ serverPid = $taskServerProcess.Id; serverPath = $taskServerExe; origin = $taskOrigin; runtime = $taskRuntime; windows = $taskVisibleProcesses } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $taskRun 'interactive-session.json') -Encoding utf8
Write-Output "Interactive verification: $taskRun/interactive-session.json"
}
Write-Output "Desktop reports: $taskRun"
} finally {
if (-not ($KeepOpen -and $taskCompleted)) {
foreach ($taskVisible in $taskVisibleProcesses) { Stop-Process -Id $taskVisible.pid -ErrorAction SilentlyContinue }
if (-not $taskServerProcess.HasExited) { Stop-Process -Id $taskServerProcess.Id }
}
$taskServerProcess.Dispose()
}
#requires -Version 7.0
param([ValidateSet('Debug','Release')][string]$Configuration = 'Release', [string]$Package, [switch]$UseLaunchProfiles, [switch]$NoBuild, [switch]$KeepOpen, [switch]$VerifyDialogs, [ValidateSet('zh','en','de','es','fr','pl','ru','uk')][string]$UiLanguage = 'zh', [string]$NativeCardManifest)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$taskRun = Join-Path $taskRoot ('artifacts/desktop-smoke-' + [Guid]::NewGuid().ToString('N'))
$taskIntermediate = Join-Path $taskRoot ('artifacts/test-obj/' + [Guid]::NewGuid().ToString('N').Substring(0, 12))
$taskRuntime = Join-Path $taskRun 'Server/runtime'
New-Item -ItemType Directory -Path $taskRuntime -Force | Out-Null
$taskLaunchProfiles = @{}
$taskVisibleProcesses = @()
$taskCompleted = $false
if ($UseLaunchProfiles -and $Package) { throw 'Choose source launch profiles or a published package, not both.' }
if ($NoBuild -and -not $UseLaunchProfiles) { throw '-NoBuild requires -UseLaunchProfiles.' }
if ($Package) {
$taskServerExe = Join-Path $Package 'Server/XFEWarframe.Server.exe'
$taskAdminExe = Join-Path $Package 'Admin/XFEWarframe.Admin.exe'
$taskPlayerExe = Join-Path $Package 'Client/XFEWarframe.Player.exe'
} elseif ($UseLaunchProfiles) {
if (-not $NoBuild) {
dotnet build (Join-Path $taskRoot 'XFEWarframe.slnx') -c $Configuration -p:Platform=x64 --nologo -v quiet
if ($LASTEXITCODE -ne 0) { throw "Solution $Configuration/x64 build failed" }
}
foreach ($taskSpec in @(
@{Role='Server';Project='Server/XFEWarframe.Server';Platform='AnyCPU'},
@{Role='Admin';Project='Admin/XFEWarframe.Admin';Platform='x64'},
@{Role='Player';Project='Client/XFEWarframe.Player';Platform='x64'}
)) {
$taskProjectDirectory = Join-Path $taskRoot $taskSpec.Project
$taskName = Split-Path $taskSpec.Project -Leaf
$taskPropertiesText = dotnet msbuild (Join-Path $taskProjectDirectory "$taskName.csproj") "-p:Configuration=$Configuration" "-p:Platform=$($taskSpec.Platform)" -getProperty:RunCommand,RunWorkingDirectory,ActiveDebugProfile,PlatformTarget,WindowsPackageType -nologo
if ($LASTEXITCODE -ne 0) { throw "Cannot evaluate $taskName debug target" }
$taskProperties = ($taskPropertiesText | ConvertFrom-Json).Properties
$taskProfiles = (Get-Content (Join-Path $taskProjectDirectory 'Properties/launchSettings.json') -Raw | ConvertFrom-Json -AsHashtable).profiles
$taskProfileName = if ($taskProperties.ActiveDebugProfile) { $taskProperties.ActiveDebugProfile } else { @($taskProfiles.Keys)[0] }
$taskProfile = $taskProfiles[$taskProfileName]
if ($taskProfile.commandName -ne 'Project') { throw "$taskName is not configured to launch its executable" }
if (-not (Test-Path -LiteralPath $taskProperties.RunCommand -PathType Leaf)) { throw "Debug executable missing: $($taskProperties.RunCommand)" }
if ($taskSpec.Role -ne 'Server' -and ($taskProperties.PlatformTarget -ne 'x64' -or $taskProperties.WindowsPackageType -ne 'None')) { throw 'WinUI must launch unpackaged on x64' }
if ($taskSpec.Role -in @('Server','Admin')) {
if ($taskProfile.commandLineArgs -notmatch '--root=([^ ]+)') { throw 'Missing server runtime argument' }
$taskConfiguredRoot = [IO.Path]::GetFullPath((Join-Path $taskProperties.RunWorkingDirectory $Matches[1]))
if ($taskConfiguredRoot -ne (Join-Path $taskRoot 'Server/runtime')) { throw 'Admin and Server debug profiles must use the same runtime directory' }
} else {
if ($taskProfile.commandLineArgs -notmatch '--payload=([^ ]+)') { throw 'Missing player payload argument' }
$taskPayloadRoot = [IO.Path]::GetFullPath((Join-Path $taskProperties.RunWorkingDirectory $Matches[1]))
if ($taskPayloadRoot -ne (Join-Path $taskRoot 'artifacts/bridge')) { throw 'Incorrect developer bridge path' }
}
$taskLaunchProfiles[$taskSpec.Role] = @{Executable=$taskProperties.RunCommand; WorkingDirectory=$taskProperties.RunWorkingDirectory; Arguments=$taskProfile.commandLineArgs; Profile=$taskProfileName}
}
$taskServerExe = $taskLaunchProfiles.Server.Executable
$taskAdminExe = $taskLaunchProfiles.Admin.Executable
$taskPlayerExe = $taskLaunchProfiles.Player.Executable
$taskLaunchProfiles | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $taskRun 'launch-profiles.json') -Encoding utf8
} else {
foreach ($taskProject in @('Server/XFEWarframe.Server','Admin/XFEWarframe.Admin','Client/XFEWarframe.Player')) {
$taskName = Split-Path $taskProject -Leaf
$taskBuildOutput = Join-Path $taskRun "build/$taskName"
dotnet build (Join-Path $taskRoot "$taskProject/$taskName.csproj") -c $Configuration ("-p:XFEIntermediateRoot=" + $taskIntermediate) ("-p:OutputPath=" + $taskBuildOutput + [IO.Path]::DirectorySeparatorChar) --nologo -v quiet
if ($LASTEXITCODE -ne 0) { throw "Build failed: $taskName" }
}
$taskServerExe = Join-Path $taskRun 'build/XFEWarframe.Server/XFEWarframe.Server.exe'
$taskAdminExe = Join-Path $taskRun 'build/XFEWarframe.Admin/XFEWarframe.Admin.exe'
$taskPlayerExe = Join-Path $taskRun 'build/XFEWarframe.Player/XFEWarframe.Player.exe'
}
$taskProbe = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$taskProbe.Start()
$taskPort = $taskProbe.LocalEndpoint.Port
$taskProbe.Stop()
$taskProbe.Start()
$taskGamePort = $taskProbe.LocalEndpoint.Port
$taskProbe.Stop()
$taskOrigin = "http://127.0.0.1:$taskPort"
$taskServerArguments = @('"--root=' + $taskRuntime + '"', "--port=$taskPort", "--game-port=$taskGamePort", '--no-irc')
$taskServerDirectory = Split-Path $taskServerExe
if ($UseLaunchProfiles) {
# The isolated root comes first so the profile cannot touch production state.
$taskServerArguments += $taskLaunchProfiles.Server.Arguments
$taskServerDirectory = $taskLaunchProfiles.Server.WorkingDirectory
}
$taskServerProcess = Start-Process -FilePath $taskServerExe -WorkingDirectory $taskServerDirectory -ArgumentList $taskServerArguments -WindowStyle Hidden -PassThru -RedirectStandardOutput (Join-Path $taskRun 'server-stdout.log') -RedirectStandardError (Join-Path $taskRun 'server-stderr.log')
try {
$taskReady = $false
for ($i = 0; $i -lt 60; $i++) {
if ($taskServerProcess.HasExited) { throw 'Isolated server exited before readiness' }
try { $null = Invoke-RestMethod "$taskOrigin/health" -TimeoutSec 1; $taskReady = $true; break } catch { Start-Sleep -Milliseconds 250 }
}
if (-not $taskReady) { throw 'Isolated server was not ready' }
$taskConfigPath = Join-Path $taskRuntime 'server.json'
$taskConfig = Get-Content -LiteralPath $taskConfigPath -Raw | ConvertFrom-Json
# --port is an in-memory override; write the test copy so Admin can discover this ephemeral listener.
$taskConfig.ListenUrls = @("$taskOrigin/")
$taskConfig.PublicUrl = $taskOrigin
$taskConfig | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $taskConfigPath -Encoding utf8
$taskHeaders = @{ Authorization = 'Bearer ' + $taskConfig.AdminToken }
$taskBody = @{ displayName = 'UI Verification'; email = 'ui@example.invalid'; password = [Guid]::NewGuid().ToString('N') } | ConvertTo-Json
$null = Invoke-RestMethod "$taskOrigin/control/v1/accounts" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskBody
$taskPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskAllowed = @{ experience=$true; 'mod.rank'=$true; riven=$true; batch=$true; quest=$true; 'create.quest'=$true; 'archive.export'=$true; 'archive.import'=$true; 'inbox.read'=$true; syndicate=$true }
foreach ($taskField in $taskPermissions.fields | Where-Object { $_.key.StartsWith('unlock.', [StringComparison]::Ordinal) }) { $taskAllowed[$taskField.key] = $true }
$taskEditorBody = @{ version = $taskPermissions.version; values = $taskAllowed } | ConvertTo-Json -Depth 6
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskEditorBody
foreach ($taskRole in @('Admin','Player')) {
$taskExe = if ($taskRole -eq 'Admin') { $taskAdminExe } else { $taskPlayerExe }
$taskReport = Join-Path $taskRun "$taskRole.json"
$taskProfile = Join-Path $taskRun "$taskRole/ProfilesRoot"
$taskArguments = @('"--verify-ui=' + $taskReport + '"', '"--profile-root=' + $taskProfile + '"', '"--root=' + $taskRuntime + '"')
$taskArguments += "--verify-api=$taskOrigin", "--ui-language=$UiLanguage"
if ($NativeCardManifest) { $taskArguments += ('"--verify-native-card-manifest=' + [IO.Path]::GetFullPath($NativeCardManifest) + '"') }
if ($KeepOpen) { $taskArguments += '--verify-visible=true' }
if ($VerifyDialogs) { $taskArguments += '--verify-dialogs=true' }
$taskWorkingDirectory = Split-Path $taskExe
if ($UseLaunchProfiles) {
$taskArguments += $taskLaunchProfiles[$taskRole].Arguments
$taskWorkingDirectory = $taskLaunchProfiles[$taskRole].WorkingDirectory
}
$taskUiProcess = Start-Process -FilePath $taskExe -WorkingDirectory $taskWorkingDirectory -ArgumentList $taskArguments -WindowStyle Hidden -PassThru
$taskUiAccepted = $false
try {
if ($KeepOpen) {
$taskDeadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-Path -LiteralPath $taskReport) -and -not $taskUiProcess.HasExited -and [DateTime]::UtcNow -lt $taskDeadline) { Start-Sleep -Milliseconds 200 }
} elseif (-not $taskUiProcess.WaitForExit(90000)) { throw "$taskRole UI verification timed out" }
if (-not (Test-Path -LiteralPath $taskReport)) { throw "$taskRole did not create a verification report (exit $($taskUiProcess.ExitCode))" }
$taskResult = Get-Content -LiteralPath $taskReport -Raw | ConvertFrom-Json
if (-not $taskResult.passed) { throw "$taskRole verification failed: $($taskResult.error)" }
if ($KeepOpen) { $taskVisibleProcesses += @{ role = $taskRole; pid = $taskUiProcess.Id; path = $taskExe; profile = $taskProfile } }
$taskUiAccepted = $true
Write-Output "PASS $taskRole : $($taskResult.checks.Count) integration checks"
} finally {
if ((-not $KeepOpen -or -not $taskUiAccepted) -and -not $taskUiProcess.HasExited) { Stop-Process -Id $taskUiProcess.Id }
$taskUiProcess.Dispose()
}
}
# Exercise the same native inventory view with genuinely revoked server permissions.
$taskSavedPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskDisabledValues = @{}
$taskSavedValues = @{}
foreach ($taskValue in $taskSavedPermissions.fields) { $taskDisabledValues[$taskValue.key] = $false; $taskSavedValues[$taskValue.key] = [bool]$taskValue.value }
$taskDisableBody = @{version=$taskSavedPermissions.version; values=$taskDisabledValues} | ConvertTo-Json -Depth 8
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskDisableBody
$null = Invoke-RestMethod "$taskOrigin/control/v1/config" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body '{"allowPlayerCurrencyGrant":false,"allowPlayerItemGrant":false}'
$taskReaderReport = Join-Path $taskRun 'PlayerReadOnly.json'
$taskReaderProfile = Join-Path $taskRun 'PlayerReadOnly/ProfilesRoot'
$taskReader = Start-Process -FilePath $taskPlayerExe -WorkingDirectory (Split-Path $taskPlayerExe) -ArgumentList @('"--verify-ui=' + $taskReaderReport + '"', '"--profile-root=' + $taskReaderProfile + '"', "--verify-api=$taskOrigin", '--verify-readonly=true', "--ui-language=$UiLanguage") -WindowStyle Hidden -PassThru
try {
if (-not $taskReader.WaitForExit(90000)) { throw 'Read-only inventory verification timed out' }
$taskReaderResult = Get-Content -LiteralPath $taskReaderReport -Raw | ConvertFrom-Json
if (-not $taskReaderResult.passed) { throw "Read-only inventory verification failed: $($taskReaderResult.error)" }
Write-Output 'PASS PlayerReadOnly : disabled permissions and inventory category navigation'
} finally {
if (-not $taskReader.HasExited) { Stop-Process -Id $taskReader.Id }
$taskReader.Dispose()
$taskCurrentPermissions = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Headers $taskHeaders
$taskRestoreBody = @{version=$taskCurrentPermissions.version; values=$taskSavedValues} | ConvertTo-Json -Depth 8
$null = Invoke-RestMethod "$taskOrigin/control/v1/editor/permissions" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body $taskRestoreBody
$null = Invoke-RestMethod "$taskOrigin/control/v1/config" -Method Post -Headers $taskHeaders -ContentType 'application/json' -Body '{"allowPlayerCurrencyGrant":true,"allowPlayerItemGrant":true}'
}
$taskCompleted = $true
if ($KeepOpen) {
@{ serverPid = $taskServerProcess.Id; serverPath = $taskServerExe; origin = $taskOrigin; runtime = $taskRuntime; windows = $taskVisibleProcesses } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $taskRun 'interactive-session.json') -Encoding utf8
Write-Output "Interactive verification: $taskRun/interactive-session.json"
}
Write-Output "Desktop reports: $taskRun"
} finally {
if (-not ($KeepOpen -and $taskCompleted)) {
foreach ($taskVisible in $taskVisibleProcesses) { Stop-Process -Id $taskVisible.pid -ErrorAction SilentlyContinue }
if (-not $taskServerProcess.HasExited) { Stop-Process -Id $taskServerProcess.Id }
}
$taskServerProcess.Dispose()
}