117 lines
4.7 KiB
TypeScript
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("不会再次提交任务或计费");
|
|
});
|
|
});
|