param(
  [int]$GatewayPort = 18889,
  [string]$GatewayToken = 'qjc-bundled-runtime-token',
  [int]$SmokePort = 4318,
  [string]$SmokeToken = 'smoke-token',
  [string]$BaseOutputDir,
  [string]$SettingsConfigPath,
  [string]$AttachmentFixturePath,
  [int]$TimeoutSeconds = 180,
  [switch]$SkipMaterializeRuntime
)

$ErrorActionPreference = 'Stop'

function Write-Utf8File {
  param([string]$filePath, [string]$content)
  $encoding = New-Object System.Text.UTF8Encoding $false
  [System.IO.File]::WriteAllText($filePath, $content, $encoding)
}

function Write-Base64File {
  param(
    [string]$FilePath,
    [string]$Base64
  )

  $parent = Split-Path -Parent $FilePath
  if ($parent) {
    New-Item -ItemType Directory -Force -Path $parent | Out-Null
  }
  [System.IO.File]::WriteAllBytes($FilePath, [System.Convert]::FromBase64String($Base64))
}

function Get-AttachmentMimeType {
  param(
    [string]$FilePath
  )

  switch ([System.IO.Path]::GetExtension($FilePath).ToLowerInvariant()) {
    '.png' { return 'image/png' }
    '.jpg' { return 'image/jpeg' }
    '.jpeg' { return 'image/jpeg' }
    '.webp' { return 'image/webp' }
    '.gif' { return 'image/gif' }
    '.bmp' { return 'image/bmp' }
    '.pdf' { return 'application/pdf' }
    '.ppt' { return 'application/vnd.ms-powerpoint' }
    '.pptx' { return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
    '.xls' { return 'application/vnd.ms-excel' }
    '.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
    '.csv' { return 'text/csv' }
    '.tsv' { return 'text/tab-separated-values' }
    '.doc' { return 'application/msword' }
    '.docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
    '.txt' { return 'text/plain' }
    '.md' { return 'text/markdown' }
    '.json' { return 'application/json' }
    default { return 'application/octet-stream' }
  }
}

function Copy-ProjectBundleSource {
  param(
    [string]$SourceRoot,
    [string]$DestinationRoot
  )

  $projectJsonPath = Join-Path $SourceRoot 'project.json'
  $excludePaths = @()
  if (Test-Path $projectJsonPath) {
    $projectConfig = Get-Content -Path $projectJsonPath -Raw | ConvertFrom-Json
    $configuredExcludes = $projectConfig.bundlePackaging.excludePaths
    if ($configuredExcludes) {
      $excludePaths = @($configuredExcludes | ForEach-Object { [string]$_ })
    }
  }
  $excludeSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
  foreach ($entry in $excludePaths) {
    if ($entry) {
      [void]$excludeSet.Add($entry)
    }
  }

  New-Item -ItemType Directory -Force -Path $DestinationRoot | Out-Null
  foreach ($entry in Get-ChildItem -LiteralPath $SourceRoot -Force) {
    if ($excludeSet.Contains($entry.Name)) {
      continue
    }
    if ($entry.Name -eq '.tmp-test-runtime') {
      continue
    }
    if ($entry.PSIsContainer -and $entry.Name -match '^tmp[a-z0-9]{6,}$') {
      Write-Warning "Skipping transient temp directory from bundle source: $($entry.FullName)"
      continue
    }
    try {
      Copy-Item -LiteralPath $entry.FullName -Destination (Join-Path $DestinationRoot $entry.Name) -Recurse -Force
    }
    catch [System.UnauthorizedAccessException] {
      Write-Warning "Skipping inaccessible bundle source entry: $($entry.FullName)"
      continue
    }
  }
}

function Invoke-ElectronSmokeWithRetry {
  param(
    [string]$ScriptPath,
    [string]$Label,
    [string[]]$ArgumentList,
    [int]$MaxAttempts = 2
  )

  for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) {
    powershell -ExecutionPolicy Bypass -File $ScriptPath @ArgumentList
    if ($LASTEXITCODE -eq 0) {
      return
    }
    if ($attempt -ge $MaxAttempts) {
      exit $LASTEXITCODE
    }
    Write-Warning "$Label failed on attempt $attempt. Retrying..."
    Start-Sleep -Seconds 2
  }
}

function New-ExpertFixtureProject {
  param(
    [string]$ProjectsRoot,
    [string]$ProjectId,
    [string]$ProjectName,
    [string]$Platform,
    [string]$Description,
    [string]$UpdatedAt
  )

  $projectRoot = Join-Path $ProjectsRoot $ProjectId
  New-Item -ItemType Directory -Force -Path $projectRoot, (Join-Path $projectRoot 'memory') | Out-Null

  $projectPayload = [ordered]@{
    id = $ProjectId
    name = $ProjectName
    description = $Description
    platform = $Platform
    ready = $true
    updatedAt = $UpdatedAt
    boundSkillIds = @()
    workspaceEntryEnabled = $true
  }
  Write-Utf8File (Join-Path $projectRoot 'project.json') ($projectPayload | ConvertTo-Json -Depth 8)
  Write-Utf8File (Join-Path $projectRoot 'README.md') "# $ProjectName`n`nFixture expert project for UI smoke coverage."
  Write-Utf8File (Join-Path $projectRoot 'AGENTS.md') "# $ProjectName`n`nThis is a passive fixture expert used for desktop UI smoke coverage."
}

function Invoke-DouyinRuntimeSelfChecks {
  param(
    [string]$RepoRoot,
    [string]$ProjectRoot,
    [bool]$RequireVectCut = $true
  )

  $projectEnvPath = Join-Path $ProjectRoot 'memory\project.env'
  if (-not (Test-Path $projectEnvPath)) {
    throw "Douyin runtime self-checks require an existing project.env: $projectEnvPath"
  }

  $projectRootEnv = Join-Path $ProjectRoot '.env'
  if (Test-Path $projectRootEnv) {
    throw "Douyin runtime self-checks expected no project root .env, but found: $projectRootEnv"
  }

  $runtimePythonExe = Join-Path $RepoRoot 'vendor\openclaw-runtime\python\python.exe'
  if (-not (Test-Path $runtimePythonExe)) {
    throw "Bundled runtime python was not found: $runtimePythonExe"
  }

  $checkScripts = @(
    'skills\video-llm-analyzer\scripts\check_setup.py',
    'skills\replication-brief-builder\scripts\check_setup.py',
    'skills\seedance-2-0-video-generator\scripts\check_setup.py',
    'skills\jimeng-omnihuman\scripts\check_setup.py'
  )
  if ($RequireVectCut) {
    $checkScripts += 'skills\douyin-post-editor\scripts\check_setup.py'
  }

  $previousClientFlag = $env:QJC_CLIENT_CONFIG_ACTIVE
  $env:QJC_CLIENT_CONFIG_ACTIVE = '1'
  try {
    $results = @()
    foreach ($relativeScript in $checkScripts) {
      $scriptPath = Join-Path $ProjectRoot $relativeScript
      if (-not (Test-Path $scriptPath)) {
        throw "Douyin runtime self-check script was not found: $scriptPath"
      }

      $outputLines = @(& $runtimePythonExe $scriptPath 2>&1)
      $exitCode = $LASTEXITCODE
      $outputText = (($outputLines | ForEach-Object { [string]$_ }) -join "`n").Trim()
      if ($exitCode -ne 0) {
        throw "Douyin runtime self-check failed for $relativeScript`n$outputText"
      }

      $jsonStart = $outputText.IndexOf('{')
      $jsonEnd = $outputText.LastIndexOf('}')
      if ($jsonStart -lt 0 -or $jsonEnd -lt $jsonStart) {
        throw "Douyin runtime self-check did not emit JSON for $relativeScript`n$outputText"
      }

      $reportText = $outputText.Substring($jsonStart, $jsonEnd - $jsonStart + 1)
      $report = $reportText | ConvertFrom-Json
      $results += [ordered]@{
        script = $relativeScript.Replace('\', '/')
        configSource = [string]$report.config_source
        runtimeEnvExists = [bool]$report.runtime_env_exists
        clientConfigActive = [bool]$report.client_config_active
      }
    }
    return @($results)
  }
  finally {
    if ($null -eq $previousClientFlag) {
      Remove-Item Env:QJC_CLIENT_CONFIG_ACTIVE -ErrorAction SilentlyContinue
    } else {
      $env:QJC_CLIENT_CONFIG_ACTIVE = $previousClientFlag
    }
  }
}

function Get-ConfigValueFromSection {
  param(
    [string]$SectionText,
    [string]$Key
  )

  if (-not $SectionText) {
    return ""
  }

  $pattern = "(?im)^\s*$([regex]::Escape($Key))\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$"
  $match = [regex]::Match($SectionText, $pattern)
  if (-not $match.Success) {
    return ""
  }

  return $match.Groups[1].Value.Trim()
}

function Convert-RealDouyinSettingsText {
  param(
    [string]$Text
  )

  $normalizedText = ($Text -replace "`r", "").Trim()
  if (-not $normalizedText) {
    throw "Settings config text is empty."
  }

  $preBriefText = $normalizedText
  $replicationStart = [regex]::Match($normalizedText, '(?im)^\s*Replication Brief\s*$')
  if ($replicationStart.Success) {
    $preBriefText = $normalizedText.Substring(0, $replicationStart.Index)
  }

  $baseUrlMatches = [regex]::Matches($preBriefText, '(?im)^\s*baseUrl\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
  $modelMatches = [regex]::Matches($preBriefText, '(?im)^\s*model\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
  $apiKeyMatches = [regex]::Matches($preBriefText, '(?im)^\s*api_key\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
  if ($baseUrlMatches.Count -lt 3 -or $modelMatches.Count -lt 3 -or $apiKeyMatches.Count -lt 3) {
    throw "Failed to parse copywriting, video, and image settings from text config."
  }

  $replicationText = ""
  if ($replicationStart.Success) {
    $replicationText = $normalizedText.Substring($replicationStart.Index)
  }
  $videoAnalyzerText = ""
  $videoAnalyzerStart = [regex]::Match($normalizedText, '(?im)^\s*Video Analyzer\s*$')
  if ($videoAnalyzerStart.Success) {
    $videoAnalyzerText = $normalizedText.Substring($videoAnalyzerStart.Index)
  }

  $vectcutApiKey = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_API_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()
  $vectcutBaseUrl = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_BASE_URL\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()
  $vectcutFileBaseUrl = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_FILE_BASE_URL\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()

  $config = [ordered]@{
    copywriting = [ordered]@{
      baseUrl = $baseUrlMatches[0].Groups[1].Value.Trim()
      apiKey = $apiKeyMatches[0].Groups[1].Value.Trim()
      modelId = $modelMatches[0].Groups[1].Value.Trim()
    }
    video = [ordered]@{
      baseUrl = $baseUrlMatches[1].Groups[1].Value.Trim()
      apiKey = $apiKeyMatches[1].Groups[1].Value.Trim()
      modelId = $modelMatches[1].Groups[1].Value.Trim()
    }
    image = [ordered]@{
      baseUrl = $baseUrlMatches[2].Groups[1].Value.Trim()
      apiKey = $apiKeyMatches[2].Groups[1].Value.Trim()
      modelId = $modelMatches[2].Groups[1].Value.Trim()
    }
    digitalHuman = [ordered]@{
      volcAccessKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_VOLC_ACCESS_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
      volcSecretKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_VOLC_SECRET_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
      qiniuAccessKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_QINIU_ACCESS_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
      qiniuSecretKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_QINIU_SECRET_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
    }
    douyinRuntimeConfig = [ordered]@{
      videoAnalyzer = [ordered]@{
        baseUrl = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'ARK_BASE_URL')
        apiKey = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'ARK_API_KEY')
        modelId = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'VIDEO_LLM_ANALYZER_MODEL')
      }
      replicationBrief = [ordered]@{
        baseUrl = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'base_url')
        apiKey = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'api_key')
        modelId = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'model')
      }
    }
  }

  if ($vectcutApiKey -or $vectcutBaseUrl -or $vectcutFileBaseUrl) {
    $config.douyinRuntimeConfig.vectcut = [ordered]@{}
    if ($vectcutBaseUrl) {
      $config.douyinRuntimeConfig.vectcut.baseUrl = $vectcutBaseUrl
    }
    if ($vectcutFileBaseUrl) {
      $config.douyinRuntimeConfig.vectcut.fileBaseUrl = $vectcutFileBaseUrl
    }
    if ($vectcutApiKey) {
      $config.douyinRuntimeConfig.vectcut.apiKey = $vectcutApiKey
    }
  }

  return $config
}

function Resolve-SmokeSettingsConfig {
  param(
    [string]$PathValue
  )

  $defaultConfig = [ordered]@{
    image = [ordered]@{
      baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/images/generations'
      apiKey = 'image-smoke-key'
      modelId = 'doubao-seedream-5-0-260128'
    }
    video = [ordered]@{
      baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'
      apiKey = 'video-smoke-key'
      modelId = 'seedance-1-0-pro-250528'
    }
    copywriting = [ordered]@{
      baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
      apiKey = 'copy-smoke-key'
      modelId = 'qwen3.5-plus'
    }
    digitalHuman = [ordered]@{
      volcAccessKey = 'digital-human-volc-ak'
      volcSecretKey = 'digital-human-volc-sk'
      qiniuAccessKey = 'digital-human-qiniu-ak'
      qiniuSecretKey = 'digital-human-qiniu-sk'
    }
    douyinRuntimeConfig = [ordered]@{
      videoAnalyzer = [ordered]@{
        baseUrl = 'https://ark.cn-beijing.volces.com/api/v3'
        apiKey = 'video-analyzer-smoke-key'
        modelId = 'video-analyzer-smoke-model'
      }
      replicationBrief = [ordered]@{
        baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
        apiKey = 'replication-brief-smoke-key'
        modelId = 'replication-brief-smoke-model'
      }
      vectcut = [ordered]@{
        baseUrl = 'https://open.vectcut.com/cut_jianying'
        fileBaseUrl = 'https://open.vectcut.com'
        apiKey = 'vectcut-smoke-key'
      }
    }
  }

  if (-not $PathValue) {
    return $defaultConfig
  }

  $resolvedPath = (Resolve-Path $PathValue).Path
  $raw = Get-Content -LiteralPath $resolvedPath -Raw -Encoding UTF8
  if (-not $raw.Trim()) {
    throw "Settings config file is empty: $resolvedPath"
  }

  try {
    $parsedJson = $raw | ConvertFrom-Json -Depth 20 -AsHashtable
    if ($parsedJson) {
      return $parsedJson
    }
  }
  catch {
  }

  return (Convert-RealDouyinSettingsText -Text $raw)
}

function Seed-SmokeSecrets {
  param(
    [string]$UserDataPath,
    [hashtable]$SmokeSettingsConfig
  )

  $secretFilePath = Join-Path $UserDataPath 'config\secrets.dev.json'
  $payload = [ordered]@{
    note = "Development fallback only. Replace this file-based secret store with keytar before shipping."
  }
  $seededSecretCount = 0

  $digitalHuman = $SmokeSettingsConfig.digitalHuman
  if ($digitalHuman) {
    if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.volcAccessKey)) {
      $payload.digitalHumanVolcAccessKey = [string]$digitalHuman.volcAccessKey
      $seededSecretCount += 1
    }
    if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.volcSecretKey)) {
      $payload.digitalHumanVolcSecretKey = [string]$digitalHuman.volcSecretKey
      $seededSecretCount += 1
    }
    if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.qiniuAccessKey)) {
      $payload.digitalHumanQiniuAccessKey = [string]$digitalHuman.qiniuAccessKey
      $seededSecretCount += 1
    }
    if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.qiniuSecretKey)) {
      $payload.digitalHumanQiniuSecretKey = [string]$digitalHuman.qiniuSecretKey
      $seededSecretCount += 1
    }
  }

  if ($seededSecretCount -eq 0) {
    return
  }

  New-Item -ItemType Directory -Force -Path (Split-Path -Parent $secretFilePath) | Out-Null
  Write-Utf8File $secretFilePath ($payload | ConvertTo-Json -Depth 6)
}

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
  $BaseOutputDir = Join-Path $repoRoot '.tmp\douyin-expert-cloud-bundle-smoke'
}
$BaseOutputDir = [System.IO.Path]::GetFullPath($BaseOutputDir)
$userDataPath = Join-Path $BaseOutputDir 'user-data'
$logsPath = Join-Path $BaseOutputDir 'logs'
$smokeOutput = Join-Path $BaseOutputDir 'result.json'
$bundleSourceRoot = Join-Path $BaseOutputDir 'bundle-src'
$bundleZipPath = Join-Path $BaseOutputDir 'douyin-expert-cloud-bundle.zip'
$bundleFileName = 'douyin-expert-cloud-bundle.zip'
$bundleProjectId = 'douyin'
$bundleProjectName = 'Douyin Automation'
$bundleSkillId = 'douyin-project-bundle'
$bundleConfigVersion = '2026-04-10T00:00:00.000Z'
$expectedBundleSourceUrl = "http://127.0.0.1:$SmokePort/downloads/$bundleFileName"
$expertPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('5biu5oiR5YGa5LiA5Liq5YWz5LqO6Ziy5pmS5Za36Zu+55qE5oqW6Z+z6KeG6aKR5paH5qGI'))
$expectedExpertIds = @('browser-expert-smoke', 'douyin', 'xhs-expert-smoke')
$electronSmokeScript = Join-Path $repoRoot 'build\scripts\electron-smoke.ps1'
$defaultAttachmentFixturePath = Join-Path $BaseOutputDir 'fixtures\smoke-image.png'
$attachmentFixtureResolvedPath = ''
$attachmentFixtureName = ''
$attachmentKind = 'image'
$attachmentMimeType = 'image/png'
$expectedAttachmentRelativeDir = 'inputs/images/main'
$expectedAttachmentExtension = '.png'
$attachmentPayload = @(
  @{
    kind = $attachmentKind
    name = $attachmentFixtureName
    mimeType = $attachmentMimeType
    localPath = $attachmentFixtureResolvedPath
  }
)
$smokeSettingsConfig = Resolve-SmokeSettingsConfig -PathValue $SettingsConfigPath
$douyinSourceCandidates = @(
  (Join-Path $repoRoot 'workspace\douyin')
)
$douyinSourceRoot = $douyinSourceCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1

if (Test-Path $BaseOutputDir) {
  Remove-Item $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $BaseOutputDir, $bundleSourceRoot, $userDataPath, $logsPath | Out-Null
Seed-SmokeSecrets -UserDataPath $userDataPath -SmokeSettingsConfig $smokeSettingsConfig

if (-not $douyinSourceRoot) {
  throw "Douyin workspace source was not found in any expected location: $($douyinSourceCandidates -join ', ')"
}

$resolvedAttachmentSource = $AttachmentFixturePath
if (-not $resolvedAttachmentSource) {
  $resolvedAttachmentSource = $defaultAttachmentFixturePath
  Write-Base64File -FilePath $resolvedAttachmentSource -Base64 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACOSURBVHhe7dAxAQAwDITA+tf4XlIDOIDhFkbetjNrAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEUT+YDdB6OSM1gxG4BEAAAAAElFTkSuQmCC'
}
$attachmentFixtureResolvedPath = (Resolve-Path $resolvedAttachmentSource).Path
$attachmentFixtureName = Split-Path -Leaf $attachmentFixtureResolvedPath
$expectedAttachmentExtension = [System.IO.Path]::GetExtension($attachmentFixtureResolvedPath).ToLowerInvariant()
$attachmentKind = if (@('.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp') -contains $expectedAttachmentExtension) { 'image' } else { 'file' }
$attachmentMimeType = Get-AttachmentMimeType -FilePath $attachmentFixtureResolvedPath
$expectedAttachmentRelativeDir = if ($attachmentKind -eq 'image') { 'inputs/images/main' } else { 'inputs/assets/manual' }
$attachmentPayload = @(
  @{
    kind = $attachmentKind
    name = $attachmentFixtureName
    mimeType = $attachmentMimeType
    localPath = $attachmentFixtureResolvedPath
  }
)
Copy-ProjectBundleSource -SourceRoot $douyinSourceRoot -DestinationRoot (Join-Path $bundleSourceRoot 'douyin')
if (Test-Path $bundleZipPath) {
  Remove-Item $bundleZipPath -Force
}
Compress-Archive -Path (Join-Path $bundleSourceRoot 'douyin') -DestinationPath $bundleZipPath -Force

$projectsRoot = Join-Path $userDataPath 'projects'
$manifestsRoot = Join-Path $userDataPath 'manifests'
New-Item -ItemType Directory -Force -Path $projectsRoot, $manifestsRoot | Out-Null
New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'xhs-expert-smoke' -ProjectName 'XHS Expert Fixture' -Platform 'xiaohongshu' -Description 'Fixture project that keeps the experts rail above two items.' -UpdatedAt '2026-04-03T00:00:00.000Z'
New-ExpertFixtureProject -ProjectsRoot $projectsRoot -ProjectId 'browser-expert-smoke' -ProjectName 'Browser Expert Fixture' -Platform 'browser' -Description 'Fixture project that keeps the experts rail above two items.' -UpdatedAt '2026-04-03T00:01:00.000Z'
Write-Utf8File (Join-Path $manifestsRoot 'active-project.json') (@{ projectId = 'browser-expert-smoke' } | ConvertTo-Json -Depth 3)

if (-not $SkipMaterializeRuntime) {
  Write-Host "Materializing bundled runtime payload on port $GatewayPort"
  powershell -ExecutionPolicy Bypass -File (Join-Path $repoRoot 'build\scripts\materialize-runtime-payload.ps1') -GatewayPort $GatewayPort -GatewayToken $GatewayToken
  if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
  }
}

$env:QJCLAW_SMOKE_BUNDLE_ZIP_PATH = $bundleZipPath
$env:QJCLAW_SMOKE_BUNDLE_FILE_NAME = $bundleFileName
$env:QJCLAW_SMOKE_BUNDLE_SKILL_ID = $bundleSkillId
$env:QJCLAW_SMOKE_BUNDLE_SKILL_TITLE = 'Douyin Project Bundle'
$env:QJCLAW_SMOKE_BUNDLE_SKILL_DESCRIPTION = 'Zip-backed Douyin project bundle for expert-page smoke validation.'
$env:QJCLAW_SMOKE_BUNDLE_CONFIG_VERSION = $bundleConfigVersion
$env:QJCLAW_SMOKE_ATTACHMENTS_JSON = (ConvertTo-Json -InputObject @($attachmentPayload) -Depth 5 -Compress)
$env:QJCLAW_SMOKE_SETTINGS_CONFIG_JSON = ($smokeSettingsConfig | ConvertTo-Json -Depth 10 -Compress)
$env:QJCLAW_SMOKE_ACCEPT_WORKSPACE_LAUNCH = '1'
$env:QJCLAW_DISABLE_LOCAL_OPENCLAW_GATEWAY = '1'

try {
  Invoke-ElectronSmokeWithRetry -ScriptPath $electronSmokeScript -Label 'douyin expert cloud-bundle smoke' -ArgumentList @(
    '-SmokeOutput', $smokeOutput,
    '-SmokePort', $SmokePort,
    '-SmokeToken', $SmokeToken,
    '-UserDataPath', $userDataPath,
    '-LogsPath', $logsPath,
    '-RuntimeMode', 'bundled-runtime',
    '-ExpectBundledRuntime',
    '-ExpectWorkspaceEntry',
    '-PreserveUserData',
    '-WorkspaceProjectId', $bundleProjectId,
    '-WorkspaceProjectName', $bundleProjectName,
    '-WorkspaceMarkerFile', 'AGENTS.md',
    '-SmokePrompt', $expertPrompt,
    '-SmokeViewMode', 'experts',
    '-SmokeProjectId', $bundleProjectId,
    '-TimeoutSeconds', $TimeoutSeconds
  )

  $vectcutConfig = $smokeSettingsConfig.douyinRuntimeConfig.vectcut
  $expectedVectcutBaseUrl = if ($vectcutConfig) { [string]$vectcutConfig.baseUrl } else { '' }
  $expectedVectcutFileBaseUrl = if ($vectcutConfig) { [string]$vectcutConfig.fileBaseUrl } else { '' }
  $expectedVectcutApiKey = if ($vectcutConfig) { [string]$vectcutConfig.apiKey } else { '' }

  $summary = & node -e @"
const fs = require('fs');
const path = require('path');
const [
  smokeOutput,
  userDataPath,
  expectedBundleSourceUrl,
  expectedBundleConfigVersion,
  expectedBundleFileName,
  expectedBundleSkillId,
  expectedPrompt,
  expectedExpertIdsCsv,
  expectedAttachmentRelativeDir,
  expectedAttachmentExtension,
  expectedImageBaseUrl,
  expectedImageApiKey,
  expectedImageModelId,
  expectedCopyBaseUrl,
  expectedCopyApiKey,
  expectedCopyModelId,
  expectedVideoAnalyzerBaseUrl,
  expectedVideoAnalyzerApiKey,
  expectedVideoAnalyzerModelId,
  expectedReplicationBriefBaseUrl,
  expectedReplicationBriefApiKey,
  expectedReplicationBriefModelId,
  expectedVectcutBaseUrl,
  expectedVectcutFileBaseUrl,
  expectedVectcutApiKey
] = process.argv.slice(1);
const result = JSON.parse(fs.readFileSync(smokeOutput, 'utf8'));
if (!result.ok) {
  throw new Error(result.error || 'Smoke failed.');
}
function parseProjectEnv(filePath) {
  const parsed = {};
  const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
  for (const line of lines) {
    if (!line || line.startsWith('#')) {
      continue;
    }
    const separatorIndex = line.indexOf('=');
    if (separatorIndex <= 0) {
      continue;
    }
    const key = line.slice(0, separatorIndex).trim();
    const rawValue = line.slice(separatorIndex + 1).trim();
    if (!key) {
      continue;
    }
    try {
      parsed[key] = JSON.parse(rawValue);
    } catch {
      parsed[key] = rawValue;
    }
  }
  return parsed;
}
function sanitizeAttachmentFileComponent(value) {
  const trimmed = String(value || '').trim();
  const sanitized = trimmed.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
  return sanitized || 'attachment';
}
const expectedExpertIds = expectedExpertIdsCsv.split(',').map((value) => value.trim()).filter(Boolean).sort();
const finalState = result.finalState || {};
const sendResult = result.sendResult || {};
const streamSmoke = sendResult.streamSmoke || {};
const workspaceSummary = finalState.workspaceSummary || {};
const nonHomeProjects = Array.isArray(workspaceSummary.projects)
  ? workspaceSummary.projects.filter((project) => !project.isBuiltinHome)
  : [];
const settingsSave = sendResult.settingsSave || {};
const savedModelConfig = settingsSave.expertModelConfig || {};
const savedDouyinRuntimeConfig = settingsSave.douyinRuntimeConfig || {};
const expertProjectIds = Array.isArray(finalState.expertProjectIds)
  ? finalState.expertProjectIds.map((value) => String(value || '')).sort()
  : [];
const bundleManifestPath = path.join(userDataPath, 'manifests', 'project-bundles.json');
const projectEnvPath = path.join(userDataPath, 'projects', 'douyin', 'memory', 'project.env');
if (!fs.existsSync(bundleManifestPath)) {
  throw new Error('Bundle manifest was not produced: ' + bundleManifestPath);
}
if (!fs.existsSync(projectEnvPath)) {
  throw new Error('project.env was not materialized: ' + projectEnvPath);
}
const projectEnv = parseProjectEnv(projectEnvPath);
const bundleManifest = JSON.parse(fs.readFileSync(bundleManifestPath, 'utf8'));
const manifestRecord = bundleManifest.douyin;
if (!manifestRecord || typeof manifestRecord !== 'object') {
  throw new Error('Bundle manifest does not contain douyin.');
}
if (String(manifestRecord.sourceUrl || '') !== expectedBundleSourceUrl) {
  throw new Error('Unexpected bundle sourceUrl: ' + String(manifestRecord.sourceUrl || ''));
}
if (String(manifestRecord.configVersion || '') !== expectedBundleConfigVersion) {
  throw new Error('Unexpected bundle configVersion: ' + String(manifestRecord.configVersion || ''));
}
if (String(manifestRecord.fileName || '') !== expectedBundleFileName) {
  throw new Error('Unexpected bundle fileName: ' + String(manifestRecord.fileName || ''));
}
if (String(manifestRecord.sourceSkillId || '') !== expectedBundleSkillId) {
  throw new Error('Unexpected bundle sourceSkillId: ' + String(manifestRecord.sourceSkillId || ''));
}
if (String(finalState.viewMode || '') !== 'experts') {
  throw new Error('Smoke did not remain on experts view.');
}
if (String(sendResult.smokeViewMode || '') !== 'experts') {
  throw new Error('Smoke request was not issued from experts mode.');
}
if (String(sendResult.smokeProjectId || '') !== 'douyin') {
  throw new Error('Smoke request did not target the douyin expert.');
}
if (String(sendResult.prompt || '') !== expectedPrompt) {
  throw new Error('Smoke prompt mismatch.');
}
if (String(savedModelConfig.image && savedModelConfig.image.baseUrl || '') !== expectedImageBaseUrl) {
  throw new Error('Saved image baseUrl mismatch: ' + String(savedModelConfig.image && savedModelConfig.image.baseUrl || ''));
}
if (String(savedModelConfig.image && savedModelConfig.image.modelId || '') !== expectedImageModelId) {
  throw new Error('Saved image modelId mismatch: ' + String(savedModelConfig.image && savedModelConfig.image.modelId || ''));
}
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.baseUrl || '') !== expectedCopyBaseUrl) {
  throw new Error('Saved copywriting baseUrl mismatch: ' + String(savedModelConfig.copywriting && savedModelConfig.copywriting.baseUrl || ''));
}
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || '') !== expectedCopyModelId) {
  throw new Error('Saved copywriting modelId mismatch: ' + String(savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || ''));
}
if (String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.baseUrl || '') !== expectedVideoAnalyzerBaseUrl) {
  throw new Error('Saved videoAnalyzer baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.baseUrl || ''));
}
if (String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.modelId || '') !== expectedVideoAnalyzerModelId) {
  throw new Error('Saved videoAnalyzer modelId mismatch: ' + String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.modelId || ''));
}
if (!Boolean(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.apiKeyConfigured)) {
  throw new Error('Saved videoAnalyzer apiKeyConfigured mismatch.');
}
if (String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.baseUrl || '') !== expectedReplicationBriefBaseUrl) {
  throw new Error('Saved replicationBrief baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.baseUrl || ''));
}
if (String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.modelId || '') !== expectedReplicationBriefModelId) {
  throw new Error('Saved replicationBrief modelId mismatch: ' + String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.modelId || ''));
}
if (!Boolean(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.apiKeyConfigured)) {
  throw new Error('Saved replicationBrief apiKeyConfigured mismatch.');
}
if (expectedVectcutApiKey) {
  if (String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.baseUrl || '') !== expectedVectcutBaseUrl) {
    throw new Error('Saved vectcut baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.baseUrl || ''));
  }
  if (String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.fileBaseUrl || '') !== expectedVectcutFileBaseUrl) {
    throw new Error('Saved vectcut fileBaseUrl mismatch: ' + String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.fileBaseUrl || ''));
  }
  if (!Boolean(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.apiKeyConfigured)) {
    throw new Error('Saved vectcut apiKeyConfigured mismatch.');
  }
}
if (String(workspaceSummary.currentProjectId || '') !== 'douyin') {
  throw new Error('Final active project was not douyin: ' + String(workspaceSummary.currentProjectId || ''));
}
if (expertProjectIds.length !== expectedExpertIds.length || expertProjectIds.some((value, index) => value !== expectedExpertIds[index])) {
  throw new Error('Expert list did not expose all expected projects. actual=' + JSON.stringify(expertProjectIds) + ' expected=' + JSON.stringify(expectedExpertIds));
}
if (nonHomeProjects.length < 3) {
  throw new Error('Workspace summary did not expose at least three non-home projects.');
}
if (!String(sendResult.sessionId || '').startsWith('project:douyin:')) {
  throw new Error('Expert smoke session did not bind to douyin: ' + String(sendResult.sessionId || ''));
}
const statusLabels = Array.isArray(streamSmoke.statusLabels)
  ? streamSmoke.statusLabels.map((value) => String(value || ''))
  : [];
const workspaceLaunchAccepted = statusLabels.some((label) => label.includes('Launching project workspace'));
if (String(streamSmoke.phase || '') !== 'completed' && !workspaceLaunchAccepted) {
  throw new Error('Expert smoke stream did not complete: ' + String(streamSmoke.phase || ''));
}
if (String(sendResult.selectedSkillId || streamSmoke.selectedSkillId || '')) {
  throw new Error('Expert smoke unexpectedly selected a skill instead of workspace-entry: ' + String(sendResult.selectedSkillId || streamSmoke.selectedSkillId || ''));
}
if (statusLabels.some((label) => label.includes('Routing to skill'))) {
  throw new Error('Expert smoke still routed through a skill: ' + JSON.stringify(statusLabels));
}
if (!workspaceLaunchAccepted) {
  throw new Error('Expert smoke did not expose a workspace-entry launch status: ' + JSON.stringify(statusLabels));
}
const smokeAttachments = Array.isArray(sendResult.smokeAttachments) ? sendResult.smokeAttachments : [];
if (smokeAttachments.length !== 1) {
  throw new Error('Expected exactly one smoke attachment. actual=' + smokeAttachments.length);
}
const attachmentSessionId = String(sendResult.sessionId || '');
const sessionSlug = sanitizeAttachmentFileComponent(attachmentSessionId.replace(/[:]/g, '-'));
const expectedAttachmentRelativePath = expectedAttachmentRelativeDir + '/' + sessionSlug + '-01' + expectedAttachmentExtension;
const expectedAttachmentPath = path.join(userDataPath, 'projects', 'douyin', ...expectedAttachmentRelativePath.split('/'));
if (!fs.existsSync(expectedAttachmentPath)) {
  throw new Error('Materialized attachment file was not found: ' + expectedAttachmentPath);
}
const attachmentStat = fs.statSync(expectedAttachmentPath);
if (!attachmentStat.isFile() || attachmentStat.size < 1) {
  throw new Error('Materialized attachment file is empty: ' + expectedAttachmentPath);
}
const assistantContent = String((sendResult.lastAssistantMessage && sendResult.lastAssistantMessage.content) || streamSmoke.finalContent || streamSmoke.renderedContent || '');
if (!assistantContent.includes('Project attachments:')) {
  throw new Error('Assistant content did not echo project attachments.');
}
if (!assistantContent.includes(expectedAttachmentRelativePath)) {
  throw new Error('Assistant content did not reference the materialized attachment path: ' + expectedAttachmentRelativePath);
}
const expectedWriterBaseUrl = expectedCopyBaseUrl.endsWith('/chat/completions')
  ? expectedCopyBaseUrl
  : expectedCopyBaseUrl.replace(/\/+$/, '') + '/chat/completions';
if (String(projectEnv.DOUYIN_WRITER_LLM_BASE_URL || '') !== expectedWriterBaseUrl) {
  throw new Error('project.env DOUYIN_WRITER_LLM_BASE_URL mismatch: ' + String(projectEnv.DOUYIN_WRITER_LLM_BASE_URL || ''));
}
if (String(projectEnv.DASHSCOPE_API_KEY || '') !== expectedCopyApiKey) {
  throw new Error('project.env DASHSCOPE_API_KEY mismatch.');
}
if (String(projectEnv.QWEN_API_KEY || '') !== expectedCopyApiKey) {
  throw new Error('project.env QWEN_API_KEY mismatch.');
}
if (String(projectEnv.DOUYIN_WRITER_LLM_MODEL || '') !== expectedCopyModelId) {
  throw new Error('project.env DOUYIN_WRITER_LLM_MODEL mismatch: ' + String(projectEnv.DOUYIN_WRITER_LLM_MODEL || ''));
}
const expectedSeedreamBaseUrl = expectedImageBaseUrl.endsWith('/images/generations')
  ? expectedImageBaseUrl.slice(0, -'/images/generations'.length)
  : expectedImageBaseUrl;
if (String(projectEnv.SEEDREAM_ARK_BASE_URL || '') !== expectedSeedreamBaseUrl) {
  throw new Error('project.env SEEDREAM_ARK_BASE_URL mismatch: ' + String(projectEnv.SEEDREAM_ARK_BASE_URL || ''));
}
if (String(projectEnv.SEEDREAM_ARK_API_KEY || '') !== expectedImageApiKey) {
  throw new Error('project.env SEEDREAM_ARK_API_KEY mismatch.');
}
if (String(projectEnv.SEEDREAM_MODEL || '') !== expectedImageModelId) {
  throw new Error('project.env SEEDREAM_MODEL mismatch: ' + String(projectEnv.SEEDREAM_MODEL || ''));
}
if (String(projectEnv.QJC_CLIENT_CONFIG_ACTIVE || '') !== '1') {
  throw new Error('project.env QJC_CLIENT_CONFIG_ACTIVE mismatch: ' + String(projectEnv.QJC_CLIENT_CONFIG_ACTIVE || ''));
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_BASE_URL || '') !== expectedVideoAnalyzerBaseUrl) {
  throw new Error('project.env VIDEO_LLM_ANALYZER_BASE_URL mismatch: ' + String(projectEnv.VIDEO_LLM_ANALYZER_BASE_URL || ''));
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_API_KEY || '') !== expectedVideoAnalyzerApiKey) {
  throw new Error('project.env VIDEO_LLM_ANALYZER_API_KEY mismatch.');
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_MODEL || '') !== expectedVideoAnalyzerModelId) {
  throw new Error('project.env VIDEO_LLM_ANALYZER_MODEL mismatch: ' + String(projectEnv.VIDEO_LLM_ANALYZER_MODEL || ''));
}
if (String(projectEnv.ARK_BASE_URL || '') !== expectedVideoAnalyzerBaseUrl) {
  throw new Error('project.env ARK_BASE_URL mismatch: ' + String(projectEnv.ARK_BASE_URL || ''));
}
if (String(projectEnv.ARK_API_KEY || '') !== expectedVideoAnalyzerApiKey) {
  throw new Error('project.env ARK_API_KEY mismatch.');
}
if (String(projectEnv.REPLICATION_BRIEF_BASE_URL || '') !== expectedReplicationBriefBaseUrl) {
  throw new Error('project.env REPLICATION_BRIEF_BASE_URL mismatch: ' + String(projectEnv.REPLICATION_BRIEF_BASE_URL || ''));
}
if (String(projectEnv.REPLICATION_BRIEF_API_KEY || '') !== expectedReplicationBriefApiKey) {
  throw new Error('project.env REPLICATION_BRIEF_API_KEY mismatch.');
}
if (String(projectEnv.REPLICATION_BRIEF_MODEL || '') !== expectedReplicationBriefModelId) {
  throw new Error('project.env REPLICATION_BRIEF_MODEL mismatch: ' + String(projectEnv.REPLICATION_BRIEF_MODEL || ''));
}
if (expectedVectcutApiKey) {
  if (String(projectEnv.VECTCUT_BASE_URL || '') !== expectedVectcutBaseUrl) {
    throw new Error('project.env VECTCUT_BASE_URL mismatch: ' + String(projectEnv.VECTCUT_BASE_URL || ''));
  }
  if (String(projectEnv.VECTCUT_FILE_BASE_URL || '') !== expectedVectcutFileBaseUrl) {
    throw new Error('project.env VECTCUT_FILE_BASE_URL mismatch: ' + String(projectEnv.VECTCUT_FILE_BASE_URL || ''));
  }
  if (String(projectEnv.VECTCUT_API_KEY || '') !== expectedVectcutApiKey) {
    throw new Error('project.env VECTCUT_API_KEY mismatch.');
  }
}
console.log(JSON.stringify({
  ok: true,
  smokeOutput,
  prompt: sendResult.prompt || null,
  currentProjectId: workspaceSummary.currentProjectId || null,
  currentProjectName: workspaceSummary.currentProjectName || null,
  expertProjectIds,
  nonHomeProjectCount: nonHomeProjects.length,
  executionPolicySource: streamSmoke.executionPolicySource || null,
  sessionId: sendResult.sessionId || null,
  selectedSkillId: sendResult.selectedSkillId || streamSmoke.selectedSkillId || null,
  attachmentCount: smokeAttachments.length,
  attachmentPath: expectedAttachmentPath,
  attachmentRelativePath: expectedAttachmentRelativePath,
  projectEnvPath,
  savedImageModelId: savedModelConfig.image && savedModelConfig.image.modelId || null,
  savedCopywritingModelId: savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || null,
  statusLabels,
  bundleManifestPath
}, null, 2));
"@ $smokeOutput $userDataPath $expectedBundleSourceUrl $bundleConfigVersion $bundleFileName $bundleSkillId $expertPrompt ($expectedExpertIds -join ',') $expectedAttachmentRelativeDir $expectedAttachmentExtension $smokeSettingsConfig.image.baseUrl $smokeSettingsConfig.image.apiKey $smokeSettingsConfig.image.modelId $smokeSettingsConfig.copywriting.baseUrl $smokeSettingsConfig.copywriting.apiKey $smokeSettingsConfig.copywriting.modelId $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.baseUrl $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.apiKey $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.modelId $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.baseUrl $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.apiKey $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.modelId $expectedVectcutBaseUrl $expectedVectcutFileBaseUrl $expectedVectcutApiKey
  if ($LASTEXITCODE -ne 0) {
    exit $LASTEXITCODE
  }
  $requireVectCut = -not [string]::IsNullOrWhiteSpace($expectedVectcutApiKey)
  $runtimeSelfCheckResults = Invoke-DouyinRuntimeSelfChecks -RepoRoot $repoRoot -ProjectRoot (Join-Path $userDataPath 'projects\douyin') -RequireVectCut $requireVectCut
  Write-Output $summary
  Write-Output ($runtimeSelfCheckResults | ConvertTo-Json -Depth 5)
}
finally {
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_ZIP_PATH -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_FILE_NAME -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_SKILL_ID -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_SKILL_TITLE -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_SKILL_DESCRIPTION -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_BUNDLE_CONFIG_VERSION -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_ATTACHMENTS_JSON -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_SETTINGS_CONFIG_JSON -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_SMOKE_ACCEPT_WORKSPACE_LAUNCH -ErrorAction SilentlyContinue
  Remove-Item Env:QJCLAW_DISABLE_LOCAL_OPENCLAW_GATEWAY -ErrorAction SilentlyContinue
}
