Files
Artemis-Eapil/apps/showcase_ui/src/app/components/llm-request-status/llm-request-status.component.spec.ts
T
chenmingxuan fac4ccb760 feat: prepare 2.2.0 with UI and execution fixes
Add dark theme, richer Markdown and readable plugin results.
Improve Android input evidence, response integrity and diagnostics.
Unify lifecycle, guidance receipts and evidence sources across clients.
Keep direct action dispatch without an added AI quota approval layer.
Align version metadata at 2.2.0 and bundle Helper 1.2.2.

Validation: 3063 backend tests before the version bump; 44 after.
Frontend: 317 tests and production build passed.
2026-10-26 00:44:13 +08:00

160 lines
9.9 KiB
TypeScript

import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { cancellationReason, FinalReview, LlmRequestStatusComponent, ModelRequest, requestHint, requestTermination, reviewStage } from './llm-request-status.component';
const waiting: ModelRequest = {
request_id: 'call-1', attempt_id: 'attempt-1', model: 'gpt-6-luna', role: 'FlashRunner',
is_fallback: false, phase: 'waiting_output', active: true, attempt: 1, max_retries: 5,
timeout_seconds: 300, elapsed_seconds: 42, started_at: Date.now() / 1000 - 42,
http_status: 200, usage: null, images: []
};
describe('模型请求诊断', () => {
beforeEach(() => TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] }));
afterEach(() => TestBed.inject(HttpTestingController).verify());
it('收到200仍显示等待,未知用量不当作0;完成后取消轮询', fakeAsync(() => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
fixture.componentRef.setInput('sessionId', 'task-one');
fixture.componentRef.setInput('running', true);
fixture.detectChanges();
const http = TestBed.inject(HttpTestingController);
http.expectOne('/api/sessions/task-one/request-diagnostics').flush({ requests: [waiting] });
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.request-live').textContent).toContain('等待模型输出');
expect(fixture.nativeElement.textContent).toContain('未知(服务未报告)');
expect(fixture.nativeElement.querySelector('details').open).toBeFalse();
tick(3000);
http.expectOne('/api/sessions/task-one/request-diagnostics').flush({ requests: [{ ...waiting, active: false, phase: 'completed', ended_at: Date.now() / 1000 }] });
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.request-live')).toBeNull();
fixture.destroy(); tick(5000);
http.expectNone('/api/sessions/task-one/request-diagnostics');
}));
it('切换会话取消旧请求,历史失败不会显示正在重试', () => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
const http = TestBed.inject(HttpTestingController);
fixture.componentRef.setInput('sessionId', 'old'); fixture.detectChanges();
const old = http.expectOne('/api/sessions/old/request-diagnostics');
fixture.componentRef.setInput('sessionId', 'new'); fixture.detectChanges();
expect(old.cancelled).toBeTrue();
http.expectOne('/api/sessions/new/request-diagnostics').flush({ requests: [{ ...waiting, phase: 'retry_wait' }] });
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.request-live')).toBeNull();
expect(fixture.nativeElement.querySelector('a').href).toContain('/new/request-diagnostics?download=true');
fixture.destroy();
});
it('诊断接口失败时明确提示状态未知', () => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
fixture.componentRef.setInput('sessionId', 'task'); fixture.detectChanges();
TestBed.inject(HttpTestingController).expectOne('/api/sessions/task/request-diagnostics').flush({}, { status: 503, statusText: 'Unavailable' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('不能据此判断任务是否卡住');
fixture.destroy();
});
it('重试倒计时到0后不谎称已发送请求', () => {
expect(requestHint({ ...waiting, phase: 'retry_wait', retry_at: 110 }, 105)).toContain('5 秒后进行第 1/5 次重试');
expect(requestHint({ ...waiting, phase: 'retry_wait', retry_at: 110 }, 111)).toContain('等待下一次请求状态');
});
it('持续生成工具参数时展示工具名和字符数,不误报工具已执行', () => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
fixture.componentRef.setInput('sessionId', 'tool-task');
fixture.componentRef.setInput('running', true);
fixture.detectChanges();
TestBed.inject(HttpTestingController).expectOne('/api/sessions/tool-task/request-diagnostics').flush({ requests: [{
...waiting, phase: 'generating_tool_arguments', tool_argument_chars: 15000,
tools: [{ name: 'click', argument_chars: 15000 }], output_chunks: 200
}] });
fixture.detectChanges();
const text = fixture.nativeElement.querySelector('.request-live').textContent;
expect(text).toContain('正在生成工具参数');
expect(text).toContain('click');
expect(text).toContain((15000).toLocaleString());
expect(text).toContain('本次工具尚未执行');
fixture.destroy();
});
it('无模型请求时仍展示验收阶段与总预算,耗尽后保留明确原因', fakeAsync(() => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
const http = TestBed.inject(HttpTestingController);
const now = Date.now() / 1000;
const review: FinalReview = {
review_id: 'review-1', attempt_id: 'final#1', status: 'running', stage: 'reading_evidence', active: true,
started_at: now - 20, deadline_at: now + 80, total_seconds: 100, elapsed_seconds: 20, remaining_seconds: 80,
request_count: 2, retry_count: 1, max_retries: 5
};
fixture.componentRef.setInput('sessionId', 'review-task');
fixture.componentRef.setInput('running', true);
fixture.detectChanges();
http.expectOne('/api/sessions/review-task/request-diagnostics').flush({ requests: [], final_reviews: [review] });
fixture.detectChanges();
const text = fixture.nativeElement.querySelector('.review-live').textContent;
expect(text).toContain('正在读取验收证据');
expect(text).toContain('测试仍为待判定');
expect(text).toContain('剩余验收预算 80 秒');
expect(text).toContain('已重试 1 次');
tick(1000); fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.review-live').textContent).toContain('剩余验收预算 79 秒');
tick(2000);
http.expectOne('/api/sessions/review-task/request-diagnostics').flush({ requests: [], final_reviews: [{
...review, active: false, status: 'budget_exhausted', ended_at: now + 3, elapsed_seconds: 100
}] });
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.review-live')).toBeNull();
expect(fixture.nativeElement.textContent).toContain('验收总预算已用尽');
expect(fixture.nativeElement.textContent).toContain('未确认通过');
fixture.destroy(); tick(3000);
http.expectNone('/api/sessions/review-task/request-diagnostics');
}));
it('区分重试耗尽、验收中断与完成,不把完成当成通过', () => {
const review = { status: 'request_retries_exhausted' } as FinalReview;
expect(reviewStage(review)).toBe('验收请求重试已用尽');
expect(reviewStage({ ...review, status: 'cancelled' })).toBe('验收已中断');
expect(reviewStage({ ...review, status: 'completed' })).toBe('验收完成');
});
it('取消原因有来源才显示,旧记录与未知字段保留未知', () => {
expect(cancellationReason('user_stop')).toContain('用户停止');
expect(cancellationReason('guidance_replaced')).toContain('新指导替换');
expect(cancellationReason('task_paused')).toContain('任务暂停');
expect(cancellationReason('task_replaced')).toContain('新任务替换');
expect(cancellationReason('superseded')).toContain('新的检查或计划');
expect(cancellationReason()).toContain('取消来源未记录');
expect(cancellationReason('untrusted new instruction')).toContain('取消来源未记录');
expect(requestTermination({ ...waiting, phase: 'cancelled' })).toContain('取消来源未记录');
});
it('区分三层超时、未完成输出与未确认来源的499', () => {
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'request' })).toContain('单次模型请求');
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'transport' })).toContain('连接或读写超时');
expect(requestTermination({ ...waiting, phase: 'failed', timeout_scope: 'final_review' })).toContain('最终验收总预算');
expect(requestTermination({ ...waiting, phase: 'cancelled', cancel_reason: 'outer_timeout', timeout_scope: 'outer' })).toContain('外层等待时限');
expect(requestTermination({ ...waiting, phase: 'failed', http_status: 499 })).toContain('来源尚未确认');
expect(requestTermination({ ...waiting, phase: 'failed', category: 'connection' })).toContain('来源需结合服务端记录');
expect(requestTermination({ ...waiting, phase: 'failed', response_guard: { reason: 'missing_completion' } })).toContain('本次输出未被采用');
expect(requestTermination({ ...waiting, phase: 'completed' })).toBe('');
});
it('将本次尝试、上次重试、链路编号与结束原因一起展示', () => {
const fixture = TestBed.createComponent(LlmRequestStatusComponent);
fixture.componentRef.setInput('sessionId', 'cancelled'); fixture.detectChanges();
TestBed.inject(HttpTestingController).expectOne('/api/sessions/cancelled/request-diagnostics').flush({ requests: [{
...waiting, active: false, phase: 'cancelled', cancel_reason: 'guidance_replaced',
cancel_phase: 'retry_wait', previous_attempt_id: 'attempt-before', client_request_id: 'client-wire-id',
server_request_id: 'server-wire-id', response_id: 'resp-from-server', server_trace_id: 'gateway-trace',
traceparent: '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01', ended_at: Date.now() / 1000
}] });
fixture.detectChanges();
const text = fixture.nativeElement.textContent;
for (const value of ['attempt-before', 'client-wire-id', 'server-wire-id', 'resp-from-server', 'gateway-trace', '新指导替换了旧请求', '中断阶段', '等待重试']) expect(text).toContain(value);
expect(fixture.nativeElement.querySelector('.request-live')).toBeNull();
fixture.destroy();
});
});