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