From 0772d9250eddd8b1c2f74fa5d1faf90db9fb6129 Mon Sep 17 00:00:00 2001 From: Wang Lvyuan <74089601+LvyuanW@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:44:10 +0800 Subject: [PATCH] fix(admin/accounts): reset edit modal state on reopen --- .../components/account/EditAccountModal.vue | 500 +++++++++--------- .../__tests__/EditAccountModal.spec.ts | 159 ++++++ 2 files changed, 414 insertions(+), 245 deletions(-) create mode 100644 frontend/src/components/account/__tests__/EditAccountModal.spec.ts diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index c2f2f7d2..5f3da1b7 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1980,271 +1980,281 @@ const normalizePoolModeRetryCount = (value: number) => { return normalized } -watch( - () => props.account, - (newAccount) => { - if (newAccount) { - antigravityMixedChannelConfirmed.value = false - showMixedChannelWarning.value = false - mixedChannelWarningDetails.value = null - mixedChannelWarningRawMessage.value = '' - mixedChannelWarningAction.value = null - form.name = newAccount.name - form.notes = newAccount.notes || '' - form.proxy_id = newAccount.proxy_id - form.concurrency = newAccount.concurrency - form.load_factor = newAccount.load_factor ?? null - form.priority = newAccount.priority - form.rate_multiplier = newAccount.rate_multiplier ?? 1 - form.status = (newAccount.status === 'active' || newAccount.status === 'inactive' || newAccount.status === 'error') - ? newAccount.status - : 'active' - form.group_ids = newAccount.group_ids || [] - form.expires_at = newAccount.expires_at ?? null +const syncFormFromAccount = (newAccount: Account | null) => { + if (!newAccount) { + return + } + antigravityMixedChannelConfirmed.value = false + showMixedChannelWarning.value = false + mixedChannelWarningDetails.value = null + mixedChannelWarningRawMessage.value = '' + mixedChannelWarningAction.value = null + form.name = newAccount.name + form.notes = newAccount.notes || '' + form.proxy_id = newAccount.proxy_id + form.concurrency = newAccount.concurrency + form.load_factor = newAccount.load_factor ?? null + form.priority = newAccount.priority + form.rate_multiplier = newAccount.rate_multiplier ?? 1 + form.status = (newAccount.status === 'active' || newAccount.status === 'inactive' || newAccount.status === 'error') + ? newAccount.status + : 'active' + form.group_ids = newAccount.group_ids || [] + form.expires_at = newAccount.expires_at ?? null - // Load intercept warmup requests setting (applies to all account types) - const credentials = newAccount.credentials as Record | undefined - interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true - autoPauseOnExpired.value = newAccount.auto_pause_on_expired === true + // Load intercept warmup requests setting (applies to all account types) + const credentials = newAccount.credentials as Record | undefined + interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true + autoPauseOnExpired.value = newAccount.auto_pause_on_expired === true - // Load mixed scheduling setting (only for antigravity accounts) - mixedScheduling.value = false - allowOverages.value = false - const extra = newAccount.extra as Record | undefined - mixedScheduling.value = extra?.mixed_scheduling === true - allowOverages.value = extra?.allow_overages === true + // Load mixed scheduling setting (only for antigravity accounts) + mixedScheduling.value = false + allowOverages.value = false + const extra = newAccount.extra as Record | undefined + mixedScheduling.value = extra?.mixed_scheduling === true + allowOverages.value = extra?.allow_overages === true - // Load OpenAI passthrough toggle (OpenAI OAuth/API Key) - openaiPassthroughEnabled.value = false - openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF - openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF - codexCLIOnlyEnabled.value = false - anthropicPassthroughEnabled.value = false - if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) { - openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true - openaiOAuthResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, { - modeKey: 'openai_oauth_responses_websockets_v2_mode', - enabledKey: 'openai_oauth_responses_websockets_v2_enabled', - fallbackEnabledKeys: ['responses_websockets_v2_enabled', 'openai_ws_enabled'], - defaultMode: OPENAI_WS_MODE_OFF - }) - openaiAPIKeyResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, { - modeKey: 'openai_apikey_responses_websockets_v2_mode', - enabledKey: 'openai_apikey_responses_websockets_v2_enabled', - fallbackEnabledKeys: ['responses_websockets_v2_enabled', 'openai_ws_enabled'], - defaultMode: OPENAI_WS_MODE_OFF - }) - if (newAccount.type === 'oauth') { - codexCLIOnlyEnabled.value = extra?.codex_cli_only === true - } - } - if (newAccount.platform === 'anthropic' && newAccount.type === 'apikey') { - anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true - } + // Load OpenAI passthrough toggle (OpenAI OAuth/API Key) + openaiPassthroughEnabled.value = false + openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF + openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF + codexCLIOnlyEnabled.value = false + anthropicPassthroughEnabled.value = false + if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) { + openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true + openaiOAuthResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, { + modeKey: 'openai_oauth_responses_websockets_v2_mode', + enabledKey: 'openai_oauth_responses_websockets_v2_enabled', + fallbackEnabledKeys: ['responses_websockets_v2_enabled', 'openai_ws_enabled'], + defaultMode: OPENAI_WS_MODE_OFF + }) + openaiAPIKeyResponsesWebSocketV2Mode.value = resolveOpenAIWSModeFromExtra(extra, { + modeKey: 'openai_apikey_responses_websockets_v2_mode', + enabledKey: 'openai_apikey_responses_websockets_v2_enabled', + fallbackEnabledKeys: ['responses_websockets_v2_enabled', 'openai_ws_enabled'], + defaultMode: OPENAI_WS_MODE_OFF + }) + if (newAccount.type === 'oauth') { + codexCLIOnlyEnabled.value = extra?.codex_cli_only === true + } + } + if (newAccount.platform === 'anthropic' && newAccount.type === 'apikey') { + anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true + } - // Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above) - if (newAccount.type === 'apikey' || newAccount.type === 'bedrock') { - const quotaVal = extra?.quota_limit as number | undefined - editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null - const dailyVal = extra?.quota_daily_limit as number | undefined - editQuotaDailyLimit.value = (dailyVal && dailyVal > 0) ? dailyVal : null - const weeklyVal = extra?.quota_weekly_limit as number | undefined - editQuotaWeeklyLimit.value = (weeklyVal && weeklyVal > 0) ? weeklyVal : null - // Load quota reset mode config - editDailyResetMode.value = (extra?.quota_daily_reset_mode as 'rolling' | 'fixed') || null - editDailyResetHour.value = (extra?.quota_daily_reset_hour as number) ?? null - editWeeklyResetMode.value = (extra?.quota_weekly_reset_mode as 'rolling' | 'fixed') || null - editWeeklyResetDay.value = (extra?.quota_weekly_reset_day as number) ?? null - editWeeklyResetHour.value = (extra?.quota_weekly_reset_hour as number) ?? null - editResetTimezone.value = (extra?.quota_reset_timezone as string) || null + // Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above) + if (newAccount.type === 'apikey' || newAccount.type === 'bedrock') { + const quotaVal = extra?.quota_limit as number | undefined + editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null + const dailyVal = extra?.quota_daily_limit as number | undefined + editQuotaDailyLimit.value = (dailyVal && dailyVal > 0) ? dailyVal : null + const weeklyVal = extra?.quota_weekly_limit as number | undefined + editQuotaWeeklyLimit.value = (weeklyVal && weeklyVal > 0) ? weeklyVal : null + // Load quota reset mode config + editDailyResetMode.value = (extra?.quota_daily_reset_mode as 'rolling' | 'fixed') || null + editDailyResetHour.value = (extra?.quota_daily_reset_hour as number) ?? null + editWeeklyResetMode.value = (extra?.quota_weekly_reset_mode as 'rolling' | 'fixed') || null + editWeeklyResetDay.value = (extra?.quota_weekly_reset_day as number) ?? null + editWeeklyResetHour.value = (extra?.quota_weekly_reset_hour as number) ?? null + editResetTimezone.value = (extra?.quota_reset_timezone as string) || null + } else { + editQuotaLimit.value = null + editQuotaDailyLimit.value = null + editQuotaWeeklyLimit.value = null + editDailyResetMode.value = null + editDailyResetHour.value = null + editWeeklyResetMode.value = null + editWeeklyResetDay.value = null + editWeeklyResetHour.value = null + editResetTimezone.value = null + } + + // Load antigravity model mapping (Antigravity 只支持映射模式) + if (newAccount.platform === 'antigravity') { + const credentials = newAccount.credentials as Record | undefined + + // Antigravity 始终使用映射模式 + antigravityModelRestrictionMode.value = 'mapping' + antigravityWhitelistModels.value = [] + + // 从 model_mapping 读取映射配置 + const rawAgMapping = credentials?.model_mapping as Record | undefined + if (rawAgMapping && typeof rawAgMapping === 'object') { + const entries = Object.entries(rawAgMapping) + // 无论是白名单样式(key===value)还是真正的映射,都统一转换为映射列表 + antigravityModelMappings.value = entries.map(([from, to]) => ({ from, to })) + } else { + // 兼容旧数据:从 model_whitelist 读取,转换为映射格式 + const rawWhitelist = credentials?.model_whitelist + if (Array.isArray(rawWhitelist) && rawWhitelist.length > 0) { + antigravityModelMappings.value = rawWhitelist + .map((v) => String(v).trim()) + .filter((v) => v.length > 0) + .map((m) => ({ from: m, to: m })) } else { - editQuotaLimit.value = null - editQuotaDailyLimit.value = null - editQuotaWeeklyLimit.value = null - editDailyResetMode.value = null - editDailyResetHour.value = null - editWeeklyResetMode.value = null - editWeeklyResetDay.value = null - editWeeklyResetHour.value = null - editResetTimezone.value = null - } - - // Load antigravity model mapping (Antigravity 只支持映射模式) - if (newAccount.platform === 'antigravity') { - const credentials = newAccount.credentials as Record | undefined - - // Antigravity 始终使用映射模式 - antigravityModelRestrictionMode.value = 'mapping' - antigravityWhitelistModels.value = [] - - // 从 model_mapping 读取映射配置 - const rawAgMapping = credentials?.model_mapping as Record | undefined - if (rawAgMapping && typeof rawAgMapping === 'object') { - const entries = Object.entries(rawAgMapping) - // 无论是白名单样式(key===value)还是真正的映射,都统一转换为映射列表 - antigravityModelMappings.value = entries.map(([from, to]) => ({ from, to })) - } else { - // 兼容旧数据:从 model_whitelist 读取,转换为映射格式 - const rawWhitelist = credentials?.model_whitelist - if (Array.isArray(rawWhitelist) && rawWhitelist.length > 0) { - antigravityModelMappings.value = rawWhitelist - .map((v) => String(v).trim()) - .filter((v) => v.length > 0) - .map((m) => ({ from: m, to: m })) - } else { - antigravityModelMappings.value = [] - } - } - } else { - antigravityModelRestrictionMode.value = 'mapping' - antigravityWhitelistModels.value = [] antigravityModelMappings.value = [] } + } + } else { + antigravityModelRestrictionMode.value = 'mapping' + antigravityWhitelistModels.value = [] + antigravityModelMappings.value = [] + } - // Load quota control settings (Anthropic OAuth/SetupToken only) - loadQuotaControlSettings(newAccount) + // Load quota control settings (Anthropic OAuth/SetupToken only) + loadQuotaControlSettings(newAccount) - loadTempUnschedRules(credentials) + loadTempUnschedRules(credentials) - // Initialize API Key fields for apikey type - if (newAccount.type === 'apikey' && newAccount.credentials) { - const credentials = newAccount.credentials as Record - const platformDefaultUrl = - newAccount.platform === 'openai' || newAccount.platform === 'sora' - ? 'https://api.openai.com' - : newAccount.platform === 'gemini' - ? 'https://generativelanguage.googleapis.com' - : 'https://api.anthropic.com' - editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl + // Initialize API Key fields for apikey type + if (newAccount.type === 'apikey' && newAccount.credentials) { + const credentials = newAccount.credentials as Record + const platformDefaultUrl = + newAccount.platform === 'openai' || newAccount.platform === 'sora' + ? 'https://api.openai.com' + : newAccount.platform === 'gemini' + ? 'https://generativelanguage.googleapis.com' + : 'https://api.anthropic.com' + editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl - // Load model mappings and detect mode - const existingMappings = credentials.model_mapping as Record | undefined - if (existingMappings && typeof existingMappings === 'object') { - const entries = Object.entries(existingMappings) + // Load model mappings and detect mode + const existingMappings = credentials.model_mapping as Record | undefined + if (existingMappings && typeof existingMappings === 'object') { + const entries = Object.entries(existingMappings) - // Detect if this is whitelist mode (all from === to) or mapping mode - const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) + // Detect if this is whitelist mode (all from === to) or mapping mode + const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) - if (isWhitelistMode) { - // Whitelist mode: populate allowedModels - modelRestrictionMode.value = 'whitelist' - allowedModels.value = entries.map(([from]) => from) - modelMappings.value = [] - } else { - // Mapping mode: populate modelMappings - modelRestrictionMode.value = 'mapping' - modelMappings.value = entries.map(([from, to]) => ({ from, to })) - allowedModels.value = [] - } - } else { - // No mappings: default to whitelist mode with empty selection (allow all) - modelRestrictionMode.value = 'whitelist' - modelMappings.value = [] - allowedModels.value = [] - } - - // Load pool mode - poolModeEnabled.value = credentials.pool_mode === true - poolModeRetryCount.value = normalizePoolModeRetryCount( - Number(credentials.pool_mode_retry_count ?? DEFAULT_POOL_MODE_RETRY_COUNT) - ) - - // Load custom error codes - customErrorCodesEnabled.value = credentials.custom_error_codes_enabled === true - const existingErrorCodes = credentials.custom_error_codes as number[] | undefined - if (existingErrorCodes && Array.isArray(existingErrorCodes)) { - selectedErrorCodes.value = [...existingErrorCodes] - } else { - selectedErrorCodes.value = [] - } - } else if (newAccount.type === 'bedrock' && newAccount.credentials) { - const bedrockCreds = newAccount.credentials as Record - const authMode = (bedrockCreds.auth_mode as string) || 'sigv4' - editBedrockRegion.value = (bedrockCreds.aws_region as string) || '' - editBedrockForceGlobal.value = (bedrockCreds.aws_force_global as string) === 'true' - - if (authMode === 'apikey') { - editBedrockApiKeyValue.value = '' - } else { - editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || '' - editBedrockSecretAccessKey.value = '' - editBedrockSessionToken.value = '' - } - - // Load pool mode for bedrock - poolModeEnabled.value = bedrockCreds.pool_mode === true - const retryCount = bedrockCreds.pool_mode_retry_count - poolModeRetryCount.value = (typeof retryCount === 'number' && retryCount >= 0) ? retryCount : DEFAULT_POOL_MODE_RETRY_COUNT - - // Load quota limits for bedrock - const bedrockExtra = (newAccount.extra as Record) || {} - editQuotaLimit.value = typeof bedrockExtra.quota_limit === 'number' ? bedrockExtra.quota_limit : null - editQuotaDailyLimit.value = typeof bedrockExtra.quota_daily_limit === 'number' ? bedrockExtra.quota_daily_limit : null - editQuotaWeeklyLimit.value = typeof bedrockExtra.quota_weekly_limit === 'number' ? bedrockExtra.quota_weekly_limit : null - - // Load model mappings for bedrock - const existingMappings = bedrockCreds.model_mapping as Record | undefined - if (existingMappings && typeof existingMappings === 'object') { - const entries = Object.entries(existingMappings) - const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) - if (isWhitelistMode) { - modelRestrictionMode.value = 'whitelist' - allowedModels.value = entries.map(([from]) => from) - modelMappings.value = [] - } else { - modelRestrictionMode.value = 'mapping' - modelMappings.value = entries.map(([from, to]) => ({ from, to })) - allowedModels.value = [] - } - } else { - modelRestrictionMode.value = 'whitelist' - modelMappings.value = [] - allowedModels.value = [] - } - } else if (newAccount.type === 'upstream' && newAccount.credentials) { - const credentials = newAccount.credentials as Record - editBaseUrl.value = (credentials.base_url as string) || '' + if (isWhitelistMode) { + // Whitelist mode: populate allowedModels + modelRestrictionMode.value = 'whitelist' + allowedModels.value = entries.map(([from]) => from) + modelMappings.value = [] } else { - const platformDefaultUrl = - newAccount.platform === 'openai' || newAccount.platform === 'sora' - ? 'https://api.openai.com' - : newAccount.platform === 'gemini' - ? 'https://generativelanguage.googleapis.com' - : 'https://api.anthropic.com' - editBaseUrl.value = platformDefaultUrl + // Mapping mode: populate modelMappings + modelRestrictionMode.value = 'mapping' + modelMappings.value = entries.map(([from, to]) => ({ from, to })) + allowedModels.value = [] + } + } else { + // No mappings: default to whitelist mode with empty selection (allow all) + modelRestrictionMode.value = 'whitelist' + modelMappings.value = [] + allowedModels.value = [] + } - // Load model mappings for OpenAI OAuth accounts - if (newAccount.platform === 'openai' && newAccount.credentials) { - const oauthCredentials = newAccount.credentials as Record - const existingMappings = oauthCredentials.model_mapping as Record | undefined - if (existingMappings && typeof existingMappings === 'object') { - const entries = Object.entries(existingMappings) - const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) - if (isWhitelistMode) { - modelRestrictionMode.value = 'whitelist' - allowedModels.value = entries.map(([from]) => from) - modelMappings.value = [] - } else { - modelRestrictionMode.value = 'mapping' - modelMappings.value = entries.map(([from, to]) => ({ from, to })) - allowedModels.value = [] - } - } else { - modelRestrictionMode.value = 'whitelist' - modelMappings.value = [] - allowedModels.value = [] - } - } else { + // Load pool mode + poolModeEnabled.value = credentials.pool_mode === true + poolModeRetryCount.value = normalizePoolModeRetryCount( + Number(credentials.pool_mode_retry_count ?? DEFAULT_POOL_MODE_RETRY_COUNT) + ) + + // Load custom error codes + customErrorCodesEnabled.value = credentials.custom_error_codes_enabled === true + const existingErrorCodes = credentials.custom_error_codes as number[] | undefined + if (existingErrorCodes && Array.isArray(existingErrorCodes)) { + selectedErrorCodes.value = [...existingErrorCodes] + } else { + selectedErrorCodes.value = [] + } + } else if (newAccount.type === 'bedrock' && newAccount.credentials) { + const bedrockCreds = newAccount.credentials as Record + const authMode = (bedrockCreds.auth_mode as string) || 'sigv4' + editBedrockRegion.value = (bedrockCreds.aws_region as string) || '' + editBedrockForceGlobal.value = (bedrockCreds.aws_force_global as string) === 'true' + + if (authMode === 'apikey') { + editBedrockApiKeyValue.value = '' + } else { + editBedrockAccessKeyId.value = (bedrockCreds.aws_access_key_id as string) || '' + editBedrockSecretAccessKey.value = '' + editBedrockSessionToken.value = '' + } + + // Load pool mode for bedrock + poolModeEnabled.value = bedrockCreds.pool_mode === true + const retryCount = bedrockCreds.pool_mode_retry_count + poolModeRetryCount.value = (typeof retryCount === 'number' && retryCount >= 0) ? retryCount : DEFAULT_POOL_MODE_RETRY_COUNT + + // Load quota limits for bedrock + const bedrockExtra = (newAccount.extra as Record) || {} + editQuotaLimit.value = typeof bedrockExtra.quota_limit === 'number' ? bedrockExtra.quota_limit : null + editQuotaDailyLimit.value = typeof bedrockExtra.quota_daily_limit === 'number' ? bedrockExtra.quota_daily_limit : null + editQuotaWeeklyLimit.value = typeof bedrockExtra.quota_weekly_limit === 'number' ? bedrockExtra.quota_weekly_limit : null + + // Load model mappings for bedrock + const existingMappings = bedrockCreds.model_mapping as Record | undefined + if (existingMappings && typeof existingMappings === 'object') { + const entries = Object.entries(existingMappings) + const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) + if (isWhitelistMode) { + modelRestrictionMode.value = 'whitelist' + allowedModels.value = entries.map(([from]) => from) + modelMappings.value = [] + } else { + modelRestrictionMode.value = 'mapping' + modelMappings.value = entries.map(([from, to]) => ({ from, to })) + allowedModels.value = [] + } + } else { + modelRestrictionMode.value = 'whitelist' + modelMappings.value = [] + allowedModels.value = [] + } + } else if (newAccount.type === 'upstream' && newAccount.credentials) { + const credentials = newAccount.credentials as Record + editBaseUrl.value = (credentials.base_url as string) || '' + } else { + const platformDefaultUrl = + newAccount.platform === 'openai' || newAccount.platform === 'sora' + ? 'https://api.openai.com' + : newAccount.platform === 'gemini' + ? 'https://generativelanguage.googleapis.com' + : 'https://api.anthropic.com' + editBaseUrl.value = platformDefaultUrl + + // Load model mappings for OpenAI OAuth accounts + if (newAccount.platform === 'openai' && newAccount.credentials) { + const oauthCredentials = newAccount.credentials as Record + const existingMappings = oauthCredentials.model_mapping as Record | undefined + if (existingMappings && typeof existingMappings === 'object') { + const entries = Object.entries(existingMappings) + const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to) + if (isWhitelistMode) { modelRestrictionMode.value = 'whitelist' + allowedModels.value = entries.map(([from]) => from) modelMappings.value = [] + } else { + modelRestrictionMode.value = 'mapping' + modelMappings.value = entries.map(([from, to]) => ({ from, to })) allowedModels.value = [] } - poolModeEnabled.value = false - poolModeRetryCount.value = DEFAULT_POOL_MODE_RETRY_COUNT - customErrorCodesEnabled.value = false - selectedErrorCodes.value = [] + } else { + modelRestrictionMode.value = 'whitelist' + modelMappings.value = [] + allowedModels.value = [] } - editApiKey.value = '' + } else { + modelRestrictionMode.value = 'whitelist' + modelMappings.value = [] + allowedModels.value = [] + } + poolModeEnabled.value = false + poolModeRetryCount.value = DEFAULT_POOL_MODE_RETRY_COUNT + customErrorCodesEnabled.value = false + selectedErrorCodes.value = [] + } + editApiKey.value = '' +} + +watch( + [() => props.show, () => props.account], + ([show, newAccount], [wasShow, previousAccount]) => { + if (!show || !newAccount) { + return + } + if (!wasShow || newAccount !== previousAccount) { + syncFormFromAccount(newAccount) } }, { immediate: true } diff --git a/frontend/src/components/account/__tests__/EditAccountModal.spec.ts b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts new file mode 100644 index 00000000..e3260168 --- /dev/null +++ b/frontend/src/components/account/__tests__/EditAccountModal.spec.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import { mount } from '@vue/test-utils' + +const { updateAccountMock, checkMixedChannelRiskMock } = vi.hoisted(() => ({ + updateAccountMock: vi.fn(), + checkMixedChannelRiskMock: vi.fn() +})) + +vi.mock('@/stores/app', () => ({ + useAppStore: () => ({ + showError: vi.fn(), + showSuccess: vi.fn(), + showInfo: vi.fn() + }) +})) + +vi.mock('@/stores/auth', () => ({ + useAuthStore: () => ({ + isSimpleMode: true + }) +})) + +vi.mock('@/api/admin', () => ({ + adminAPI: { + accounts: { + update: updateAccountMock, + checkMixedChannelRisk: checkMixedChannelRiskMock + } + } +})) + +vi.mock('@/api/admin/accounts', () => ({ + getAntigravityDefaultModelMapping: vi.fn() +})) + +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') + return { + ...actual, + useI18n: () => ({ + t: (key: string) => key + }) + } +}) + +import EditAccountModal from '../EditAccountModal.vue' + +const BaseDialogStub = defineComponent({ + name: 'BaseDialog', + props: { + show: { + type: Boolean, + default: false + } + }, + template: '
' +}) + +const ModelWhitelistSelectorStub = defineComponent({ + name: 'ModelWhitelistSelector', + props: { + modelValue: { + type: Array, + default: () => [] + } + }, + emits: ['update:modelValue'], + template: ` +
+ + + {{ Array.isArray(modelValue) ? modelValue.join(',') : '' }} + +
+ ` +}) + +function buildAccount() { + return { + id: 1, + name: 'OpenAI Key', + notes: '', + platform: 'openai', + type: 'apikey', + credentials: { + api_key: 'sk-test', + base_url: 'https://api.openai.com', + model_mapping: { + 'gpt-5.2': 'gpt-5.2' + } + }, + extra: {}, + proxy_id: null, + concurrency: 1, + priority: 1, + rate_multiplier: 1, + status: 'active', + group_ids: [], + expires_at: null, + auto_pause_on_expired: false + } as any +} + +function mountModal(account = buildAccount()) { + return mount(EditAccountModal, { + props: { + show: true, + account, + proxies: [], + groups: [] + }, + global: { + stubs: { + BaseDialog: BaseDialogStub, + Select: true, + Icon: true, + ProxySelector: true, + GroupSelector: true, + ModelWhitelistSelector: ModelWhitelistSelectorStub + } + } + }) +} + +describe('EditAccountModal', () => { + it('reopening the same account rehydrates the OpenAI whitelist from props', async () => { + const account = buildAccount() + updateAccountMock.mockReset() + checkMixedChannelRiskMock.mockReset() + checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false }) + updateAccountMock.mockResolvedValue(account) + + const wrapper = mountModal(account) + + expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2') + + await wrapper.get('[data-testid="rewrite-to-snapshot"]').trigger('click') + expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2-2025-12-11') + + await wrapper.setProps({ show: false }) + await wrapper.setProps({ show: true }) + + expect(wrapper.get('[data-testid="model-whitelist-value"]').text()).toBe('gpt-5.2') + + await wrapper.get('form#edit-account-form').trigger('submit.prevent') + + expect(updateAccountMock).toHaveBeenCalledTimes(1) + expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.model_mapping).toEqual({ + 'gpt-5.2': 'gpt-5.2' + }) + }) +})