Files
NianAIGC/tests/seedream-layer-edit-result-sync.test.ts
2026-09-02 17:02:02 +08:00

117 lines
4.7 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { afterEach, describe, expect, it, vi } from "vitest";
import { waitForJobOutputAssets } from "../lib/client/generation-result-sync";
import type { Asset, GenerationJob } from "../lib/types";
const resultAsset: Asset = {
id: "asset-result",
ownerId: "owner-1",
kind: "image",
name: "编辑结果.png",
url: "/generated-results/asset-result.png",
source: "generated",
tags: [],
metadata: {},
createdAt: "2026-09-02T00:00:00.000Z",
updatedAt: "2026-09-02T00:00:00.000Z"
};
const succeededBeforeOutputSync: GenerationJob = {
id: "job-edit-1",
ownerId: "owner-1",
capability: "image.generate",
provider: "seedream",
reqKey: "doubao-seedream-5-0-pro-260628",
status: "succeeded",
inputAssetIds: ["asset-source"],
inputUrls: [],
outputAssetIds: [],
requestPayload: {},
createdAt: "2026-09-02T00:00:00.000Z",
updatedAt: "2026-09-02T00:00:00.000Z"
};
describe("Seedream layer edit result synchronization", () => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it("uses the supported asset collection when production rejects platform item GET", async () => {
vi.useFakeTimers();
vi.stubGlobal("window", { setTimeout, clearTimeout });
const job = { ...succeededBeforeOutputSync, outputAssetIds: [resultAsset.id] };
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/generations/image/job-edit-1") return Response.json({ job });
if (url === "/api/assets/asset-result") return new Response(null, { status: 405 });
if (url === "/api/assets") return Response.json({ assets: [resultAsset] });
return Response.json({ error: "unexpected request" }, { status: 404 });
});
vi.stubGlobal("fetch", fetchMock);
const pending = waitForJobOutputAssets(job, new AbortController().signal);
const assertion = expect(pending).resolves.toEqual([resultAsset]);
await vi.runAllTimersAsync();
await assertion;
expect(fetchMock).toHaveBeenCalledWith("/api/assets", expect.objectContaining({ cache: "no-store" }));
expect(fetchMock).not.toHaveBeenCalledWith("/api/assets/asset-result", expect.anything());
});
it("refreshes a succeeded job whose output IDs arrive after the terminal status", async () => {
vi.useFakeTimers();
vi.stubGlobal("window", { setTimeout, clearTimeout });
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/generations/image/job-edit-1") {
return Response.json({ job: { ...succeededBeforeOutputSync, outputAssetIds: [resultAsset.id] } });
}
if (url === "/api/assets") return Response.json({ assets: [resultAsset] });
return Response.json({ error: "unexpected request" }, { status: 404 });
});
vi.stubGlobal("fetch", fetchMock);
const pending = waitForJobOutputAssets(succeededBeforeOutputSync, new AbortController().signal);
await vi.runAllTimersAsync();
await expect(pending).resolves.toEqual([resultAsset]);
expect(fetchMock).toHaveBeenCalledWith("/api/generations/image/job-edit-1", expect.objectContaining({ cache: "no-store" }));
expect(fetchMock).toHaveBeenCalledWith("/api/assets", expect.objectContaining({ cache: "no-store" }));
});
it("retries the asset collection while the registered result is not readable yet", async () => {
vi.useFakeTimers();
vi.stubGlobal("window", { setTimeout, clearTimeout });
let assetReads = 0;
const job = { ...succeededBeforeOutputSync, outputAssetIds: [resultAsset.id] };
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/generations/image/job-edit-1") return Response.json({ job });
if (url === "/api/assets") {
assetReads += 1;
return assetReads === 1
? Response.json({ assets: [] })
: Response.json({ assets: [resultAsset] });
}
return Response.json({ error: "unexpected request" }, { status: 404 });
});
vi.stubGlobal("fetch", fetchMock);
const pending = waitForJobOutputAssets(job, new AbortController().signal);
await vi.runAllTimersAsync();
await expect(pending).resolves.toEqual([resultAsset]);
expect(assetReads).toBe(2);
});
it("offers result-only retry instead of submitting and billing another edit task", async () => {
const viewer = await readFile(new URL("../components/seedream-layer-result-viewer.tsx", import.meta.url), "utf8");
expect(viewer).toContain("retryLayerEditResultSync");
expect(viewer).toContain("重新同步结果");
expect(viewer).toContain("不会再次提交任务或计费");
});
});