mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(cli): keep v5 settings migration idempotent (#5676)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (No-AK Smoke) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (No-AK Smoke) (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
This commit is contained in:
parent
01d28a1481
commit
2fd2104fa1
6 changed files with 63 additions and 45 deletions
|
|
@ -508,17 +508,17 @@ function setupAcpTest(
|
|||
expect(modelOption).toBeDefined();
|
||||
expect(modelOption!.currentValue).toBeTruthy();
|
||||
|
||||
// Test: Set model using set_config_option
|
||||
// Use openai model to avoid auth issues
|
||||
const openaiModel = newSession.models.availableModels.find((model) =>
|
||||
model.modelId.includes('openai'),
|
||||
);
|
||||
expect(openaiModel).toBeDefined();
|
||||
const modelId = modelOption!.currentValue;
|
||||
expect(
|
||||
newSession.models.availableModels.some(
|
||||
(model) => model.modelId === modelId,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const setModelResult = (await sendRequest('session/set_config_option', {
|
||||
sessionId: newSession.sessionId,
|
||||
configId: 'model',
|
||||
value: openaiModel!.modelId,
|
||||
value: modelId,
|
||||
})) as {
|
||||
configOptions: Array<{
|
||||
id: string;
|
||||
|
|
@ -535,7 +535,7 @@ function setupAcpTest(
|
|||
(opt) => opt.id === 'model',
|
||||
);
|
||||
expect(updatedModelOption).toBeDefined();
|
||||
expect(updatedModelOption!.currentValue).toBe(openaiModel!.modelId);
|
||||
expect(updatedModelOption!.currentValue).toBe(modelId);
|
||||
} catch (e) {
|
||||
if (stderr.length) {
|
||||
console.error('Agent stderr:', stderr.join(''));
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ describe('file-system', () => {
|
|||
await rig.setup('should correctly handle file paths with spaces');
|
||||
const fileName = 'my test file.txt';
|
||||
|
||||
const result = await rig.run(`write "hello" to "${fileName}"`);
|
||||
const result = await rig.run(
|
||||
`Use write_file to write exactly "hello" to "${fileName}".`,
|
||||
);
|
||||
|
||||
const foundToolCall = await rig.waitForToolCall('write_file');
|
||||
if (!foundToolCall) {
|
||||
|
|
@ -108,7 +110,7 @@ describe('file-system', () => {
|
|||
).toBeTruthy();
|
||||
|
||||
const newFileContent = rig.readFile(fileName);
|
||||
expect(newFileContent).toBe('hello');
|
||||
expect(newFileContent.trimEnd()).toBe('hello');
|
||||
});
|
||||
|
||||
it('should perform a read-then-write sequence', async () => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import {
|
|||
} from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const CURRENT_SETTINGS_VERSION = 5;
|
||||
|
||||
// Helper: list files under a directory recursively, returning relative paths
|
||||
function listFilesRecursive(dir: string, base = dir): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
|
|
@ -217,9 +219,7 @@ describe('QWEN_HOME environment variable', () => {
|
|||
);
|
||||
const migrated = JSON.parse(migratedRaw) as Record<string, unknown>;
|
||||
|
||||
// Migration should have bumped the version to the current SETTINGS_VERSION
|
||||
// (packages/cli/src/config/settings.ts). Update this when the schema bumps.
|
||||
expect(migrated['$version']).toBe(4);
|
||||
expect(migrated['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -249,10 +249,16 @@ describe('QWEN_HOME environment variable', () => {
|
|||
process.env['QWEN_HOME'] = customConfigDir;
|
||||
|
||||
// Seed QWEN_HOME with the current schema version so it shouldn't migrate.
|
||||
// Bump alongside SETTINGS_VERSION in packages/cli/src/config/settings.ts.
|
||||
writeFileSync(
|
||||
join(customConfigDir, 'settings.json'),
|
||||
JSON.stringify({ $version: 4, customKey: 'in-global-dir' }, null, 2),
|
||||
JSON.stringify(
|
||||
{
|
||||
$version: CURRENT_SETTINGS_VERSION,
|
||||
customKey: 'in-global-dir',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Overwrite the workspace settings.json with V1 format so migration is observable
|
||||
|
|
@ -284,14 +290,14 @@ describe('QWEN_HOME environment variable', () => {
|
|||
}
|
||||
|
||||
// The workspace settings.json must have been migrated to the current
|
||||
// SETTINGS_VERSION — proving the CLI read it from the workspace dir, not
|
||||
// from QWEN_HOME. Update the version when the schema bumps.
|
||||
// settings version, proving the CLI read it from the workspace dir, not
|
||||
// from QWEN_HOME.
|
||||
const workspaceRaw = readFileSync(workspaceSettingsPath, 'utf-8');
|
||||
const workspaceSettings = JSON.parse(workspaceRaw) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(workspaceSettings['$version']).toBe(4);
|
||||
expect(workspaceSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(workspaceSettings['customWorkspaceKey']).toBe('workspace-value');
|
||||
|
||||
// The QWEN_HOME settings.json must be unchanged (still at the version we wrote)
|
||||
|
|
|
|||
|
|
@ -27,18 +27,16 @@ const {
|
|||
v3GitCoAuthorBooleanSettings,
|
||||
} = workspacesSettings;
|
||||
|
||||
const CURRENT_SETTINGS_VERSION = 5;
|
||||
|
||||
/**
|
||||
* Integration tests for settings migration chain (V1 -> V2 -> V3 -> V4)
|
||||
* Integration tests for settings migration chain.
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. V1 settings are automatically migrated to V4 on CLI startup
|
||||
* 2. V2 settings are automatically migrated to V4 on CLI startup
|
||||
* 3. V3 settings are automatically migrated to V4 on CLI startup
|
||||
* 1. V1 settings are automatically migrated to current settings on CLI startup
|
||||
* 2. V2 settings are automatically migrated to current settings on CLI startup
|
||||
* 3. V3 settings are automatically migrated to current settings on CLI startup
|
||||
* 4. Migration is idempotent (running multiple times produces same result)
|
||||
*
|
||||
* The numeric assertions use the literal `4` to match
|
||||
* `SETTINGS_VERSION`; bump that constant and the literal together
|
||||
* when adding a future migration.
|
||||
*/
|
||||
describe('settings-migration', () => {
|
||||
let rig: TestRig;
|
||||
|
|
@ -99,8 +97,7 @@ describe('settings-migration', () => {
|
|||
// Read migrated settings
|
||||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Verify migration to V4 (current SETTINGS_VERSION)
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(migratedSettings['ui']).toEqual({
|
||||
theme: 'dark',
|
||||
hideTips: false,
|
||||
|
|
@ -142,7 +139,7 @@ describe('settings-migration', () => {
|
|||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Expected output based on stable test output
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(migratedSettings['tools']).toEqual({ autoAccept: false });
|
||||
expect(migratedSettings['context']).toEqual({ includeDirectories: [] });
|
||||
expect(migratedSettings['model']).toEqual({ name: ['gemini', 'claude'] });
|
||||
|
|
@ -166,8 +163,7 @@ describe('settings-migration', () => {
|
|||
// Read migrated settings
|
||||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Should be migrated to V4
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
// Legacy string values for ui/general should be preserved as-is (user data)
|
||||
expect(migratedSettings['ui']).toBe('legacy-ui-string');
|
||||
expect(migratedSettings['general']).toBe('legacy-general-string');
|
||||
|
|
@ -194,7 +190,7 @@ describe('settings-migration', () => {
|
|||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Expected output based on stable test output
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(migratedSettings['model']).toEqual({ name: 'qwen-plus' });
|
||||
expect(migratedSettings['ui']).toEqual({
|
||||
hideWindowTitle: true,
|
||||
|
|
@ -230,8 +226,7 @@ describe('settings-migration', () => {
|
|||
// Read migrated settings
|
||||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Verify migration to V4 (current SETTINGS_VERSION)
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
|
||||
// Verify disable* -> enable* conversion with inversion
|
||||
expect(
|
||||
|
|
@ -307,8 +302,7 @@ describe('settings-migration', () => {
|
|||
// Read migrated settings
|
||||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Should be updated to V4 version
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
// Other settings should remain unchanged
|
||||
expect(migratedSettings['ui']).toEqual({ theme: 'dark' });
|
||||
expect(migratedSettings['model']).toEqual({ name: 'gemini' });
|
||||
|
|
@ -335,7 +329,7 @@ describe('settings-migration', () => {
|
|||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Version metadata should still be normalized to current version
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
// Existing user content should be preserved
|
||||
expect(migratedSettings['customOnlyKey']).toBe('value');
|
||||
});
|
||||
|
|
@ -377,7 +371,7 @@ describe('settings-migration', () => {
|
|||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Coercible strings are migrated; invalid disable* values are removed.
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(migratedSettings['general']).toEqual({
|
||||
enableAutoUpdate: false,
|
||||
});
|
||||
|
|
@ -442,7 +436,7 @@ describe('settings-migration', () => {
|
|||
const migratedSettings = readSettingsFile(rig);
|
||||
|
||||
// Expected output based on stable test output
|
||||
expect(migratedSettings['$version']).toBe(4);
|
||||
expect(migratedSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
// Migration converts disable* to enable* by inverting the value
|
||||
// disableAutoUpdate: false -> enableAutoUpdate: true (inverted)
|
||||
// But disableUpdateNag: true may affect the consolidation
|
||||
|
|
@ -509,7 +503,7 @@ describe('settings-migration', () => {
|
|||
// V3 → V4 migration bumps the version; V3→V4 only touches
|
||||
// general.gitCoAuthor, so unrelated legacy disable* keys remain as-is
|
||||
// (V2→V3 ran on original V3 load, not re-applied here).
|
||||
expect(finalSettings['$version']).toBe(4);
|
||||
expect(finalSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(
|
||||
(finalSettings['general'] as Record<string, unknown>)?.[
|
||||
'disableAutoUpdate'
|
||||
|
|
@ -563,7 +557,7 @@ describe('settings-migration', () => {
|
|||
|
||||
const finalSettings = readSettingsFile(rig);
|
||||
|
||||
expect(finalSettings['$version']).toBe(4);
|
||||
expect(finalSettings['$version']).toBe(CURRENT_SETTINGS_VERSION);
|
||||
expect(
|
||||
(finalSettings['general'] as Record<string, unknown>)?.['gitCoAuthor'],
|
||||
).toEqual({ commit: false, pr: false });
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ describe('V4ToV5Migration', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for V4 settings without modelProviders', () => {
|
||||
expect(migration.shouldMigrate({ $version: 4 })).toBe(false);
|
||||
it('returns true for V4 settings without modelProviders', () => {
|
||||
expect(migration.shouldMigrate({ $version: 4 })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for V4 settings with already-object modelProviders', () => {
|
||||
it('returns true for V4 settings with already-object modelProviders', () => {
|
||||
expect(
|
||||
migration.shouldMigrate({
|
||||
$version: 4,
|
||||
|
|
@ -45,7 +45,7 @@ describe('V4ToV5Migration', () => {
|
|||
openai: { protocol: 'openai', models: [{ id: 'gpt-4o' }] },
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-object input', () => {
|
||||
|
|
@ -92,6 +92,19 @@ describe('V4ToV5Migration', () => {
|
|||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('bumps V4 settings without modelProviders to V5', () => {
|
||||
const { settings, warnings } = migration.migrate(
|
||||
{ $version: 4, custom: true },
|
||||
'user',
|
||||
) as {
|
||||
settings: Record<string, unknown>;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
expect(settings).toEqual({ $version: 5, custom: true });
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['openai', 'openai'],
|
||||
['qwen-oauth', 'qwen-oauth'],
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ export class V4ToV5Migration implements SettingsMigration {
|
|||
if (s['$version'] !== 4 && s['$version'] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
if (s['$version'] === 4) {
|
||||
return true;
|
||||
}
|
||||
const modelProviders = s['modelProviders'];
|
||||
if (typeof modelProviders !== 'object' || modelProviders === null) {
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue