test: 引入 Vitest 测试骨架并抽出颜色/比例纯函数模块

This commit is contained in:
2026-06-15 13:39:02 +08:00
parent 307537a7ce
commit 45e6534ee1
12 changed files with 3773 additions and 199 deletions
+81
View File
@@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { classifyTaskError, translateTaskError, type TaskErrorCategory } from "./translateTaskError";
// 每条规则至少一个正例,按规则顺序排列(classifyTaskError 先匹配先返回)。
const RULE_CASES: Array<{ name: string; input: string; category: TaskErrorCategory }> = [
{ name: "content policy", input: "content violated our policies", category: "content_policy" },
{ name: "nsfw", input: "image flagged as nsfw", category: "content_policy" },
{ name: "auth 401", input: "401 Unauthorized", category: "auth_failure" },
{ name: "token expired", input: "token expired", category: "auth_failure" },
{ name: "insufficient balance 402", input: "402 Payment Required", category: "insufficient_balance" },
{ name: "余额不足", input: "余额不足", category: "insufficient_balance" },
{ name: "concurrency pool full", input: "concurrency pool is full", category: "concurrency_busy" },
{ name: "rate limit 429", input: "429 Too Many Requests", category: "concurrency_busy" },
{ name: "unsupported model", input: "model not found", category: "unsupported_model" },
{ name: "invalid asset", input: "invalid image format", category: "invalid_asset" },
{ name: "network ECONNREFUSED", input: "fetch failed: ECONNREFUSED", category: "network_failure" },
{ name: "timeout ETIMEDOUT", input: "ETIMEDOUT", category: "timeout" },
{ name: "quota exceeded", input: "quota exceeded", category: "insufficient_balance" },
{ name: "cancelled", input: "task was cancelled", category: "cancelled" },
{ name: "已取消", input: "任务已取消", category: "cancelled" },
{ name: "all providers failed", input: "all providers failed", category: "concurrency_busy" },
{ name: "500 server error", input: "500 Internal Server Error", category: "network_failure" },
{ name: "forbidden 403", input: "403 Forbidden", category: "auth_failure" },
{ name: "aborted", input: "request aborted", category: "timeout" },
];
describe("classifyTaskError rule coverage", () => {
for (const { name, input, category } of RULE_CASES) {
it(`classifies "${name}" as ${category}`, () => {
const result = classifyTaskError(input);
expect(result.category).toBe(category);
expect(result.message).toBeTruthy();
expect(result.action).toBeTruthy();
});
}
});
describe("classifyTaskError edge cases", () => {
it("returns unknown for empty/null/undefined", () => {
expect(classifyTaskError("").category).toBe("unknown");
expect(classifyTaskError(undefined).category).toBe("unknown");
expect(classifyTaskError(null).category).toBe("unknown");
});
it("returns the raw (truncated) message for unrecognized Chinese errors", () => {
const result = classifyTaskError("这是一条未知的中文错误信息");
expect(result.category).toBe("unknown");
expect(result.message).toContain("未知");
expect(result.message).not.toContain("服务异常");
});
it("truncates long Chinese errors to 80 chars + ellipsis", () => {
const long = "错误".repeat(50);
const result = classifyTaskError(long);
expect(result.message.endsWith("...")).toBe(true);
expect(result.message.length).toBeLessThanOrEqual(83);
});
it("returns generic service message for unrecognized English errors", () => {
const result = classifyTaskError("something completely unexpected");
expect(result.category).toBe("unknown");
expect(result.message).toBe("服务异常,请稍后重试");
});
});
describe("classifyTaskError rule ordering (first match wins)", () => {
it("content_policy beats auth_failure when both patterns present", () => {
// "nsfw" appears before "401" in rule order
const result = classifyTaskError("nsfw content with 401");
expect(result.category).toBe("content_policy");
});
});
describe("translateTaskError", () => {
it("returns the message from classifyTaskError", () => {
expect(translateTaskError("401")).toBe("登录已过期,请重新登录");
});
it("returns generic message for empty input", () => {
expect(translateTaskError("")).toBe("任务失败,请重试");
});
});
+87
View File
@@ -0,0 +1,87 @@
import { describe, it, expect } from "vitest";
import { resolveHappyHorseRequestModel, HAPPY_HORSE_UI_MODEL, HAPPY_HORSE_T2V_MODEL, HAPPY_HORSE_I2V_MODEL, HAPPY_HORSE_R2V_MODEL } from "./happyHorseRouting";
import { resolveViduRequestModel, VIDU_UI_MODEL, VIDU_T2V_MODEL, VIDU_I2V_MODEL } from "./viduRouting";
import { resolvePixverseRequestModel, PIXVERSE_UI_MODEL, PIXVERSE_T2V_MODEL, PIXVERSE_I2V_MODEL, PIXVERSE_KF2V_MODEL } from "./pixverseRouting";
type ResolveFn = (input: { model: string; referenceUrls?: string[]; imageReferenceCount?: number }) => string;
// 三家路由在参考图数量上的分支差异是回归测试重点。
// HappyHorse: 0->t2v, 1->i2v, >=2->r2v
// Vidu: 0->t2v, >=1->i2v (无 r2v)
// Pixverse: 0->t2v, 1->i2v, >=2->kf2v
describe.each([
{ name: "HappyHorse", resolve: resolveHappyHorseRequestModel, ui: HAPPY_HORSE_UI_MODEL, t2v: HAPPY_HORSE_T2V_MODEL, i2v: HAPPY_HORSE_I2V_MODEL, third: HAPPY_HORSE_R2V_MODEL },
{ name: "Vidu", resolve: resolveViduRequestModel, ui: VIDU_UI_MODEL, t2v: VIDU_T2V_MODEL, i2v: VIDU_I2V_MODEL, third: null },
{ name: "Pixverse", resolve: resolvePixverseRequestModel, ui: PIXVERSE_UI_MODEL, t2v: PIXVERSE_T2V_MODEL, i2v: PIXVERSE_I2V_MODEL, third: PIXVERSE_KF2V_MODEL },
] as Array<{ name: string; resolve: ResolveFn; ui: string; t2v: string; i2v: string; third: string | null }>)(
"$name routing by imageReferenceCount",
({ resolve, ui, t2v, i2v, third }) => {
it("returns the input model unchanged when it is not this provider", () => {
expect(resolve({ model: "some-other-model" })).toBe("some-other-model");
});
it("routes 0 reference images to t2v", () => {
expect(resolve({ model: ui, imageReferenceCount: 0 })).toBe(t2v);
});
it("routes 1 reference image to i2v", () => {
expect(resolve({ model: ui, imageReferenceCount: 1 })).toBe(i2v);
});
if (third) {
it("routes >=2 reference images to the third model", () => {
expect(resolve({ model: ui, imageReferenceCount: 2 })).toBe(third);
expect(resolve({ model: ui, imageReferenceCount: 5 })).toBe(third);
});
} else {
it("routes >=1 reference images to i2v (no third model for this provider)", () => {
expect(resolve({ model: ui, imageReferenceCount: 2 })).toBe(i2v);
expect(resolve({ model: ui, imageReferenceCount: 5 })).toBe(i2v);
});
}
},
);
describe("reference count fallback (referenceUrls when imageReferenceCount omitted)", () => {
it("HappyHorse counts non-empty urls", () => {
expect(
resolveHappyHorseRequestModel({
model: HAPPY_HORSE_UI_MODEL,
referenceUrls: ["", " ", "https://example.com/a.png"],
}),
).toBe(HAPPY_HORSE_I2V_MODEL);
});
it("Vidu falls back to 0 when all urls are empty/whitespace", () => {
expect(
resolveViduRequestModel({
model: VIDU_UI_MODEL,
referenceUrls: ["", " "],
}),
).toBe(VIDU_T2V_MODEL);
});
it("Pixverse counts two non-empty urls as kf2v", () => {
expect(
resolvePixverseRequestModel({
model: PIXVERSE_UI_MODEL,
referenceUrls: ["https://a.png", "https://b.png"],
}),
).toBe(PIXVERSE_KF2V_MODEL);
});
it("imageReferenceCount takes precedence over referenceUrls length", () => {
// Even though referenceUrls has 3 entries, explicit count of 0 wins.
expect(
resolveHappyHorseRequestModel({
model: HAPPY_HORSE_UI_MODEL,
referenceUrls: ["a", "b", "c"],
imageReferenceCount: 0,
}),
).toBe(HAPPY_HORSE_T2V_MODEL);
});
it("handles undefined referenceUrls with undefined count", () => {
expect(resolveViduRequestModel({ model: VIDU_UI_MODEL })).toBe(VIDU_T2V_MODEL);
});
});