mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-18 23:42:43 +00:00
test(installer): cover Windows release script regressions
This commit is contained in:
parent
bab96dc094
commit
3caf3e2f2a
2 changed files with 232 additions and 20 deletions
|
|
@ -3054,6 +3054,70 @@ describe('Windows installer end-to-end', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Windows PowerShell uninstaller end-to-end', () => {
|
||||
itOnWindows('prints help without deleting standalone files', () => {
|
||||
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-uninstall-test-'));
|
||||
|
||||
try {
|
||||
const installRoot = path.join(tmpDir, 'install');
|
||||
const installDir = path.join(installRoot, 'qwen-code');
|
||||
const home = path.join(tmpDir, 'home');
|
||||
createFakeWindowsStandaloneInstall(installRoot);
|
||||
|
||||
const output = runWindowsPowerShellScript(
|
||||
'scripts/installation/uninstall-qwen-standalone.ps1',
|
||||
['-Help'],
|
||||
{
|
||||
USERPROFILE: home,
|
||||
QWEN_INSTALL_ROOT: installRoot,
|
||||
},
|
||||
).toString();
|
||||
|
||||
expect(output).toContain('Usage:');
|
||||
expect(output).toContain('-Purge');
|
||||
expect(existsSync(installDir)).toBe(true);
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
itOnWindows('purges the source marker while preserving other config', () => {
|
||||
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-uninstall-test-'));
|
||||
|
||||
try {
|
||||
const installRoot = path.join(tmpDir, 'install');
|
||||
const installDir = path.join(installRoot, 'qwen-code');
|
||||
const installBinDir = path.join(installRoot, 'bin');
|
||||
const home = path.join(tmpDir, 'home');
|
||||
const qwenConfigDir = path.join(home, '.qwen');
|
||||
const sourceMarker = path.join(qwenConfigDir, 'source.json');
|
||||
const settingsFile = path.join(qwenConfigDir, 'settings.json');
|
||||
|
||||
createFakeWindowsStandaloneInstall(installRoot);
|
||||
mkdirSync(qwenConfigDir, { recursive: true });
|
||||
writeFileSync(sourceMarker, '{"source":"smoke"}\n');
|
||||
writeFileSync(settingsFile, '{"theme":"dark"}\n');
|
||||
|
||||
const output = runWindowsPowerShellScript(
|
||||
'scripts/installation/uninstall-qwen-standalone.ps1',
|
||||
['-Purge'],
|
||||
{
|
||||
USERPROFILE: home,
|
||||
QWEN_INSTALL_ROOT: installRoot,
|
||||
},
|
||||
).toString();
|
||||
|
||||
expect(output).toContain('Removed');
|
||||
expect(existsSync(installDir)).toBe(false);
|
||||
expect(existsSync(path.join(installBinDir, 'qwen.cmd'))).toBe(false);
|
||||
expect(existsSync(sourceMarker)).toBe(false);
|
||||
expect(readScript(settingsFile)).toContain('"theme":"dark"');
|
||||
} finally {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function ensureMinimalDist() {
|
||||
const distPath = path.resolve('dist');
|
||||
const backupPath = existsSync(distPath)
|
||||
|
|
@ -3185,6 +3249,30 @@ function createFakeWindowsStandaloneArchive(tmpDir) {
|
|||
return archive;
|
||||
}
|
||||
|
||||
function createFakeWindowsStandaloneInstall(installRoot) {
|
||||
const installDir = path.join(installRoot, 'qwen-code');
|
||||
const installBinDir = path.join(installRoot, 'bin');
|
||||
mkdirSync(path.join(installDir, 'bin'), { recursive: true });
|
||||
mkdirSync(path.join(installDir, 'node'), { recursive: true });
|
||||
mkdirSync(installBinDir, { recursive: true });
|
||||
|
||||
writeFileSync(
|
||||
path.join(installDir, 'manifest.json'),
|
||||
JSON.stringify({ name: '@qwen-code/qwen-code', target: 'win-x64' }),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(installDir, 'bin', 'qwen.cmd'),
|
||||
['@echo off', 'echo 0.0.0-smoke', ''].join('\r\n'),
|
||||
);
|
||||
writeFileSync(path.join(installDir, 'node', 'node.exe'), 'fake node.exe\n');
|
||||
writeFileSync(
|
||||
path.join(installBinDir, 'qwen.cmd'),
|
||||
['@echo off', `"${path.join(installDir, 'bin', 'qwen.cmd')}" %*`, ''].join(
|
||||
'\r\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function createFakeWindowsNpmTools(fakeBin) {
|
||||
mkdirSync(fakeBin, { recursive: true });
|
||||
writeFileSync(
|
||||
|
|
@ -3222,7 +3310,19 @@ function createFakeWindowsCurlCommand(fakeBin) {
|
|||
'if "%~1"=="" goto done_parse',
|
||||
'set "arg=%~1"',
|
||||
'if "!arg:~0,1!"=="-" (',
|
||||
' echo(!arg! | findstr /C:"o" >nul && (',
|
||||
' if /i "!arg!"=="-o" (',
|
||||
' shift',
|
||||
' set "destination=%~1"',
|
||||
' shift',
|
||||
' goto parse_args',
|
||||
' )',
|
||||
' if /i "!arg!"=="--output" (',
|
||||
' shift',
|
||||
' set "destination=%~1"',
|
||||
' shift',
|
||||
' goto parse_args',
|
||||
' )',
|
||||
' if not "!arg:~0,2!"=="--" if /i "!arg:~-1!"=="o" (',
|
||||
' shift',
|
||||
' set "destination=%~1"',
|
||||
' shift',
|
||||
|
|
@ -3457,18 +3557,61 @@ function runWindowsInstaller(
|
|||
|
||||
function runWindowsCommand(command, env = {}) {
|
||||
const prepared = prepareWindowsCommand(command, env);
|
||||
return execFileSync(
|
||||
process.env.ComSpec || 'cmd.exe',
|
||||
['/d', '/c', prepared.command],
|
||||
{
|
||||
env: {
|
||||
...prepared.env,
|
||||
try {
|
||||
return execFileSync(
|
||||
process.env.ComSpec || 'cmd.exe',
|
||||
['/d', '/c', prepared.command],
|
||||
{
|
||||
env: {
|
||||
...prepared.env,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
// cmd.exe parses the command string itself; preserve quoted paths.
|
||||
windowsVerbatimArguments: true,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
// cmd.exe parses the command string itself; preserve quoted paths.
|
||||
windowsVerbatimArguments: true,
|
||||
},
|
||||
);
|
||||
);
|
||||
} catch (error) {
|
||||
const processError = error;
|
||||
throw new Error(
|
||||
[
|
||||
processError.message,
|
||||
processError.stdout?.toString() || '',
|
||||
processError.stderr?.toString() || '',
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runWindowsPowerShellScript(scriptPath, args = [], env = {}) {
|
||||
try {
|
||||
return execFileSync(
|
||||
'powershell',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
path.resolve(scriptPath),
|
||||
...args,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const processError = error;
|
||||
throw new Error(
|
||||
[
|
||||
processError.message,
|
||||
processError.stdout?.toString() || '',
|
||||
processError.stderr?.toString() || '',
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const WINDOWS_COMMAND_ENV_OVERRIDES = [
|
||||
|
|
|
|||
|
|
@ -83,10 +83,76 @@ describe('parseUploadArgs', () => {
|
|||
});
|
||||
|
||||
describe('uploadAssets (integration)', () => {
|
||||
function prependProcessPath(directory) {
|
||||
const pathKeys = Object.keys(process.env).filter(
|
||||
(key) => key.toLowerCase() === 'path',
|
||||
);
|
||||
const pathKey = pathKeys[0] || 'PATH';
|
||||
const previousValues = new Map(
|
||||
pathKeys.map((key) => [key, process.env[key]]),
|
||||
);
|
||||
const nextValue = `${directory}${path.delimiter}${process.env[pathKey] || ''}`;
|
||||
|
||||
if (pathKeys.length === 0) {
|
||||
process.env[pathKey] = nextValue;
|
||||
} else {
|
||||
for (const key of pathKeys) {
|
||||
process.env[key] = nextValue;
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (previousValues.size === 0) {
|
||||
delete process.env[pathKey];
|
||||
return;
|
||||
}
|
||||
for (const key of pathKeys) {
|
||||
const previousValue = previousValues.get(key);
|
||||
if (previousValue === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = previousValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeOssutilShim(workDir, behavior = 'success') {
|
||||
fs.mkdirSync(workDir, { recursive: true });
|
||||
const ossutilPath = path.join(workDir, 'ossutil');
|
||||
const ossutilPath = path.join(
|
||||
workDir,
|
||||
process.platform === 'win32' ? 'ossutil.cmd' : 'ossutil',
|
||||
);
|
||||
const logPath = path.join(workDir, 'ossutil.log');
|
||||
if (process.platform === 'win32') {
|
||||
const successScript = [
|
||||
'@echo off',
|
||||
':log_args',
|
||||
'if "%~1"=="" goto done_log_args',
|
||||
`>>"${logPath}" echo(%~1`,
|
||||
'shift',
|
||||
'goto log_args',
|
||||
':done_log_args',
|
||||
'exit /b 0',
|
||||
'',
|
||||
].join('\r\n');
|
||||
const failScript = [
|
||||
'@echo off',
|
||||
':log_args',
|
||||
'if "%~1"=="" goto done_log_args',
|
||||
`>>"${logPath}" echo(%~1`,
|
||||
'shift',
|
||||
'goto log_args',
|
||||
':done_log_args',
|
||||
'exit /b 1',
|
||||
'',
|
||||
].join('\r\n');
|
||||
fs.writeFileSync(
|
||||
ossutilPath,
|
||||
behavior === 'fail' ? failScript : successScript,
|
||||
);
|
||||
return { ossutilPath, logPath };
|
||||
}
|
||||
|
||||
const successScript = `#!/usr/bin/env bash
|
||||
printf '%s\\n' "$@" >> "${logPath}"
|
||||
exit 0
|
||||
|
|
@ -115,8 +181,7 @@ exit 1
|
|||
const configPath = path.join(tmp, '.ossutilconfig');
|
||||
fs.writeFileSync(configPath, '[Credentials]\n');
|
||||
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.PATH = `${tmp}${path.delimiter}${previousPath}`;
|
||||
const restorePath = prependProcessPath(tmp);
|
||||
try {
|
||||
uploadAssets({
|
||||
assets,
|
||||
|
|
@ -125,7 +190,7 @@ exit 1
|
|||
prefix: 'releases/qwen-code/v0.0.0',
|
||||
});
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
restorePath();
|
||||
}
|
||||
|
||||
const log = fs.readFileSync(logPath, 'utf8');
|
||||
|
|
@ -145,14 +210,13 @@ exit 1
|
|||
it('aggregates failures from ossutil non-zero exits', async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-upload-fail-'));
|
||||
try {
|
||||
makeOssutilShim(tmp, 'fail');
|
||||
const { logPath } = makeOssutilShim(tmp, 'fail');
|
||||
const assetPath = path.join(tmp, 'asset.tar.gz');
|
||||
fs.writeFileSync(assetPath, 'asset');
|
||||
const configPath = path.join(tmp, '.ossutilconfig');
|
||||
fs.writeFileSync(configPath, '[Credentials]\n');
|
||||
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.PATH = `${tmp}${path.delimiter}${previousPath}`;
|
||||
const restorePath = prependProcessPath(tmp);
|
||||
try {
|
||||
expect(() =>
|
||||
uploadAssets({
|
||||
|
|
@ -162,8 +226,13 @@ exit 1
|
|||
prefix: 'releases/qwen-code/v0.0.0',
|
||||
}),
|
||||
).toThrow(/ossutil failed after 3 attempts/);
|
||||
const uploadAttempts = fs
|
||||
.readFileSync(logPath, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line === assetPath);
|
||||
expect(uploadAttempts).toHaveLength(3);
|
||||
} finally {
|
||||
process.env.PATH = previousPath;
|
||||
restorePath();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue