Files
Artemis-Eapil/apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts
T
chenmingxuan 83bcfd54d5 feat: release 2.1.0 with optional Flash final AI review
Default Flash final review to off and persist the setting across the UI,
MCP, SDK and queued tasks. Keep execution separate from test verdicts.

Bundle UI fonts and licenses to remove Google Fonts from startup builds.
Align backend/frontend versions and document installation and upgrades.
2026-10-23 12:36:56 +08:00

257 lines
15 KiB
TypeScript

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<HTMLSelectElement>('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<HTMLInputElement>('input[aria-label="模型名称(自定义)"]')!;
expect(input.autocomplete).toBe('off');
input.value = 'custom/model-v2';
input.dispatchEvent(new Event('input'));
const reasoning = root.querySelector<HTMLInputElement>('input[aria-label="思考强度"]')!;
reasoning.value = '7';
reasoning.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
root.querySelector<HTMLFormElement>('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<HTMLDetailsElement>('.recovery-settings')!.open = true;
const fallbackSelect = root.querySelectorAll<HTMLSelectElement>('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<HTMLInputElement>('input[aria-label="兜底模型名称(自定义)"]')!;
fallback.value = 'custom/fallback';
fallback.dispatchEvent(new Event('input'));
const wait = root.querySelector<HTMLInputElement>('input[name="rateLimitRetry"]')!;
wait.value = '90';
wait.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
root.querySelector<HTMLFormElement>('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<HTMLDetailsElement>('.execution-settings')!.open).toBeFalse();
root.querySelector<HTMLElement>('summary')!.click();
const timeout = root.querySelector<HTMLInputElement>('input[type="number"]')!;
timeout.value = '3600';
timeout.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
root.querySelector<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLInputElement>('[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<HTMLButtonElement>('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<HTMLButtonElement>('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();
});
});