import { ErrorCode } from '@lingxi/shared'; import { describe, expect, it, vi } from 'vitest'; import { AppException } from '../../common/errors/app-exception'; import type { SystemConfigService } from '../platform/system-config/system-config.service'; import { estimateTask, MediaTaskEstimator } from './media-task-estimator'; function expectCode(fn: () => unknown, code: ErrorCode): void { try { fn(); } catch (error) { expect(error).toBeInstanceOf(AppException); expect((error as AppException).code).toBe(code); return; } throw new Error('预期抛出 AppException'); } const CONFIG = { maxItems: 20, costPerItem: 0.02, secondsPerItem: 3 }; describe('任务预估纯计算(R-26:上限 / 单价来自系统配置)', () => { it('返回条数 / 成本(2 位小数)/ 耗时', () => { expect(estimateTask('demo', 5, CONFIG)).toEqual({ estimatedItems: 5, estimatedCost: 0.1, estimatedSeconds: 15, }); // 0.02 * 3 = 0.06 不产生浮点尾巴 expect(estimateTask('demo', 3, CONFIG).estimatedCost).toBe(0.06); expect(estimateTask('demo', 20, CONFIG).estimatedCost).toBe(0.4); }); it('项数超过配置上限 → 40001(业务上限,不是 schema 硬上限)', () => { expectCode(() => estimateTask('demo', 21, CONFIG), ErrorCode.PARAM_INVALID); expectCode( () => estimateTask('demo', 101, { ...CONFIG, maxItems: 100 }), ErrorCode.PARAM_INVALID, ); }); it('非法项数 / 非法配置 → 40001', () => { expectCode(() => estimateTask('demo', 0, CONFIG), ErrorCode.PARAM_INVALID); expectCode(() => estimateTask('demo', 2.5, CONFIG), ErrorCode.PARAM_INVALID); expectCode(() => estimateTask('demo', 1, { ...CONFIG, maxItems: 0 }), ErrorCode.PARAM_INVALID); expectCode( () => estimateTask('demo', 1, { ...CONFIG, costPerItem: Number.NaN }), ErrorCode.PARAM_INVALID, ); expectCode( () => estimateTask('demo', 1, { ...CONFIG, costPerItem: -1 }), ErrorCode.PARAM_INVALID, ); expectCode( () => estimateTask('demo', 1, { ...CONFIG, secondsPerItem: 0 }), ErrorCode.PARAM_INVALID, ); expectCode( () => estimateTask('demo', 1, { ...CONFIG, secondsPerItem: Number.POSITIVE_INFINITY }), ErrorCode.PARAM_INVALID, ); }); }); describe('MediaTaskEstimator(读 SystemConfigService)', () => { function estimatorWith(values: readonly number[]): { estimator: MediaTaskEstimator; getNumber: ReturnType; } { const getNumber = vi.fn(); for (const value of values) getNumber.mockResolvedValueOnce(value); const config = { getNumber } as unknown as SystemConfigService; return { estimator: new MediaTaskEstimator(config), getNumber }; } it('按配置计算预估', async () => { const { estimator, getNumber } = estimatorWith([20, 0.02, 3]); await expect(estimator.estimate('demo', 5)).resolves.toEqual({ estimatedItems: 5, estimatedCost: 0.1, estimatedSeconds: 15, }); expect(getNumber).toHaveBeenNthCalledWith(1, 'task.demo.max_items'); expect(getNumber).toHaveBeenNthCalledWith(2, 'task.estimate.demo_cost_per_item'); expect(getNumber).toHaveBeenNthCalledWith(3, 'task.estimate.demo_seconds_per_item'); }); it('读配置失败(缺预估字段)→ 40001,不允许静默 0', async () => { const getNumber = vi.fn().mockRejectedValue(new Error('system_configs 不可用')); const config = { getNumber } as unknown as SystemConfigService; const estimator = new MediaTaskEstimator(config); await expect(estimator.estimate('demo', 5)).rejects.toMatchObject({ code: ErrorCode.PARAM_INVALID, }); }); it('配置值越界(超过上限)→ 40001', async () => { const { estimator } = estimatorWith([4, 0.02, 3]); await expect(estimator.estimate('demo', 5)).rejects.toMatchObject({ code: ErrorCode.PARAM_INVALID, }); }); });