import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { UI_READ_TIMEOUT_MS } from '../../core/http-timeouts'; import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { ModelSettingsComponent } from './model-settings.component'; import { CapabilitySettingsComponent } from './capability-settings.component'; describe('持久化模型与功能设置', () => { let http: HttpTestingController; const config = { base_url: 'https://example.test/v1', model: 'gpt-6-luna', api_key_configured: true, timeout_seconds: 300, max_retries: 5, retry_interval_seconds: 10, reasoning_effort: 'high', rate_limit_retry_seconds: 60, tool_fallback_model: 'gpt-6-luna', tool_fallback_reasoning_effort: 'high', tool_fallback_max_retries: 3, tool_fallback_no_progress_attempts: 3, model_presets: ['gpt-6-luna', 'gpt-6.1-sol', 'gpt-6-astra', 'mimo-v2.6-pro'], env_path: '/app/.env' }; beforeEach(() => { TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }); http = TestBed.inject(HttpTestingController); }); afterEach(() => http.verify()); it('配置请求排队超时后退出加载状态,不会发送保存请求', fakeAsync(() => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); const request = http.expectOne('/api/system/model-settings'); tick(UI_READ_TIMEOUT_MS); expect(request.cancelled).toBeTrue(); expect(fixture.componentInstance.loading()).toBeFalse(); expect(fixture.componentInstance.error()).toBeTruthy(); expect(fixture.componentInstance.valid()).toBeFalse(); http.expectNone('/api/system/model-settings'); })); it('加载配置后保留密钥,保存新模型且不发送空密钥', () => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/model-settings').flush(config); const component = fixture.componentInstance; expect(component.apiKey).toBe(''); expect(component.keyConfigured()).toBeTrue(); component.model = 'another-model'; component.save(); const request = http.expectOne('/api/system/model-settings'); expect(request.request.method).toBe('POST'); expect(request.request.body.model).toBe('another-model'); expect(request.request.body.reasoning_effort).toBe('high'); expect(request.request.body.timeout_seconds).toBe(300); expect(request.request.body.max_retries).toBe(5); expect(request.request.body.retry_interval_seconds).toBe(10); expect(request.request.body.rate_limit_retry_seconds).toBe(60); expect(request.request.body.tool_fallback_model).toBe('gpt-6-luna'); expect(request.request.body.tool_fallback_no_progress_attempts).toBe(3); expect(request.request.body.api_key).toBeUndefined(); request.flush({ ...config, model: 'another-model' }); expect(component.busy()).toBeNull(); expect(component.message()).toContain('保存'); }); it('统一选择预设或自定义模型,自定义名称与 max 可以保存并重新加载', async () => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/model-settings').flush(config); fixture.detectChanges(); await fixture.whenStable(); const root: HTMLElement = fixture.nativeElement; const select = root.querySelector('app-model-selector select')!; expect(Array.from(select.options, option => option.value)).toEqual([...config.model_presets, '__custom__']); expect(select.value).toBe('gpt-6-luna'); select.value = 'mimo-v2.6-pro'; select.dispatchEvent(new Event('change')); expect(fixture.componentInstance.model).toBe('mimo-v2.6-pro'); select.value = '__custom__'; select.dispatchEvent(new Event('change')); fixture.detectChanges(); const input = root.querySelector('input[aria-label="模型名称(自定义)"]')!; expect(input.autocomplete).toBe('off'); input.value = 'custom/model-v2'; input.dispatchEvent(new Event('input')); const reasoning = root.querySelector('input[aria-label="思考强度"]')!; reasoning.value = '7'; reasoning.dispatchEvent(new Event('input')); fixture.detectChanges(); await fixture.whenStable(); root.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); const request = http.expectOne('/api/system/model-settings'); expect(request.request.body.model).toBe('custom/model-v2'); expect(request.request.body.reasoning_effort).toBe('max'); const saved = { ...config, model: 'custom/model-v2', reasoning_effort: 'max' }; request.flush(saved); fixture.destroy(); const reopened = TestBed.createComponent(ModelSettingsComponent); reopened.detectChanges(); http.expectOne('/api/system/model-settings').flush(saved); reopened.detectChanges(); await reopened.whenStable(); expect(reopened.nativeElement.querySelector('input[aria-label="模型名称(自定义)"]').value).toBe('custom/model-v2'); expect(reopened.nativeElement.querySelector('input[aria-label="思考强度"]').getAttribute('aria-valuetext')).toBe('最高 · max'); }); it('高级稳定性设置可编辑并保存', async () => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/model-settings').flush(config); fixture.detectChanges(); await fixture.whenStable(); const root: HTMLElement = fixture.nativeElement; root.querySelector('.recovery-settings')!.open = true; const fallbackSelect = root.querySelectorAll('app-model-selector select')[1]; expect(Array.from(fallbackSelect.options, option => option.value)).toEqual([...config.model_presets, '__custom__']); fallbackSelect.value = '__custom__'; fallbackSelect.dispatchEvent(new Event('change')); fixture.detectChanges(); const fallback = root.querySelector('input[aria-label="兜底模型名称(自定义)"]')!; fallback.value = 'custom/fallback'; fallback.dispatchEvent(new Event('input')); const wait = root.querySelector('input[name="rateLimitRetry"]')!; wait.value = '90'; wait.dispatchEvent(new Event('input')); fixture.detectChanges(); await fixture.whenStable(); root.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); const request = http.expectOne('/api/system/model-settings'); expect(request.request.body.tool_fallback_model).toBe('custom/fallback'); expect(request.request.body.rate_limit_retry_seconds).toBe(90); request.flush({ ...config, tool_fallback_model: 'custom/fallback', rate_limit_retry_seconds: 90 }); }); it('连接测试使用当前输入而不写入保存接口', () => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/model-settings').flush(config); fixture.componentInstance.test(); const request = http.expectOne('/api/system/model-settings/test'); expect(request.request.body.model).toBe('gpt-6-luna'); request.flush({ message: 'Responses 连接测试成功' }); http.expectNone('/api/system/model-settings'); expect(fixture.componentInstance.message()).toContain('成功'); }); it('加载失败时保持功能开关不可保存,避免覆盖现有设置', () => { const fixture = TestBed.createComponent(CapabilitySettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/capability-settings').flush({}, { status: 500, statusText: 'Error' }); expect(fixture.componentInstance.error()).toContain('加载失败'); fixture.componentInstance.save(); http.expectNone('/api/system/capability-settings'); }); it('录屏与视频分析独立保存,重新打开后保留各自状态', () => { const capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: true, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: false, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 }; const fixture = TestBed.createComponent(CapabilitySettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/capability-settings').flush(capabilities); fixture.componentInstance.config.video_enabled = false; fixture.componentInstance.config.recording_enabled = true; fixture.componentInstance.config.screenshot_limit_enabled = false; fixture.componentInstance.save(); const request = http.expectOne('/api/system/capability-settings'); expect(request.request.body.video_enabled).toBeFalse(); expect(request.request.body.recording_enabled).toBeTrue(); expect(request.request.body.screenshot_limit_enabled).toBeFalse(); const saved = { ...capabilities, video_enabled: false, recording_enabled: true, screenshot_limit_enabled: false }; request.flush(saved); fixture.destroy(); const reopened = TestBed.createComponent(CapabilitySettingsComponent); reopened.detectChanges(); http.expectOne('/api/system/capability-settings').flush(saved); expect(reopened.componentInstance.config.video_enabled).toBeFalse(); expect(reopened.componentInstance.config.recording_enabled).toBeTrue(); expect(reopened.componentInstance.config.screenshot_limit_enabled).toBeFalse(); }); it('执行保护默认折叠,修改时限可保存且拒绝无效数值', async () => { const capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: false, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: true, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 }; const fixture = TestBed.createComponent(CapabilitySettingsComponent); fixture.detectChanges(); http.expectOne('/api/system/capability-settings').flush(capabilities); fixture.detectChanges(); await fixture.whenStable(); const root: HTMLElement = fixture.nativeElement; expect(root.querySelector('.execution-settings')!.open).toBeFalse(); root.querySelector('summary')!.click(); const timeout = root.querySelector('input[type="number"]')!; timeout.value = '3600'; timeout.dispatchEvent(new Event('input')); fixture.detectChanges(); await fixture.whenStable(); root.querySelector('button')!.click(); const request = http.expectOne('/api/system/capability-settings'); expect(request.request.body.task_timeout_seconds).toBe(3600); expect(request.request.body.observation_timeout_seconds).toBe(60); request.flush({ ...capabilities, task_timeout_seconds: 3600 }); expect(fixture.componentInstance.message()).toContain('已保存'); timeout.value = '0'; timeout.dispatchEvent(new Event('input')); fixture.detectChanges(); await fixture.whenStable(); expect(root.querySelector('button')!.disabled).toBeTrue(); fixture.componentInstance.save(); http.expectNone('/api/system/capability-settings'); }); it('自动连接默认开启,可独立关闭并保存发现预算', () => { const fixture = TestBed.createComponent(CapabilitySettingsComponent); const capabilities = { ...fixture.componentInstance.config }; fixture.detectChanges(); http.expectOne('/api/system/capability-settings').flush(capabilities); fixture.detectChanges(); const root: HTMLElement = fixture.nativeElement; expect(root.textContent).toContain('自动连接设备'); expect(fixture.componentInstance.config.adb_auto_connect).toBeTrue(); fixture.componentInstance.config.adb_auto_connect = false; fixture.componentInstance.config.adb_discovery_timeout_seconds = 8; fixture.componentInstance.save(); const request = http.expectOne('/api/system/capability-settings'); expect(request.request.body.adb_auto_connect).toBeFalse(); expect(request.request.body.adb_discovery_timeout_seconds).toBe(8); expect(request.request.body.recording_enabled).toBeFalse(); request.flush(request.request.body); fixture.componentInstance.config.adb_discovery_timeout_seconds = 0; fixture.componentInstance.save(); http.expectNone('/api/system/capability-settings'); }); it('Flash 最终 AI 判定默认关闭,保存失败保留选择,成功后重新打开仍保留', async () => { const fixture = TestBed.createComponent(CapabilitySettingsComponent); const capabilities = { ...fixture.componentInstance.config }; fixture.detectChanges(); const root: HTMLElement = fixture.nativeElement; const toggle = root.querySelector('[aria-label="Flash 最终 AI 判定与分析"]')!; await Promise.resolve(); expect(toggle.disabled).toBeTrue(); http.expectOne('/api/system/capability-settings').flush(capabilities); fixture.detectChanges(); await fixture.whenStable(); expect(toggle.checked).toBeFalse(); expect(root.textContent).toContain('不影响 Pro'); toggle.click(); fixture.detectChanges(); await fixture.whenStable(); root.querySelector('button')!.click(); let request = http.expectOne('/api/system/capability-settings'); expect(request.request.body.flash_final_check_enabled).toBeTrue(); request.flush({}, {status:500,statusText:'Error'}); fixture.detectChanges(); expect(fixture.componentInstance.config.flash_final_check_enabled).toBeTrue(); expect(fixture.componentInstance.error()).toContain('保存失败'); root.querySelector('button')!.click(); request = http.expectOne('/api/system/capability-settings'); const saved = request.request.body; request.flush(saved); fixture.destroy(); const reopened = TestBed.createComponent(CapabilitySettingsComponent); reopened.detectChanges(); http.expectOne('/api/system/capability-settings').flush(saved); reopened.detectChanges(); await reopened.whenStable(); expect(reopened.nativeElement.querySelector('[aria-label="Flash 最终 AI 判定与分析"]').checked).toBeTrue(); reopened.destroy(); }); });