diff --git a/src/features/config/ConfigPage.tsx b/src/features/config/ConfigPage.tsx index e21d9fce..85f35076 100644 --- a/src/features/config/ConfigPage.tsx +++ b/src/features/config/ConfigPage.tsx @@ -43,7 +43,6 @@ import { SectionConnectivity } from './components/sections/SectionConnectivity'; import { SectionLogging } from './components/sections/SectionLogging'; import { SectionNetwork } from './components/sections/SectionNetwork'; import { SectionPayload } from './components/sections/SectionPayload'; -import { SectionQuota } from './components/sections/SectionQuota'; import { SectionStreaming } from './components/sections/SectionStreaming'; import styles from './ConfigPage.module.scss'; @@ -283,8 +282,6 @@ export function ConfigPage() { return ; case 'logging': return ; - case 'quota': - return ; case 'streaming': return ; case 'advanced': diff --git a/src/features/config/components/fields/sharedFields.tsx b/src/features/config/components/fields/sharedFields.tsx index 99432952..56c754ae 100644 --- a/src/features/config/components/fields/sharedFields.tsx +++ b/src/features/config/components/fields/sharedFields.tsx @@ -1,4 +1,4 @@ -// 8 个高频字段的唯一渲染源:SectionCommon(常用 tab)与各正典分区共用这些组件, +// 6 个高频字段的唯一渲染源:SectionCommon(常用 tab)与各正典分区共用这些组件, // 两处渲染结构性不可能漂移(旧简单模式靠共享 JSX 常量达成同一目的)。 // 注意:只挂载激活 tab,所以设置行的 DOM id 不会重复。 @@ -138,38 +138,3 @@ export function LoggingToFileToggle({ values, disabled, onChange, wide }: Shared /> ); } - -export function QuotaSwitchProjectToggle({ values, disabled, onChange, wide }: SharedFieldProps) { - const { t } = useTranslation(); - return ( - onChange({ quotaSwitchProject })} - /> - ); -} - -export function QuotaSwitchPreviewModelToggle({ - values, - disabled, - onChange, - wide, -}: SharedFieldProps) { - const { t } = useTranslation(); - return ( - onChange({ quotaSwitchPreviewModel })} - /> - ); -} diff --git a/src/features/config/components/sections/SectionAdvanced.tsx b/src/features/config/components/sections/SectionAdvanced.tsx index b6a0a7ea..469940b6 100644 --- a/src/features/config/components/sections/SectionAdvanced.tsx +++ b/src/features/config/components/sections/SectionAdvanced.tsx @@ -21,7 +21,7 @@ import { SectionOAuthBehavior } from './SectionOAuthBehavior'; const Icon = CONFIG_TAB_ICONS.advanced; -/** 06 高级与实验:插件源、供应商敏感词、签名缓存与请求头默认值。 */ +/** 05 高级与实验:插件源、供应商敏感词、签名缓存与请求头默认值。 */ export function SectionAdvanced({ values, validationErrors, @@ -127,6 +127,15 @@ export function SectionAdvanced({ defaultOpen={false} > + + onChange({ quotaAntigravityCredits })} + /> + - - ); diff --git a/src/features/config/components/sections/SectionPayload.tsx b/src/features/config/components/sections/SectionPayload.tsx index 7a5a0690..9f7bd977 100644 --- a/src/features/config/components/sections/SectionPayload.tsx +++ b/src/features/config/components/sections/SectionPayload.tsx @@ -16,7 +16,7 @@ export type SectionPayloadProps = ConfigSectionProps & { hasPayloadValidationErrors: boolean; }; -/** 07 Payload 配置:默认值 / 原始 JSON / 覆盖 / 过滤 五个规则组。 */ +/** 06 Payload 配置:默认值 / 原始 JSON / 覆盖 / 过滤 五个规则组。 */ export function SectionPayload({ values, disabled, diff --git a/src/features/config/components/sections/SectionQuota.tsx b/src/features/config/components/sections/SectionQuota.tsx deleted file mode 100644 index 35b75d0a..00000000 --- a/src/features/config/components/sections/SectionQuota.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { CONFIG_TAB_ICONS, SECTION_INDEX_LABELS } from '../../constants'; -import type { ConfigSectionProps } from '../../types'; -import { SectionCard } from '../SectionCard'; -import { SettingList, ToggleSetting } from '../fields/FieldPrimitives'; -import { QuotaSwitchPreviewModelToggle, QuotaSwitchProjectToggle } from '../fields/sharedFields'; - -const Icon = CONFIG_TAB_ICONS.quota; - -/** 04 配额回退:配额耗尽时的回退策略(两个开关默认 true)。 */ -export function SectionQuota({ values, disabled, animateIn, onChange }: ConfigSectionProps) { - const { t } = useTranslation(); - const fieldProps = { values, disabled, onChange }; - - return ( - } - title={t('config_management.visual.sections.quota.title')} - description={t('config_management.visual.sections.quota.description')} - animateIn={animateIn} - > - - - - onChange({ quotaAntigravityCredits })} - /> - - - ); -} diff --git a/src/features/config/components/sections/SectionStreaming.tsx b/src/features/config/components/sections/SectionStreaming.tsx index b1d82d78..f2163ca0 100644 --- a/src/features/config/components/sections/SectionStreaming.tsx +++ b/src/features/config/components/sections/SectionStreaming.tsx @@ -7,7 +7,7 @@ import { getValidationMessage } from '../blocks/shared'; const Icon = CONFIG_TAB_ICONS.streaming; -/** 05 流式传输:keepalive 与 bootstrap 重试;nonstream-keepalive-interval 是顶层 YAML 键。 */ +/** 04 流式传输:keepalive 与 bootstrap 重试;nonstream-keepalive-interval 是顶层 YAML 键。 */ export function SectionStreaming({ values, validationErrors, diff --git a/src/features/config/constants.ts b/src/features/config/constants.ts index 96d828a2..05fa5297 100644 --- a/src/features/config/constants.ts +++ b/src/features/config/constants.ts @@ -7,7 +7,6 @@ import { IconScrollText, IconShield, IconSlidersHorizontal, - IconTimer, type IconProps, } from '@/components/ui/icons'; import type { VisualConfigFieldPath } from '@/types/visualConfig'; @@ -16,14 +15,13 @@ import type { VisualSectionId } from './searchIndex'; /** 编辑模式:可视化表单 or YAML 源码。 */ export type ConfigEditorMode = 'visual' | 'source'; -/** 顶部 tabs:'common'(常用,原简单模式的继任者)+ 7 个正典分区。 */ +/** 顶部 tabs:'common'(常用,原简单模式的继任者)+ 6 个正典分区。 */ export type ConfigTabId = 'common' | VisualSectionId; export const CONFIG_SECTION_IDS = [ 'connectivity', 'network', 'logging', - 'quota', 'streaming', 'advanced', 'payload', @@ -31,15 +29,14 @@ export const CONFIG_SECTION_IDS = [ export const CONFIG_TAB_IDS: readonly ConfigTabId[] = ['common', ...CONFIG_SECTION_IDS]; -/** 分区序号(01–07)。常用 tab 是别名视图,不占序号。 */ +/** 分区序号(01–06)。常用 tab 是别名视图,不占序号。 */ export const SECTION_INDEX_LABELS: Record = { connectivity: '01', network: '02', logging: '03', - quota: '04', - streaming: '05', - advanced: '06', - payload: '07', + streaming: '04', + advanced: '05', + payload: '06', }; export const CONFIG_TAB_ICONS: Record> = { @@ -47,13 +44,12 @@ export const CONFIG_TAB_ICONS: Record> = { connectivity: IconKey, network: IconNetwork, logging: IconScrollText, - quota: IconTimer, streaming: IconSatellite, advanced: IconShield, payload: IconCode, }; -/** 常用 tab 的 8 个字段(原简单模式),渲染源与正典分区共享(fields/sharedFields.tsx)。 */ +/** 常用 tab 的 6 个字段(原简单模式),渲染源与正典分区共享(fields/sharedFields.tsx)。 */ export const COMMON_FIELD_IDS = [ 'host', 'port', @@ -61,8 +57,6 @@ export const COMMON_FIELD_IDS = [ 'proxyUrl', 'debug', 'loggingToFile', - 'quotaSwitchProject', - 'quotaSwitchPreviewModel', ] as const; /** @@ -81,7 +75,6 @@ export const SECTION_VALIDATION_FIELDS: Record = { errorLogsMaxFiles: ['errorLogsMaxFiles'], redisUsageQueueRetentionSeconds: ['redisUsageQueueRetentionSeconds'], usageStatisticsEnabled: ['usageStatisticsEnabled'], - // ── quota ───────────────────────────────────────────────────────────────── - quotaSwitchProject: ['quotaSwitchProject'], - quotaSwitchPreviewModel: ['quotaSwitchPreviewModel'], - quotaAntigravityCredits: ['quotaAntigravityCredits'], // ── streaming ───────────────────────────────────────────────────────────── streamingKeepaliveSeconds: ['streaming.keepaliveSeconds'], streamingBootstrapRetries: ['streaming.bootstrapRetries'], streamingNonstreamKeepalive: ['streaming.nonstreamKeepaliveInterval'], // ── advanced ────────────────────────────────────────────────────────────── + quotaAntigravityCredits: ['quotaAntigravityCredits'], pluginsEnabled: ['pluginsEnabled'], pluginStoreSources: ['pluginStoreSources'], pluginStoreAuth: ['pluginStoreAuth'], diff --git a/src/features/config/searchIndex.ts b/src/features/config/searchIndex.ts index 32828cef..b6fce592 100644 --- a/src/features/config/searchIndex.ts +++ b/src/features/config/searchIndex.ts @@ -9,7 +9,7 @@ // extra entry anywhere fails CI. export type VisualSectionId = - 'connectivity' | 'network' | 'logging' | 'quota' | 'streaming' | 'advanced' | 'payload'; + 'connectivity' | 'network' | 'logging' | 'streaming' | 'advanced' | 'payload'; export interface ConfigFieldSearchEntry { /** Stable anchor id; matches FieldAnchor's `fieldId` and the rendered DOM id. */ @@ -518,27 +518,6 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [ hintKey: L('sections.system.usage_statistics_enabled_desc'), yamlKeys: ['observability', 'usage', 'usage-statistics-enabled'], }, - // ── quota ───────────────────────────────────────────────────────────────── - { - fieldId: 'quotaSwitchProject', - sectionId: 'quota', - labelKey: L('sections.quota.switch_project'), - hintKey: L('sections.quota.switch_project_desc'), - yamlKeys: ['quota-exceeded', 'switch-project'], - }, - { - fieldId: 'quotaSwitchPreviewModel', - sectionId: 'quota', - labelKey: L('sections.quota.switch_preview_model'), - hintKey: L('sections.quota.switch_preview_model_desc'), - yamlKeys: ['quota-exceeded', 'switch-preview-model'], - }, - { - fieldId: 'quotaAntigravityCredits', - sectionId: 'quota', - labelKey: L('sections.quota.antigravity_credits'), - yamlKeys: ['oauth', 'providers', 'antigravity', 'antigravity-credits'], - }, // ── streaming ───────────────────────────────────────────────────────────── { fieldId: 'streamingKeepaliveSeconds', @@ -562,6 +541,12 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [ yamlKeys: ['requests', 'nonstream-keepalive-interval'], }, // ── advanced ────────────────────────────────────────────────────────────── + { + fieldId: 'quotaAntigravityCredits', + sectionId: 'advanced', + labelKey: L('sections.advanced.antigravity_credits'), + yamlKeys: ['oauth', 'providers', 'antigravity', 'antigravity-credits'], + }, { fieldId: 'pluginsEnabled', sectionId: 'advanced', diff --git a/src/hooks/useVisualConfig.ts b/src/hooks/useVisualConfig.ts index 88c3a73c..9ef3cf72 100644 --- a/src/hooks/useVisualConfig.ts +++ b/src/hooks/useVisualConfig.ts @@ -1250,8 +1250,6 @@ function getNextDirtyFields( 'maxRetryCredentials', 'maxRetryInterval', 'wsAuth', - 'quotaSwitchProject', - 'quotaSwitchPreviewModel', 'quotaAntigravityCredits', 'routingStrategy', 'routingSessionAffinity', @@ -1426,7 +1424,6 @@ function parseVisualValuesFromYaml(yamlContent: string): VisualConfigValues { const v8ObservabilityUsage = asRecord(v8Observability?.['usage']); const tls = asRecord(v8Server?.['tls']); const remoteManagement = asRecord(parsed['management']); - const quotaExceeded = asRecord(parsed['quota-exceeded']); const routing = asRecord(parsed.routing); const payload = asRecord(v8Requests?.['payload']); const streaming = asRecord(v8Requests?.['streaming']); @@ -1524,12 +1521,6 @@ function parseVisualValuesFromYaml(yamlContent: string): VisualConfigValues { ? codexHeaderDefaults['beta-features'] : '', - quotaSwitchProject: Boolean( - quotaExceeded?.['switch-project'] ?? DEFAULT_VISUAL_VALUES.quotaSwitchProject - ), - quotaSwitchPreviewModel: Boolean( - quotaExceeded?.['switch-preview-model'] ?? DEFAULT_VISUAL_VALUES.quotaSwitchPreviewModel - ), quotaAntigravityCredits: Boolean(antigravity?.['antigravity-credits'] ?? false), routingStrategy: parseRoutingStrategy(routing?.strategy), @@ -1979,25 +1970,12 @@ export function useVisualConfig() { deleteIfMapEmpty(doc, ['oauth', 'providers', 'codex', 'header-defaults']); } - const quotaDirty = - dirtyFields.has('quotaSwitchProject') || - dirtyFields.has('quotaSwitchPreviewModel') || - dirtyFields.has('quotaAntigravityCredits'); - if (quotaDirty) { - ensureMapInDoc(doc, ['quota-exceeded']); - if (dirtyFields.has('quotaSwitchProject')) { - doc.setIn(['quota-exceeded', 'switch-project'], values.quotaSwitchProject); - } - if (dirtyFields.has('quotaSwitchPreviewModel')) { - doc.setIn(['quota-exceeded', 'switch-preview-model'], values.quotaSwitchPreviewModel); - } - if (dirtyFields.has('quotaAntigravityCredits')) { - doc.setIn( - ['oauth', 'providers', 'antigravity', 'antigravity-credits'], - values.quotaAntigravityCredits - ); - } - deleteIfMapEmpty(doc, ['quota-exceeded']); + if (dirtyFields.has('quotaAntigravityCredits')) { + ensureMapInDoc(doc, ['oauth', 'providers', 'antigravity']); + doc.setIn( + ['oauth', 'providers', 'antigravity', 'antigravity-credits'], + values.quotaAntigravityCredits + ); } const routingDirty = diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 03ba7148..5b8e34fc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1279,6 +1279,7 @@ "description": "Debug, log output, and usage statistics" }, "advanced": { + "antigravity_credits": "Use Antigravity Credits", "title": "Advanced & Experimental", "description": "Header spoofing, signature cache, plugin sources, and other rarely changed settings", "plugins_title": "Plugins", @@ -1427,15 +1428,6 @@ "ws_auth": "WebSocket Authentication", "ws_auth_desc": "Require authentication for AI Studio WebSocket connections (oauth.providers.aistudio.ws-auth)" }, - "quota": { - "title": "Quota Fallback", - "description": "Fallback strategy when quota is exceeded", - "switch_project": "Switch Project", - "switch_project_desc": "Automatically switch to another project when quota is exceeded", - "switch_preview_model": "Switch to Preview Model", - "switch_preview_model_desc": "Switch to preview model version when quota is exceeded", - "antigravity_credits": "Use Antigravity Credits" - }, "streaming": { "title": "Streaming Configuration", "description": "Keepalive and bootstrap retry settings", diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index f895856e..7d4b2747 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -1279,6 +1279,7 @@ "description": "디버그, 로그 출력 및 사용량 통계" }, "advanced": { + "antigravity_credits": "Antigravity 크레딧 사용", "title": "고급 및 실험", "description": "헤더 위장, 서명 캐시, 플러그인 소스 및 거의 변경되지 않는 기타 설정", "plugins_title": "플러그인", @@ -1427,15 +1428,6 @@ "ws_auth": "WebSocket 인증", "ws_auth_desc": "AI Studio WebSocket 연결에 인증을 요구합니다 (oauth.providers.aistudio.ws-auth)" }, - "quota": { - "title": "할당량 폴백", - "description": "할당량이 초과되었을 때의 폴백 전략", - "switch_project": "프로젝트 전환", - "switch_project_desc": "할당량이 초과되면 다른 프로젝트로 자동 전환합니다.", - "switch_preview_model": "프리뷰 모델로 전환", - "switch_preview_model_desc": "할당량이 초과되면 프리뷰 모델 버전으로 전환합니다.", - "antigravity_credits": "Antigravity 크레딧 사용" - }, "streaming": { "title": "스트리밍 설정", "description": "Keepalive 및 부트스트랩 재시도 설정", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 97676d73..2c5b5492 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1282,6 +1282,7 @@ "description": "Отладка, вывод логов и статистика использования" }, "advanced": { + "antigravity_credits": "Использовать Antigravity Credits", "title": "Дополнительно и эксперименты", "description": "Подмена заголовков, кэш подписей, источники плагинов и другие редко изменяемые настройки", "plugins_title": "Плагины", @@ -1430,15 +1431,6 @@ "ws_auth": "Аутентификация WebSocket", "ws_auth_desc": "Аутентификация WebSocket AI Studio (oauth.providers.aistudio.ws-auth)" }, - "quota": { - "title": "Резерв по квоте", - "description": "Стратегия при превышении квоты", - "switch_project": "Переключить проект", - "switch_project_desc": "Автоматически переходить на другой проект при превышении квоты", - "switch_preview_model": "Переключить на preview-модель", - "switch_preview_model_desc": "Переключаться на preview-версию модели при превышении квоты", - "antigravity_credits": "Использовать Antigravity Credits" - }, "streaming": { "title": "Настройки стриминга", "description": "Параметры keepalive и повторов запуска", diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index 244ebe69..05678b74 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -1072,6 +1072,7 @@ "description": "Debug, xuất log và thống kê sử dụng" }, "advanced": { + "antigravity_credits": "Dùng credit Antigravity", "title": "Nâng cao & Thử nghiệm", "description": "Giả mạo header, cache chữ ký, nguồn plugin và các cài đặt ít thay đổi khác", "plugins_title": "Plugin", @@ -1220,15 +1221,6 @@ "proxy_url_sponsor_hint": "Chưa có proxy phù hợp?", "max_retry_interval_hint": "Đặt thành 0 hoặc số nguyên âm để bỏ qua thời gian chờ cooldown giữa các lượt thử lại." }, - "quota": { - "title": "Quota dự phòng", - "description": "Chiến lược dự phòng khi vượt quota", - "switch_project": "Chuyển Project", - "switch_project_desc": "Tự động chuyển sang project khác khi vượt quota", - "switch_preview_model": "Chuyển sang model Preview", - "switch_preview_model_desc": "Chuyển sang phiên bản model preview khi vượt quota", - "antigravity_credits": "Dùng credit Antigravity" - }, "streaming": { "title": "Cấu hình Streaming", "description": "Cài đặt keepalive và thử lại bootstrap", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index de93bac2..6ab2c835 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1278,6 +1278,7 @@ "description": "调试、日志输出与使用统计" }, "advanced": { + "antigravity_credits": "使用Antigravity Credits", "title": "高级与实验", "description": "请求头伪装、签名缓存、插件源等不常修改的设置", "plugins_title": "插件", @@ -1426,15 +1427,6 @@ "ws_auth": "WebSocket 认证", "ws_auth_desc": "启用 AI Studio WebSocket 连接认证(oauth.providers.aistudio.ws-auth)" }, - "quota": { - "title": "配额回退", - "description": "配额耗尽时的回退策略", - "switch_project": "切换项目", - "switch_project_desc": "配额耗尽时自动切换到其他项目", - "switch_preview_model": "切换预览模型", - "switch_preview_model_desc": "配额耗尽时切换到预览版本模型", - "antigravity_credits": "使用Antigravity Credits" - }, "streaming": { "title": "流式传输配置", "description": "Keepalive 与 bootstrap 重试设置", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 2aadb72b..7400c668 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1306,6 +1306,7 @@ "description": "除錯、日誌輸出與使用統計" }, "advanced": { + "antigravity_credits": "使用Antigravity Credits", "title": "進階與實驗", "description": "請求標頭偽裝、簽章快取、外掛來源等不常修改的設定", "plugins_title": "外掛", @@ -1454,15 +1455,6 @@ "ws_auth": "WebSocket 驗證", "ws_auth_desc": "啟用 AI Studio WebSocket 連線驗證(oauth.providers.aistudio.ws-auth)" }, - "quota": { - "title": "配額回退", - "description": "配額耗盡時的回退策略", - "switch_project": "切換專案", - "switch_project_desc": "配額耗盡時自動切換到其他專案", - "switch_preview_model": "切換預覽模型", - "switch_preview_model_desc": "配額耗盡時切換到預覽版本模型", - "antigravity_credits": "使用Antigravity Credits" - }, "streaming": { "title": "串流傳輸設定", "description": "Keepalive 與 bootstrap 重試設定", diff --git a/src/services/api/transformers.ts b/src/services/api/transformers.ts index 34579545..9b24ef15 100644 --- a/src/services/api/transformers.ts +++ b/src/services/api/transformers.ts @@ -360,10 +360,7 @@ export const normalizeConfigResponse = (raw: unknown): Config => { config.apiKeys = Array.isArray(keys) ? keys.filter((key): key is string => typeof key === 'string') : []; - const quota = at('quota-exceeded'); config.quotaExceeded = { - switchProject: isRecord(quota) ? normalizeBoolean(quota['switch-project']) : false, - switchPreviewModel: isRecord(quota) ? normalizeBoolean(quota['switch-preview-model']) : false, antigravityCredits: normalizeBoolean(at('oauth.providers.antigravity.antigravity-credits')) ?? false, }; diff --git a/src/types/config.ts b/src/types/config.ts index 2dd89298..de8a38c5 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -6,8 +6,6 @@ import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from './provider'; export interface QuotaExceededConfig { - switchProject?: boolean; - switchPreviewModel?: boolean; antigravityCredits?: boolean; } diff --git a/src/types/visualConfig.ts b/src/types/visualConfig.ts index c95d3c64..28203ecb 100644 --- a/src/types/visualConfig.ts +++ b/src/types/visualConfig.ts @@ -187,8 +187,6 @@ export type VisualConfigValues = { disableImageGeneration: DisableImageGenerationMode; gptImage2BaseModel: string; authAutoRefreshWorkers: string; - quotaSwitchProject: boolean; - quotaSwitchPreviewModel: boolean; /** OAuth-only: oauth.providers.antigravity.antigravity-credits. */ quotaAntigravityCredits: boolean; routingStrategy: RoutingStrategy; @@ -291,8 +289,6 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = { disableImageGeneration: 'false', gptImage2BaseModel: '', authAutoRefreshWorkers: '', - quotaSwitchProject: false, - quotaSwitchPreviewModel: false, quotaAntigravityCredits: false, routingStrategy: 'round-robin', routingSessionAffinity: false, diff --git a/tests/configFieldParity.test.ts b/tests/configFieldParity.test.ts index b9d438ef..6fa576c6 100644 --- a/tests/configFieldParity.test.ts +++ b/tests/configFieldParity.test.ts @@ -113,7 +113,7 @@ describe('JSX anchor parity (source scan)', () => { }); describe('registry consistency', () => { - test('tab id registry is common + the seven canonical sections', () => { + test('tab id registry is common + the canonical sections', () => { expect([...CONFIG_TAB_IDS]).toEqual(['common', ...CONFIG_SECTION_IDS]); }); @@ -138,13 +138,19 @@ describe('registry consistency', () => { }); describe('v8 YAML search paths', () => { + test('deprecated quota switches are absent from values and search metadata', () => { + for (const fieldId of ['quotaSwitchProject', 'quotaSwitchPreviewModel']) { + expect(LEAF_VALUE_KEYS.has(fieldId)).toBe(false); + expect(findConfigFieldById(fieldId)).toBeUndefined(); + expect(FIELD_VALUE_KEYS).not.toHaveProperty(fieldId); + } + }); + for (const [fieldId, path] of [ ['apiKeys', 'access.api-keys'], ['commercialMode', 'server.commercial-mode'], ['requestRetry', 'routing.retry.request-retry'], ['routingStrategy', 'routing.strategy'], - ['quotaSwitchProject', 'quota-exceeded.switch-project'], - ['quotaSwitchPreviewModel', 'quota-exceeded.switch-preview-model'], ['quotaAntigravityCredits', 'oauth.providers.antigravity.antigravity-credits'], ['wsAuth', 'oauth.providers.aistudio.ws-auth'], ['codexHeaderUserAgent', 'oauth.providers.codex.header-defaults.user-agent'], diff --git a/tests/configNormalizationV8.test.ts b/tests/configNormalizationV8.test.ts index 00abedf7..8747654f 100644 --- a/tests/configNormalizationV8.test.ts +++ b/tests/configNormalizationV8.test.ts @@ -9,12 +9,28 @@ describe('v8 persisted configuration normalization', () => { ).toBe(false); }); - test('reads Antigravity credits without requiring the unrelated quota-exceeded block', () => { + test('reads only Antigravity credits from the OAuth provider path', () => { + expect(normalizeConfigResponse({}).quotaExceeded).toEqual({ antigravityCredits: false }); + for (const enabled of [true, false]) { + const oauth = { providers: { antigravity: { 'antigravity-credits': enabled } } }; + expect(normalizeConfigResponse({ oauth }).quotaExceeded).toEqual({ + antigravityCredits: enabled, + }); + const config = normalizeConfigResponse({ + oauth, + 'quota-exceeded': { + 'switch-project': true, + 'switch-preview-model': true, + 'antigravity-credits': !enabled, + }, + }); + expect(config.quotaExceeded).toEqual({ antigravityCredits: enabled }); + expect(config.quotaExceeded).not.toHaveProperty('switchProject'); + expect(config.quotaExceeded).not.toHaveProperty('switchPreviewModel'); + } expect( - normalizeConfigResponse({ - oauth: { providers: { antigravity: { 'antigravity-credits': true } } }, - }).quotaExceeded?.antigravityCredits - ).toBe(true); + normalizeConfigResponse({ 'quota-exceeded': { 'antigravity-credits': true } }).quotaExceeded + ).toEqual({ antigravityCredits: false }); }); test('does not interpret legacy root settings as v8 values', () => { diff --git a/tests/configOAuthBehaviorUi.test.ts b/tests/configOAuthBehaviorUi.test.ts index 2b84e4b1..6ebdf722 100644 --- a/tests/configOAuthBehaviorUi.test.ts +++ b/tests/configOAuthBehaviorUi.test.ts @@ -28,8 +28,8 @@ const server = { }; describe('OAuth behavior configuration UI', () => { - test('keeps seven canonical tabs and places every addition in its designated section', () => { - expect(CONFIG_SECTION_IDS).toHaveLength(7); + test('keeps six canonical tabs and places every addition in its designated section', () => { + expect(CONFIG_SECTION_IDS).toHaveLength(6); const network = render(createElement(SectionNetwork, props)); const advanced = render(createElement(SectionAdvanced, props)); const additions = CONFIG_FIELD_SEARCH_INDEX.filter((entry) => diff --git a/tests/configServerUi.test.ts b/tests/configServerUi.test.ts index e792ee6c..dd5bfe61 100644 --- a/tests/configServerUi.test.ts +++ b/tests/configServerUi.test.ts @@ -32,7 +32,7 @@ const populated = { describe('server configuration UI', () => { test('keeps nine searchable anchors in connectivity without adding a tab', () => { - expect(CONFIG_SECTION_IDS).toHaveLength(7); + expect(CONFIG_SECTION_IDS).toHaveLength(6); expect(extras).toHaveLength(9); const markup = render(createElement(SectionConnectivity, props)); for (const entry of extras) { diff --git a/tests/configSettingLayout.test.ts b/tests/configSettingLayout.test.ts index 2030a690..632a9a4d 100644 --- a/tests/configSettingLayout.test.ts +++ b/tests/configSettingLayout.test.ts @@ -25,7 +25,6 @@ const sections = { .SectionConnectivity, network: (await import('@/features/config/components/sections/SectionNetwork')).SectionNetwork, logging: (await import('@/features/config/components/sections/SectionLogging')).SectionLogging, - quota: (await import('@/features/config/components/sections/SectionQuota')).SectionQuota, streaming: (await import('@/features/config/components/sections/SectionStreaming')) .SectionStreaming, advanced: (await import('@/features/config/components/sections/SectionAdvanced')).SectionAdvanced, diff --git a/tests/configUiState.test.ts b/tests/configUiState.test.ts index 1b3d3a2c..e0cbfb5e 100644 --- a/tests/configUiState.test.ts +++ b/tests/configUiState.test.ts @@ -1,4 +1,16 @@ import { describe, expect, test } from 'bun:test'; +import { + COMMON_FIELD_IDS, + CONFIG_SECTION_IDS, + CONFIG_TAB_IDS, + FIELD_VALUE_KEYS, + SECTION_INDEX_LABELS, +} from '@/features/config/constants'; +import { + CONFIG_FIELD_SEARCH_INDEX, + findConfigFieldById, + searchConfigFields, +} from '@/features/config/searchIndex'; import { buildHeaderMeta, countSectionErrors, @@ -94,7 +106,7 @@ describe('countSectionErrors', () => { expect(counts.network).toBe(1); expect(counts.streaming).toBe(1); expect(counts.logging).toBe(0); - expect(counts.quota).toBe(0); + expect(counts).not.toHaveProperty('quota'); expect(counts.advanced).toBe(0); expect(counts.payload).toBe(0); // port 由常用 tab 渲染,同一错误在两个 tab 都要可见 @@ -144,9 +156,10 @@ describe('resolveDirtyTabs', () => { expect(tabs.has('connectivity')).toBe(true); expect(tabs.size).toBe(2); - const quotaTabs = resolveDirtyTabs(new Set(['quotaSwitchProject'])); - expect(quotaTabs.has('common')).toBe(true); - expect(quotaTabs.has('quota')).toBe(true); + expect([...resolveDirtyTabs(new Set(['quotaAntigravityCredits']))]).toEqual(['advanced']); + expect(resolveDirtyTabs(new Set(['quotaSwitchProject', 'quotaSwitchPreviewModel'])).size).toBe( + 0 + ); }); test('unknown keys are ignored instead of crashing', () => { @@ -155,6 +168,72 @@ describe('resolveDirtyTabs', () => { }); }); +describe('Antigravity credits UI and search', () => { + test('removes the quota tab and keeps section numbering contiguous', () => { + expect(CONFIG_TAB_IDS).not.toContain('quota'); + expect(CONFIG_SECTION_IDS.map((id) => SECTION_INDEX_LABELS[id])).toEqual([ + '01', + '02', + '03', + '04', + '05', + '06', + ]); + expect(CONFIG_FIELD_SEARCH_INDEX.some((entry) => String(entry.sectionId) === 'quota')).toBe( + false + ); + for (const field of ['quotaSwitchProject', 'quotaSwitchPreviewModel']) { + expect(findConfigFieldById(field)).toBeUndefined(); + expect(FIELD_VALUE_KEYS).not.toHaveProperty(field); + expect(COMMON_FIELD_IDS).not.toContain(field); + } + for (const query of ['switch-project', 'switch-preview-model', 'quota-exceeded']) { + expect(searchConfigFields(query, (key) => key)).toEqual([]); + } + }); + + test('credits search targets Advanced in every locale', async () => { + const entry = findConfigFieldById('quotaAntigravityCredits'); + expect(entry?.sectionId).toBe('advanced'); + expect(entry?.labelKey).toBe('config_management.visual.sections.advanced.antigravity_credits'); + for (const locale of LANGUAGE_ORDER) { + const json = await Bun.file(`src/i18n/locales/${locale}.json`).json(); + const sections = json.config_management.visual.sections; + expect(sections).not.toHaveProperty('quota'); + expect(typeof sections.advanced.antigravity_credits).toBe('string'); + const translate = (key: string): string => + key === entry?.labelKey ? sections.advanced.antigravity_credits : key; + for (const query of [sections.advanced.antigravity_credits, 'antigravity-credits']) { + expect(searchConfigFields(query, translate)).toContainEqual(entry); + } + } + }); + + test('renders credits inside the Advanced Antigravity group, not common', async () => { + const root = 'src/features/config'; + const advanced = await Bun.file(`${root}/components/sections/SectionAdvanced.tsx`).text(); + const group = advanced.slice( + advanced.indexOf("label={t('config_management.visual.sections.advanced.antigravity_title')}"), + advanced.indexOf("label={t('config_management.visual.sections.advanced.devin_title')}") + ); + expect(group).toContain('fieldId="quotaAntigravityCredits"'); + expect(group).toContain('checked={values.quotaAntigravityCredits}'); + expect(group).toContain( + 'onChange={(quotaAntigravityCredits) => onChange({ quotaAntigravityCredits })}' + ); + expect(group).toContain('config_management.visual.sections.advanced.antigravity_credits'); + for (const file of [ + 'components/sections/SectionCommon.tsx', + 'components/fields/sharedFields.tsx', + 'ConfigPage.tsx', + ]) { + const source = await Bun.file(`${root}/${file}`).text(); + expect(source).not.toMatch(/QuotaSwitch|quotaSwitch|SectionQuota|case 'quota'/); + } + expect(await Bun.file(`${root}/components/sections/SectionQuota.tsx`).exists()).toBe(false); + }); +}); + describe('buildHeaderMeta', () => { const base = { fieldCount: 58, @@ -216,6 +295,7 @@ describe('localStorage readers', () => { }); test('readSavedSection falls back to common on stale values', () => { + expect(readSavedSection('quota')).toBe('common'); expect(readSavedSection('payload')).toBe('payload'); expect(readSavedSection('common')).toBe('common'); expect(readSavedSection('server')).toBe('common'); // 历史分区 id 不再存在 diff --git a/tests/fixtures/visualConfig-v8.yaml b/tests/fixtures/visualConfig-v8.yaml index 6107bc8f..4a2909c2 100644 --- a/tests/fixtures/visualConfig-v8.yaml +++ b/tests/fixtures/visualConfig-v8.yaml @@ -61,9 +61,6 @@ oauth: sensitive-words: [fixture-word] devin: sensitive-words: [fixture-devin] -quota-exceeded: - switch-project: false - switch-preview-model: false multimedia: disable-image-generation: false gpt-image-2-base-model: fixture-base diff --git a/tests/visualConfigDefaults.test.ts b/tests/visualConfigDefaults.test.ts index 8f23f16e..60410ad0 100644 --- a/tests/visualConfigDefaults.test.ts +++ b/tests/visualConfigDefaults.test.ts @@ -2,36 +2,54 @@ import { describe, expect, test } from 'bun:test'; import { parse as parseYaml } from 'yaml'; import { runVisualConfig } from './helpers/visualConfig'; -// Backend contract: config_load.go defaults WebsocketAuth to true; QuotaExceeded -// uses Go's false zero values. config.example.yaml values are not missing-key defaults. +// Backend contract: WebsocketAuth defaults to true and Antigravity credits to false. +// config.example.yaml values are not missing-key defaults. describe('visual config boolean defaults', () => { test('initial and missing-key values agree with backend defaults', () => { for (const config of [runVisualConfig(), runVisualConfig('server:\n port: 8317\n')]) { expect(config.visualValues.wsAuth).toBe(true); - expect(config.visualValues.quotaSwitchProject).toBe(false); - expect(config.visualValues.quotaSwitchPreviewModel).toBe(false); + expect(config.visualValues).not.toHaveProperty('quotaSwitchProject'); + expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel'); + expect(config.visualValues.quotaAntigravityCredits).toBe(false); expect(config.visualDirty).toBe(false); } }); for (const enabled of [true, false]) { test(`preserves explicit boolean values: ${enabled}`, () => { - const yaml = `quota-exceeded: - switch-project: ${enabled} - switch-preview-model: ${enabled} -oauth: + const yaml = `oauth: providers: aistudio: ws-auth: ${enabled} + antigravity: + antigravity-credits: ${enabled} `; const config = runVisualConfig(yaml); expect(config.visualValues.wsAuth).toBe(enabled); - expect(config.visualValues.quotaSwitchProject).toBe(enabled); - expect(config.visualValues.quotaSwitchPreviewModel).toBe(enabled); + expect(config.visualValues.quotaAntigravityCredits).toBe(enabled); expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual(parseYaml(yaml)); }); } + test('does not expose deprecated quota switches from existing YAML', () => { + const yaml = 'quota-exceeded: {switch-project: true, switch-preview-model: true}\n'; + const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]); + expect(config.visualValues).not.toHaveProperty('quotaSwitchProject'); + expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel'); + expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({ + ...parseYaml(yaml), + oauth: { providers: { antigravity: { 'antigravity-credits': true } } }, + }); + }); + + test('writes credits when the OAuth provider map is null', () => { + const yaml = 'oauth: {providers: {antigravity: null}}\n'; + const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]); + expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({ + oauth: { providers: { antigravity: { 'antigravity-credits': true } } }, + }); + }); + test('writes explicit false when disabling omitted ws-auth', () => { const yaml = 'server:\n port: 8317\n'; const config = runVisualConfig(yaml, [{ wsAuth: false }]); @@ -42,43 +60,36 @@ oauth: }); }); - test('writes enabled quota options when their block was omitted', () => { + test('writes enabled credits without creating quota-exceeded', () => { const yaml = 'server:\n port: 8317\n'; - const config = runVisualConfig(yaml, [ - { quotaSwitchProject: true, quotaSwitchPreviewModel: true }, - ]); + const config = runVisualConfig(yaml, [{ quotaAntigravityCredits: true }]); + expect([...config.visualDirtyFields]).toEqual(['quotaAntigravityCredits']); expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({ server: { port: 8317 }, - 'quota-exceeded': { 'switch-project': true, 'switch-preview-model': true }, + oauth: { providers: { antigravity: { 'antigravity-credits': true } } }, }); }); test('writes toggles in both directions without changing unrelated settings', () => { for (const enabled of [true, false]) { - const yaml = `quota-exceeded: - switch-project: ${!enabled} - switch-preview-model: ${!enabled} + const yaml = `server: + port: 8317 oauth: providers: aistudio: ws-auth: ${!enabled} antigravity: - antigravity-credits: true + antigravity-credits: ${!enabled} `; - const config = runVisualConfig(yaml, [ - { wsAuth: enabled, quotaSwitchProject: enabled, quotaSwitchPreviewModel: enabled }, - ]); + const config = runVisualConfig(yaml, [{ wsAuth: enabled, quotaAntigravityCredits: enabled }]); expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({ oauth: { providers: { aistudio: { 'ws-auth': enabled }, - antigravity: { 'antigravity-credits': true }, + antigravity: { 'antigravity-credits': enabled }, }, }, - 'quota-exceeded': { - 'switch-project': enabled, - 'switch-preview-model': enabled, - }, + server: { port: 8317 }, }); } }); @@ -86,8 +97,8 @@ oauth: test('reverting toggles clears dirty state without inserting missing keys', () => { const yaml = 'server:\n port: 8317\n'; const config = runVisualConfig(yaml, [ - { wsAuth: false, quotaSwitchProject: true, quotaSwitchPreviewModel: true }, - { wsAuth: true, quotaSwitchProject: false, quotaSwitchPreviewModel: false }, + { wsAuth: false, quotaAntigravityCredits: true }, + { wsAuth: true, quotaAntigravityCredits: false }, ]); expect(config.visualDirty).toBe(false); expect(config.visualDirtyFields.size).toBe(0); diff --git a/tests/visualConfigV8.test.ts b/tests/visualConfigV8.test.ts index ec6a9607..b30f3400 100644 --- a/tests/visualConfigV8.test.ts +++ b/tests/visualConfigV8.test.ts @@ -29,6 +29,9 @@ describe('v8 visual config contract', () => { codexHeaderUserAgent: 'fixture-oauth-agent', streaming: { keepaliveSeconds: '0', bootstrapRetries: '0', nonstreamKeepaliveInterval: '0' }, }); + expect(config.visualValues).not.toHaveProperty('quotaSwitchProject'); + expect(config.visualValues).not.toHaveProperty('quotaSwitchPreviewModel'); + expect(parseYaml(fixture)).not.toHaveProperty('quota-exceeded'); expect(config.visualDirty).toBe(false); expect(parseYaml(config.applyVisualChangesToYaml(fixture))).toEqual(parseYaml(fixture)); }); @@ -43,13 +46,12 @@ describe('v8 visual config contract', () => { expect(runVisualConfig('api-keys:\n codex: []\n').visualValues.apiKeysText).toBe(''); }); - test('OAuth edits stay OAuth-only and preserve unmoved routing, quota, and plugin fields', () => { + test('OAuth edits stay OAuth-only and preserve unmoved routing and plugin fields', () => { const config = runVisualConfig(fixture, [ { codexHeaderUserAgent: 'new-oauth-agent', antigravitySensitiveWords: [], quotaAntigravityCredits: true, - quotaSwitchProject: true, }, ]); const original = parseYaml(fixture); @@ -57,10 +59,7 @@ describe('v8 visual config contract', () => { expect(output['api-keys']).toEqual(original['api-keys']); expect(output.routing).toEqual(original.routing); expect(output.plugins).toEqual(original.plugins); - expect(output['quota-exceeded']).toEqual({ - 'switch-project': true, - 'switch-preview-model': false, - }); + expect(output).not.toHaveProperty('quota-exceeded'); expect(output.oauth.providers.codex['header-defaults']['user-agent']).toBe('new-oauth-agent'); expect(output.oauth.providers.antigravity['sensitive-words']).toBeUndefined(); expect(output.oauth.providers.antigravity['antigravity-credits']).toBe(true);