refactor(api): 收口 server/client 数据解析层,消除 aiGenerationClient 的 as T 断言
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
parseAiTaskStatus,
|
||||
parseTaskCreateResponse,
|
||||
parseImageTaskCreateResponse,
|
||||
parseAiTaskStatusList,
|
||||
parseSseTaskFrame,
|
||||
} from "./dtoParsers";
|
||||
|
||||
describe("parseAiTaskStatus", () => {
|
||||
it("parses a well-formed camelCase DTO", () => {
|
||||
const result = parseAiTaskStatus({
|
||||
taskId: "task-1",
|
||||
type: "video",
|
||||
status: "running",
|
||||
progress: 42,
|
||||
resultUrl: "https://example.com/r.mp4",
|
||||
error: null,
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:01:00Z",
|
||||
});
|
||||
expect(result.taskId).toBe("task-1");
|
||||
expect(result.type).toBe("video");
|
||||
expect(result.status).toBe("running");
|
||||
expect(result.progress).toBe(42);
|
||||
expect(result.resultUrl).toBe("https://example.com/r.mp4");
|
||||
});
|
||||
|
||||
it("tolerates snake_case field names", () => {
|
||||
const result = parseAiTaskStatus({
|
||||
task_id: "task-2",
|
||||
result_url: "https://example.com/x.png",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
});
|
||||
expect(result.taskId).toBe("task-2");
|
||||
expect(result.resultUrl).toBe("https://example.com/x.png");
|
||||
});
|
||||
|
||||
it("falls back to safe defaults for missing fields", () => {
|
||||
const result = parseAiTaskStatus({});
|
||||
expect(result.taskId).toBe("");
|
||||
expect(result.type).toBe("image");
|
||||
expect(result.status).toBe("failed");
|
||||
expect(result.progress).toBe(0);
|
||||
expect(result.resultUrl).toBeNull();
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects unknown status/type values", () => {
|
||||
const result = parseAiTaskStatus({ status: "weird", type: "audio" });
|
||||
expect(result.status).toBe("failed");
|
||||
expect(result.type).toBe("image");
|
||||
});
|
||||
|
||||
it("clamps progress to [0, 100]", () => {
|
||||
expect(parseAiTaskStatus({ progress: 150 }).progress).toBe(100);
|
||||
expect(parseAiTaskStatus({ progress: -10 }).progress).toBe(0);
|
||||
expect(parseAiTaskStatus({ progress: "not-a-number" }).progress).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves numeric conversationId and nulls others", () => {
|
||||
expect(parseAiTaskStatus({ conversationId: 7 }).conversationId).toBe(7);
|
||||
expect(parseAiTaskStatus({ conversation_id: 9 }).conversationId).toBe(9);
|
||||
expect(parseAiTaskStatus({ conversationId: "nope" }).conversationId).toBeNull();
|
||||
expect(parseAiTaskStatus({}).conversationId).toBeNull();
|
||||
});
|
||||
|
||||
it("returns empty string for a non-record payload", () => {
|
||||
const result = parseAiTaskStatus("garbage");
|
||||
expect(result.taskId).toBe("");
|
||||
expect(result.status).toBe("failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTaskCreateResponse", () => {
|
||||
it("extracts taskId from a create response", () => {
|
||||
expect(parseTaskCreateResponse({ taskId: "abc" }).taskId).toBe("abc");
|
||||
expect(parseTaskCreateResponse({ task_id: "def" }).taskId).toBe("def");
|
||||
expect(parseTaskCreateResponse({ id: "ghi" }).taskId).toBe("ghi");
|
||||
});
|
||||
|
||||
it("throws when taskId is missing", () => {
|
||||
expect(() => parseTaskCreateResponse({})).toThrow();
|
||||
expect(() => parseTaskCreateResponse({ taskId: "" })).toThrow();
|
||||
expect(() => parseTaskCreateResponse({ taskId: " " })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseImageTaskCreateResponse", () => {
|
||||
it("includes providerDebug when present", () => {
|
||||
const result = parseImageTaskCreateResponse({
|
||||
taskId: "img-1",
|
||||
providerDebug: {
|
||||
requestedModel: "gpt-image",
|
||||
effectiveModel: "dall-e-3",
|
||||
route: ["primary", "fallback"],
|
||||
candidates: [{ provider: "openai", model: "dall-e-3" }],
|
||||
},
|
||||
});
|
||||
expect(result.taskId).toBe("img-1");
|
||||
expect(result.providerDebug?.effectiveModel).toBe("dall-e-3");
|
||||
expect(result.providerDebug?.route).toEqual(["primary", "fallback"]);
|
||||
expect(result.providerDebug?.candidates?.[0]?.model).toBe("dall-e-3");
|
||||
});
|
||||
|
||||
it("omits providerDebug when absent", () => {
|
||||
const result = parseImageTaskCreateResponse({ taskId: "img-2" });
|
||||
expect(result.taskId).toBe("img-2");
|
||||
expect(result.providerDebug).toBeUndefined();
|
||||
});
|
||||
|
||||
it("tolerates snake_case providerDebug fields", () => {
|
||||
const result = parseImageTaskCreateResponse({
|
||||
taskId: "img-3",
|
||||
provider_debug: { requested_model: "x", primary_provider: "openai" },
|
||||
});
|
||||
expect(result.providerDebug?.requestedModel).toBe("x");
|
||||
expect(result.providerDebug?.primaryProvider).toBe("openai");
|
||||
});
|
||||
|
||||
it("throws when taskId missing even if providerDebug present", () => {
|
||||
expect(() => parseImageTaskCreateResponse({ providerDebug: {} })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseAiTaskStatusList", () => {
|
||||
it("parses a bare array", () => {
|
||||
const result = parseAiTaskStatusList([{ taskId: "a" }, { taskId: "b" }]);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].taskId).toBe("a");
|
||||
});
|
||||
|
||||
it("parses an envelope { tasks: [...] }", () => {
|
||||
const result = parseAiTaskStatusList({ tasks: [{ taskId: "a" }, { task_id: "b" }] });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].taskId).toBe("b");
|
||||
});
|
||||
|
||||
it("parses an envelope { items: [...] }", () => {
|
||||
const result = parseAiTaskStatusList({ items: [{ taskId: "a" }] });
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("drops rows with no taskId rather than crashing", () => {
|
||||
const result = parseAiTaskStatusList([{ taskId: "keep" }, { status: "running" }, {}]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].taskId).toBe("keep");
|
||||
});
|
||||
|
||||
it("returns empty array for non-array non-record payload", () => {
|
||||
expect(parseAiTaskStatusList(null)).toEqual([]);
|
||||
expect(parseAiTaskStatusList("nope")).toEqual([]);
|
||||
expect(parseAiTaskStatusList({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSseTaskFrame", () => {
|
||||
it("parses a well-formed SSE frame", () => {
|
||||
const frame = parseSseTaskFrame({
|
||||
taskId: "sse-1",
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
resultUrl: "https://example.com/done.png",
|
||||
});
|
||||
expect(frame.taskId).toBe("sse-1");
|
||||
expect(frame.status).toBe("completed");
|
||||
expect(frame.progress).toBe(100);
|
||||
expect(frame.resultUrl).toBe("https://example.com/done.png");
|
||||
});
|
||||
|
||||
it("clamps progress and rejects unknown status", () => {
|
||||
const frame = parseSseTaskFrame({ taskId: "sse-2", status: "oops", progress: 999 });
|
||||
expect(frame.status).toBe("failed");
|
||||
expect(frame.progress).toBe(100);
|
||||
});
|
||||
|
||||
it("handles a non-object payload", () => {
|
||||
const frame = parseSseTaskFrame("garbage");
|
||||
expect(frame.taskId).toBe("");
|
||||
expect(frame.status).toBe("failed");
|
||||
expect(frame.progress).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user