diff --git a/integration-tests/cli/acp-integration.test.ts b/integration-tests/cli/acp-integration.test.ts index 375dd57c72..5ab4d1f393 100644 --- a/integration-tests/cli/acp-integration.test.ts +++ b/integration-tests/cli/acp-integration.test.ts @@ -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('')); diff --git a/integration-tests/cli/file-system.test.ts b/integration-tests/cli/file-system.test.ts index bb3c0914cf..bfba679ea5 100644 --- a/integration-tests/cli/file-system.test.ts +++ b/integration-tests/cli/file-system.test.ts @@ -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 () => { diff --git a/integration-tests/cli/qwen-config-dir.test.ts b/integration-tests/cli/qwen-config-dir.test.ts index aa7268f423..e880c3ed70 100644 --- a/integration-tests/cli/qwen-config-dir.test.ts +++ b/integration-tests/cli/qwen-config-dir.test.ts @@ -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; - // 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) diff --git a/integration-tests/cli/settings-migration.test.ts b/integration-tests/cli/settings-migration.test.ts index e67a900407..82c9c538bf 100644 --- a/integration-tests/cli/settings-migration.test.ts +++ b/integration-tests/cli/settings-migration.test.ts @@ -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)?.[ '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)?.['gitCoAuthor'], ).toEqual({ commit: false, pr: false }); diff --git a/packages/cli/src/config/migration/versions/v4-to-v5.test.ts b/packages/cli/src/config/migration/versions/v4-to-v5.test.ts index 6340a0c9d4..7cc0ae037b 100644 --- a/packages/cli/src/config/migration/versions/v4-to-v5.test.ts +++ b/packages/cli/src/config/migration/versions/v4-to-v5.test.ts @@ -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; + warnings: string[]; + }; + + expect(settings).toEqual({ $version: 5, custom: true }); + expect(warnings).toEqual([]); + }); + it.each([ ['openai', 'openai'], ['qwen-oauth', 'qwen-oauth'], diff --git a/packages/cli/src/config/migration/versions/v4-to-v5.ts b/packages/cli/src/config/migration/versions/v4-to-v5.ts index a5eb4ce254..48db00ddb7 100644 --- a/packages/cli/src/config/migration/versions/v4-to-v5.ts +++ b/packages/cli/src/config/migration/versions/v4-to-v5.ts @@ -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;