fix(installer): auto-detect SYSTEM account and default PATH scope to machine (#4903)

* fix(installer): auto-detect SYSTEM account and default PATH scope to machine

When the Windows standalone installer runs as the SYSTEM account (SID
S-1-5-18), which is common for ECS Workbench SSM, WinRM, and scheduled
tasks, the default --path-scope is now 'machine' instead of 'user'.

Previously, persisting PATH to the SYSTEM user's HKCU hive had no
effect on new sessions spawned by the SSM agent service, because the
agent inherits its environment at service start time and never re-reads
HKCU. Writing to the Machine PATH (HKLM) ensures new sessions pick up
the qwen shim immediately.

Also adds:
- --repair-path flag to fix PATH for an existing install without
  reinstalling
- --path-scope user|machine CLI option and QWEN_INSTALL_PATH_SCOPE env
  var
- Uninstaller now cleans both User and Machine PATH entries
- CI validation step for hosted installation assets

Resolves #4901

* test(installer): add Windows self-hosted qwen smoke

* fix(installer): address Windows PATH review feedback

* refactor(installer): remove redundant YAML validation, deduplicate test fixtures

- Remove windows-self-hosted-qwen-smoke.yml (operational tool, not part of fix)
- Revert sync-release-to-oss.yml validate step (redundant with JS behavior guards)
- Extract duplicated test fixture strings into shared constants
- Pass normalized path to Remove-PathEntry to avoid double Get-NormalizedPath
- Fix stale comment referencing "user PATH" when scope is now configurable

* chore: drop unrelated dev.test.js changes from installer PR
This commit is contained in:
易良 2026-06-10 21:02:10 +08:00 committed by GitHub
parent 01bd2b64d1
commit 968a0d1f03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 225 additions and 56 deletions

View file

@ -71,6 +71,22 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = {
name: '--version parser',
pattern: /ARG_KEY!"=="--version"|"%~1"=="--version"/,
},
{
name: '--repair-path parser',
pattern: /if \/i "%~1"=="--repair-path"/,
},
{
name: '--path-scope parser',
pattern: /if \/i "!ARG_KEY!"=="--path-scope"/,
},
{
name: 'QWEN_INSTALL_REPAIR_PATH',
pattern: /QWEN_INSTALL_REPAIR_PATH/,
},
{
name: 'QWEN_INSTALL_PATH_SCOPE',
pattern: /QWEN_INSTALL_PATH_SCOPE/,
},
],
'install-qwen-standalone.ps1': [
{
@ -111,7 +127,7 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = {
},
{
name: 'PATH cleanup',
pattern: /Remove-UserPathEntry/,
pattern: /Remove-PathEntryFromAllScopes/,
},
{
name: 'current cmd shim cleanup',

View file

@ -11,7 +11,7 @@ The installers are intentionally lightweight:
- They try a standalone archive first by default.
- They do not install Node.js, NVM, or any other Node version manager.
- They do not edit npm config. Standalone installs may update the shell profile
or user PATH so the generated `qwen` shim is discoverable.
or user/machine PATH so the generated `qwen` shim is discoverable.
- They do not start `qwen` automatically after installation.
- They store source information in `~/.qwen/source.json` or
`%USERPROFILE%\.qwen\source.json` when `--source` is provided.
@ -133,6 +133,14 @@ install-qwen-standalone.bat --method standalone
install-qwen-standalone.bat --method npm
```
Repair PATH for an existing standalone Windows install without downloading or
reinstalling Qwen Code:
```bat
install-qwen-standalone.bat --repair-path
install-qwen-standalone.bat --repair-path --path-scope machine
```
## Optional Native Modules
The standalone archives bundle Qwen Code and a private Node.js runtime. They do
@ -211,6 +219,18 @@ Standalone installs to:
Override with `QWEN_INSTALL_ROOT`, `QWEN_INSTALL_LIB_DIR`, or
`QWEN_INSTALL_BIN_DIR` when needed.
For self-hosted Windows runners, install under the runner account once, then
repair machine-level PATH without reinstalling:
```powershell
$env:QWEN_INSTALL_REPAIR_PATH = '1'
$env:QWEN_INSTALL_PATH_SCOPE = 'machine'
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
```
Restart the runner service or machine after updating machine-level PATH so jobs
inherit the refreshed environment.
Restart the terminal if `qwen` is not immediately available on PATH.
Uninstall a standalone Windows install:
@ -220,10 +240,11 @@ powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangz
```
The uninstaller removes only the standalone runtime, generated `qwen.cmd`
wrapper, user PATH entry, and the current-session `cmd.exe` shim created by the
hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen` by default. Set
`QWEN_UNINSTALL_PURGE=1` to remove `%USERPROFILE%\.qwen\source.json`; other
config and auth files are still preserved.
wrapper, user or machine PATH entry, and the current-session `cmd.exe` shim
created by the hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen`
by default. Set `QWEN_UNINSTALL_PURGE=1` to remove
`%USERPROFILE%\.qwen\source.json`; other config and auth files are still
preserved.
## Mirrors and Overrides
@ -236,6 +257,8 @@ Options:
- `--version VERSION`
- `--registry REGISTRY`
- `--source SOURCE`
- `--repair-path`
- `--path-scope user|machine`
Environment variables:
@ -245,6 +268,8 @@ Environment variables:
- `QWEN_INSTALL_ARCHIVE`
- `QWEN_INSTALL_VERSION`
- `QWEN_NPM_REGISTRY`
- `QWEN_INSTALL_REPAIR_PATH`
- `QWEN_INSTALL_PATH_SCOPE`
Use `--base-url` for private mirrors. The URL must contain
`qwen-code-<target>` archives and `SHA256SUMS` in the same directory. Custom

View file

@ -15,6 +15,16 @@ set "MIRROR=auto"
if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!"
set "NO_MODIFY_PATH=0"
if defined QWEN_NO_MODIFY_PATH set "NO_MODIFY_PATH=!QWEN_NO_MODIFY_PATH!"
set "REPAIR_PATH=0"
if defined QWEN_INSTALL_REPAIR_PATH set "REPAIR_PATH=!QWEN_INSTALL_REPAIR_PATH!"
set "PATH_SCOPE=user"
REM Auto-detect SYSTEM account and default to machine scope so that new sessions
REM spawned by service processes (SSM agent, WinRM, scheduled tasks) inherit PATH.
if not defined QWEN_INSTALL_PATH_SCOPE (
whoami /user 2>nul | findstr /i "S-1-5-18" >nul 2>&1
if !ERRORLEVEL! EQU 0 set "PATH_SCOPE=machine"
)
if defined QWEN_INSTALL_PATH_SCOPE set "PATH_SCOPE=!QWEN_INSTALL_PATH_SCOPE!"
set "BASE_URL="
if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!"
set "ARCHIVE_PATH="
@ -194,6 +204,30 @@ if /i "%~1"=="--no-modify-path" (
shift
goto parse_args
)
if /i "%~1"=="--repair-path" (
set "REPAIR_PATH=1"
shift
goto parse_args
)
if /i "!ARG_KEY!"=="--path-scope" (
if "!ARG_HAS_INLINE_VALUE!"=="1" (
if "!ARG_VALUE!"=="" (
echo ERROR: --path-scope requires a value
exit /b 1
)
set "PATH_SCOPE=!ARG_VALUE!"
shift
goto parse_args
)
if "%~2"=="" (
echo ERROR: --path-scope requires a value
exit /b 1
)
set "PATH_SCOPE=%~2"
shift
shift
goto parse_args
)
if /i "%~1"=="-h" goto usage
if /i "%~1"=="--help" goto usage
@ -206,6 +240,13 @@ goto usage_error
call :ValidateOptions
if %ERRORLEVEL% NEQ 0 exit /b 1
if /i "!REPAIR_PATH!"=="1" (
call :RepairPath
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
exit /b 0
)
call :PrintHeader
REM Discover all qwen executables on disk BEFORE we install. We can't
@ -302,7 +343,9 @@ echo --base-url URL Override standalone archive base URL
echo --archive PATH Install from a local standalone archive
echo --version VERSION Release version (default: latest)
echo --registry URL npm registry (default: https://registry.npmmirror.com)
echo --no-modify-path Do not modify user PATH
echo --no-modify-path Do not modify PATH
echo --repair-path Repair PATH for an existing standalone install
echo --path-scope SCOPE PATH scope: user or machine (default: user, auto machine for SYSTEM)
echo -s, --source SOURCE Record installation source
echo -h, --help Show this help message
exit /b 0
@ -329,7 +372,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "$esc = [char]27; $bar =
exit /b 0
:ValidateRawEnvironmentOptions
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_REPAIR_PATH','QWEN_INSTALL_PATH_SCOPE','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0"
if %ERRORLEVEL% EQU 0 exit /b 0
echo ERROR: installer options contain unsafe command characters.
exit /b 1
@ -347,11 +390,12 @@ set "QWEN_VALIDATE_INSTALL_BASE=!INSTALL_BASE!"
set "QWEN_VALIDATE_INSTALL_DIR=!INSTALL_DIR!"
set "QWEN_VALIDATE_INSTALL_BIN_DIR=!INSTALL_BIN_DIR!"
set "QWEN_VALIDATE_SOURCE=!SOURCE!"
set "QWEN_VALIDATE_PATH_SCOPE=!PATH_SCOPE!"
call :CreateTempFile "qwen-validate-options" ".ps1"
if !ERRORLEVEL! NEQ 0 exit /b 1
set "QWEN_VALIDATE_OPTIONS_SCRIPT=!TEMP_FILE!"
> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124)
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE')
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE','PATH_SCOPE')
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo foreach ($name in $names) {
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name)
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 }
@ -371,6 +415,7 @@ set "QWEN_VALIDATE_INSTALL_BASE="
set "QWEN_VALIDATE_INSTALL_DIR="
set "QWEN_VALIDATE_INSTALL_BIN_DIR="
set "QWEN_VALIDATE_SOURCE="
set "QWEN_VALIDATE_PATH_SCOPE="
if %PS_STATUS% NEQ 0 (
echo ERROR: installer options contain unsafe command characters.
exit /b 1
@ -421,6 +466,18 @@ echo ERROR: --mirror must be auto, github, or aliyun.
exit /b 1
:validate_mirror_ok
if "!REPAIR_PATH!"=="0" goto validate_repair_path_ok
if "!REPAIR_PATH!"=="1" goto validate_repair_path_ok
echo ERROR: QWEN_INSTALL_REPAIR_PATH must be 0 or 1.
exit /b 1
:validate_repair_path_ok
if /i "!PATH_SCOPE!"=="user" goto validate_path_scope_ok
if /i "!PATH_SCOPE!"=="machine" goto validate_path_scope_ok
echo ERROR: --path-scope must be user or machine.
exit /b 1
:validate_path_scope_ok
call :ValidateHttpsUrlVar "BASE_URL" "--base-url"
if %ERRORLEVEL% NEQ 0 exit /b 1
@ -592,17 +649,19 @@ set "GITHUB_FALLBACK_BASE_URL="
set "MIRROR=github"
exit /b 0
:MaybeUpdateUserPath
:MaybeUpdatePath
rem args: %~1=install_bin_dir
rem Prepend the install dir to the user-level PATH (HKCU\Environment) via
rem Prepend the install dir to the selected PATH scope via
rem [Environment]::SetEnvironmentVariable. Idempotent: skips if the dir is
rem already on the user PATH. Uses PowerShell rather than `setx` because setx
rem already on PATH. Uses PowerShell rather than `setx` because setx
rem truncates PATH at 1024 chars, which can silently mangle long PATHs.
set "QWEN_NEW_BIN=%~1"
if "!QWEN_NEW_BIN!"=="" exit /b 0
powershell -NoProfile -ExecutionPolicy Bypass -Command "$bin = $env:QWEN_NEW_BIN; $userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); if ([string]::IsNullOrEmpty($userPath)) { $userPath = '' }; $entries = @($userPath -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { $_ -ne $bin }); if ($entries.Count -gt 0 -and $entries[0] -eq $bin -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, 'User'); exit 0"
set "QWEN_PATH_SCOPE=!PATH_SCOPE!"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { $bin = $env:QWEN_NEW_BIN; $scope = $env:QWEN_PATH_SCOPE; $target = if ($scope -ieq 'machine') { 'Machine' } else { 'User' }; $pathValue = [Environment]::GetEnvironmentVariable('Path', $target); if ([string]::IsNullOrEmpty($pathValue)) { $pathValue = '' }; $entries = @($pathValue -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { -not [string]::Equals($_, $bin, [StringComparison]::OrdinalIgnoreCase) }); if ($entries.Count -gt 0 -and [string]::Equals($entries[0], $bin, [StringComparison]::OrdinalIgnoreCase) -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, $target); exit 0 } catch { exit 1 }"
set "PS_STATUS=%ERRORLEVEL%"
set "QWEN_NEW_BIN="
set "QWEN_PATH_SCOPE="
exit /b %PS_STATUS%
:UrlExists
@ -1014,6 +1073,35 @@ if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
REM Standalone archive installed to !INSTALL_DIR!
exit /b 0
:RepairPath
set "INSTALLED_BIN=!INSTALL_BIN_DIR!\qwen.cmd"
if not exist "!INSTALLED_BIN!" (
echo ERROR: Qwen Code standalone wrapper was not found:
echo !INSTALLED_BIN!
echo ERROR: Set QWEN_INSTALL_ROOT or QWEN_INSTALL_BIN_DIR to the existing install, or install Qwen Code first.
exit /b 1
)
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
if /i not "!NO_MODIFY_PATH!"=="1" (
call :MaybeUpdatePath "!INSTALL_BIN_DIR!"
if !ERRORLEVEL! NEQ 0 (
echo ERROR: Failed to update !PATH_SCOPE! PATH. Run as administrator for --path-scope machine, or add this directory manually:
echo !INSTALL_BIN_DIR!
exit /b 1
)
)
set "INSTALLED_VERSION=unknown"
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
echo.
echo Qwen Code !INSTALLED_VERSION! is already installed.
echo PATH repaired for !PATH_SCOPE! scope:
echo !INSTALL_BIN_DIR!
echo.
exit /b 0
:CreateTempDir
set "TEMP_DIR="
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $dir = Join-Path $env:TEMP ('qwen-code-install-' + [IO.Path]::GetRandomFileName()); New-Item -ItemType Directory -Path $dir -ErrorAction Stop | Out-Null; [Console]::Write($dir)"`) do set "TEMP_DIR=%%I"
@ -1239,11 +1327,11 @@ if not "!INSTALLED_BIN!"=="" if exist "!INSTALLED_BIN!" (
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
)
rem Persist the install bin to user PATH unless --no-modify-path is set.
rem Persist the install bin to PATH unless --no-modify-path is set.
if not "!EXTRA_BIN!"=="" if /i not "!NO_MODIFY_PATH!"=="1" (
call :MaybeUpdateUserPath "!EXTRA_BIN!"
call :MaybeUpdatePath "!EXTRA_BIN!"
if !ERRORLEVEL! NEQ 0 (
echo WARNING: Failed to update user PATH. Add the directory manually:
echo WARNING: Failed to update !PATH_SCOPE! PATH. Add the directory manually:
echo !EXTRA_BIN!
) else (
set "PATH_UPDATE_APPLIED=1"

View file

@ -150,22 +150,25 @@ function Test-QwenStandaloneInstallDir {
return $true
}
function Remove-UserPathEntry {
param([string]$BinDir)
function Remove-PathEntry {
param(
[string]$BinDir,
[string]$Scope
)
$target = Get-NormalizedPath -PathValue $BinDir
if ([string]::IsNullOrEmpty($target)) {
return
}
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ([string]::IsNullOrEmpty($userPath)) {
$pathValue = [Environment]::GetEnvironmentVariable('Path', $Scope)
if ([string]::IsNullOrEmpty($pathValue)) {
return
}
$kept = New-Object System.Collections.Generic.List[string]
$removed = $false
foreach ($entry in @($userPath -split ';')) {
foreach ($entry in @($pathValue -split ';')) {
if ([string]::IsNullOrEmpty($entry)) {
continue
}
@ -179,9 +182,25 @@ function Remove-UserPathEntry {
}
if ($removed) {
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')
Write-Success "Removed $BinDir from user PATH."
try {
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), $Scope)
Write-Success "Removed $BinDir from $Scope PATH."
} catch {
Write-WarningMessage "Could not remove $BinDir from $Scope PATH: $($_.Exception.Message)"
}
}
}
function Remove-PathEntryFromAllScopes {
param([string]$BinDir)
$target = Get-NormalizedPath -PathValue $BinDir
if ([string]::IsNullOrEmpty($target)) {
return
}
Remove-PathEntry -BinDir $target -Scope 'User'
Remove-PathEntry -BinDir $target -Scope 'Machine'
$current = New-Object System.Collections.Generic.List[string]
foreach ($entry in @($env:Path -split ';')) {
@ -360,7 +379,7 @@ if ($installWasManaged) {
Write-Info "Leaving $(Join-Path $installBinDir 'qwen.cmd') unchanged because no managed standalone runtime was removed."
}
Remove-UserPathEntry -BinDir $installBinDir
Remove-PathEntryFromAllScopes -BinDir $installBinDir
Remove-SourceMarker
if ([string]::IsNullOrEmpty($env:QWEN_INSTALL_BIN_DIR)) {
Remove-EmptyDirectory -Directory $installBinDir

View file

@ -346,7 +346,9 @@ describe('installation scripts', () => {
expect(script).toContain('qwen-code\\node\\node.exe');
expect(script).toContain('Archive contains symlinks or reparse points');
expect(script).toContain('unsafe path with control character');
expect(script).toContain('Failed to update user PATH');
expect(script).toContain('Failed to update !PATH_SCOPE! PATH');
expect(script).toContain("$ErrorActionPreference = 'Stop'; try");
expect(script).toContain('catch { exit 1 }');
expect(script).toContain('PRE_INSTALL_QWENS_LIST');
expect(script).toContain('QWEN_INSTALL_ROOT');
expect(script).toContain('npm fallback also failed');
@ -574,6 +576,35 @@ describe('release-script-utils', () => {
});
});
const STUB_BAT_CONTENT =
'@echo off\r\n' +
'set "VERSION=%QWEN_INSTALL_VERSION%"\r\n' +
'set "REPAIR_PATH=%QWEN_INSTALL_REPAIR_PATH%"\r\n' +
'set "PATH_SCOPE=%QWEN_INSTALL_PATH_SCOPE%"\r\n' +
'if "%VERSION%"=="" set "VERSION=latest"\r\n' +
'set "VERSION=latest"\r\n' +
'if "%~1"=="--version" set "VERSION=%~2"\r\n' +
'if /i "%~1"=="--repair-path" set "REPAIR_PATH=1"\r\n' +
'set "ARG_KEY=%~1"\r\n' +
'if /i "!ARG_KEY!"=="--path-scope" set "PATH_SCOPE=%~2"\r\n';
const STUB_SH_CONTENT =
'#!/usr/bin/env bash\n' +
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n';
const STUB_UNINSTALL_SH_CONTENT =
'#!/usr/bin/env bash\n' +
'is_qwen_standalone_install_dir() { return 0; }\n' +
'remove_shell_path_entry() { :; }\n' +
'QWEN_UNINSTALL_PURGE=""\n';
const STUB_UNINSTALL_PS1_CONTENT =
'function Test-QwenStandaloneInstallDir { return $true }\n' +
'function Remove-PathEntryFromAllScopes { }\n' +
'function Remove-CurrentCmdPathShim { }\n' +
'$env:QWEN_UNINSTALL_PURGE = ""\n';
describe('standalone release packaging', () => {
it('defines a standalone packaging script', () => {
const packageJson = JSON.parse(readScript('package.json'));
@ -1049,13 +1080,11 @@ describe('standalone release packaging', () => {
mkdirSync(sourceDir, { recursive: true });
writeFileSync(
path.join(sourceDir, 'install-qwen-standalone.sh'),
'#!/usr/bin/env bash\n' +
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n',
STUB_SH_CONTENT,
);
writeFileSync(
path.join(sourceDir, 'install-qwen-standalone.bat'),
'@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n',
STUB_BAT_CONTENT,
);
// The ps1 shim has every required behavior pattern but also contains
// a hardcoded $env:QWEN_INSTALL_VERSION assignment, which must be
@ -1070,17 +1099,11 @@ describe('standalone release packaging', () => {
);
writeFileSync(
path.join(sourceDir, 'uninstall-qwen-standalone.sh'),
'#!/usr/bin/env bash\n' +
'is_qwen_standalone_install_dir() { return 0; }\n' +
'remove_shell_path_entry() { :; }\n' +
'QWEN_UNINSTALL_PURGE=""\n',
STUB_UNINSTALL_SH_CONTENT,
);
writeFileSync(
path.join(sourceDir, 'uninstall-qwen-standalone.ps1'),
'function Test-QwenStandaloneInstallDir { return $true }\n' +
'function Remove-UserPathEntry { }\n' +
'function Remove-CurrentCmdPathShim { }\n' +
'$env:QWEN_UNINSTALL_PURGE = ""\n',
STUB_UNINSTALL_PS1_CONTENT,
);
await expect(
@ -1106,13 +1129,11 @@ describe('standalone release packaging', () => {
mkdirSync(sourceDir, { recursive: true });
writeFileSync(
path.join(sourceDir, 'install-qwen-standalone.sh'),
'#!/usr/bin/env bash\n' +
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n',
STUB_SH_CONTENT,
);
writeFileSync(
path.join(sourceDir, 'install-qwen-standalone.bat'),
'@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n',
STUB_BAT_CONTENT,
);
// ps1 contains the exact docstring shipped in production
// ("$env:QWEN_INSTALL_VERSION = 'vX.Y.Z'") as a `#` comment; the
@ -1128,17 +1149,11 @@ describe('standalone release packaging', () => {
);
writeFileSync(
path.join(sourceDir, 'uninstall-qwen-standalone.sh'),
'#!/usr/bin/env bash\n' +
'is_qwen_standalone_install_dir() { return 0; }\n' +
'remove_shell_path_entry() { :; }\n' +
'QWEN_UNINSTALL_PURGE=""\n',
STUB_UNINSTALL_SH_CONTENT,
);
writeFileSync(
path.join(sourceDir, 'uninstall-qwen-standalone.ps1'),
'function Test-QwenStandaloneInstallDir { return $true }\n' +
'function Remove-UserPathEntry { }\n' +
'function Remove-CurrentCmdPathShim { }\n' +
'$env:QWEN_UNINSTALL_PURGE = ""\n',
STUB_UNINSTALL_PS1_CONTENT,
);
// Build should succeed (only resolves; throws would fail the test).
@ -1804,7 +1819,7 @@ describe('standalone release packaging', () => {
expect(releaseWorkflow).not.toContain('verify_node_checksum()');
expect(releaseWorkflow).not.toContain('download_node()');
const createReleaseStepIndex = releaseWorkflow.indexOf(
"name: 'Create GitHub Release and Tag'",
"- name: 'Create GitHub Release and Tag'",
);
expect(createReleaseStepIndex).toBeGreaterThanOrEqual(0);
const createReleaseStep = releaseWorkflow.slice(createReleaseStepIndex);
@ -1844,21 +1859,25 @@ describe('standalone release packaging', () => {
);
const syncStepIndex = ossWorkflow.indexOf(
"name: 'Sync Release Assets to Aliyun OSS'",
"- name: 'Sync Release Assets to Aliyun OSS'",
);
const packageHostedStepIndex = ossWorkflow.indexOf(
"- name: 'Package Hosted Installation Assets'",
);
const verifyStepIndex = ossWorkflow.indexOf(
"name: 'Verify Aliyun OSS Release Assets'",
"- name: 'Verify Aliyun OSS Release Assets'",
);
const publishLatestStepIndex = ossWorkflow.indexOf(
"name: 'Publish Aliyun OSS Latest VERSION'",
"- name: 'Publish Aliyun OSS Latest VERSION'",
);
const syncHostedStepIndex = ossWorkflow.indexOf(
"name: 'Sync Hosted Installation Assets to Aliyun OSS'",
"- name: 'Sync Hosted Installation Assets to Aliyun OSS'",
);
const verifyHostedStepIndex = ossWorkflow.indexOf(
"name: 'Verify Aliyun OSS Hosted Installation Assets'",
"- name: 'Verify Aliyun OSS Hosted Installation Assets'",
);
expect(syncStepIndex).toBeGreaterThanOrEqual(0);
expect(packageHostedStepIndex).toBeGreaterThanOrEqual(0);
expect(verifyStepIndex).toBeGreaterThan(syncStepIndex);
expect(syncHostedStepIndex).toBeGreaterThan(verifyStepIndex);
expect(verifyHostedStepIndex).toBeGreaterThan(syncHostedStepIndex);
@ -1988,7 +2007,9 @@ describe('standalone release packaging', () => {
expect(uninstallPowerShellSource).toContain(
'Test-QwenStandaloneInstallDir',
);
expect(uninstallPowerShellSource).toContain('Remove-UserPathEntry');
expect(uninstallPowerShellSource).toContain(
'Remove-PathEntryFromAllScopes',
);
expect(uninstallPowerShellSource).toContain('Remove-CurrentCmdPathShim');
expect(uninstallPowerShellSource).toContain(
'Remove-RecordedCurrentCmdPathShim',