import { expect, test } from "@playwright/test"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import vm from "node:vm"; import * as crypto from "node:crypto"; import ts from "typescript"; import { collectImageArchiveSources, replaceArchivedImageUrls, } from "@/lib/storage/image-archive-urls"; import { normalizeCanvasImageTaskQueueConfig } from "@/lib/canvas/image-task-config"; import type { ImageNodeData } from "@/types/canvas"; import { useCanvasStore } from "@/store/canvas"; const root = path.resolve(__dirname, "../.."); const png = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64", ); const raw = "https://provider.example/result.png?signature=keep-me"; const permanent = "https://bucket.example/result.png"; function deferred() { let resolve!: (value: T) => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; } // Execute the actual server modules with isolated DB/storage adapters: no provider // requests, production DB connections, or paid generations are made by these tests. function serverLoader(mocks: Record, env: Record = {}) { const modules = new Map(); const runtimeGlobal = {}; const load = (relative: string): any => { const filename = path.resolve(root, relative); if (modules.has(filename)) return modules.get(filename)!.exports; const module = { exports: {} }; modules.set(filename, module); const source = ts.transpileModule(readFileSync(filename, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true, }, }).outputText; const nativeRequire = createRequire(filename); const requireMock = (name: string) => { if (name === "server-only") return {}; if (Object.prototype.hasOwnProperty.call(mocks, name)) return mocks[name]; if (name.startsWith("@/")) return load(`${name.slice(2)}.ts`); if (name.startsWith(".")) return load(path.relative(root, path.resolve(path.dirname(filename), `${name}.ts`))); return nativeRequire(name); }; const context = vm.createContext({ module, exports: module.exports, require: requireMock, globalThis: runtimeGlobal, process: { env }, Buffer, URL, Response, crypto, setTimeout, clearTimeout, setImmediate, AbortSignal, console: { log() {}, warn() {}, error() {}, info() {} }, fetch: mocks.fetch || (() => { throw new Error("Unexpected network call"); }), }); new vm.Script(source, { filename }).runInContext(context); return module.exports; }; return load; } const loggerMock = { createLogger: () => ({ info() {}, warn() {}, error() {} }) }; test("160 requests from the same user and model start without admission waiting", async () => { const module = serverLoader({})("lib/canvas/image-concurrency.ts"); let queued = 0; const slots = await Promise.all( Array.from({ length: 160 }, () => module.acquireCanvasImageModelSlot({ modelKey: "same-model", ownerKey: "same-user", onQueued: () => { queued += 1; }, }), ), ); expect(module.getCanvasImageRunningCountForModel("same-model")).toBe(160); expect(queued).toBe(0); expect(slots.every((slot: any) => slot.waitMs === 0 && slot.maxConcurrent === 0)).toBe(true); await Promise.all(slots.map((slot: any) => slot.release())); await slots[0].release(); expect(module.getCanvasImageRunningCountForModel("same-model")).toBe(0); expect( (await module.getCanvasImageModelQueueStatus({ modelKey: "same-model" })).estimatedWaitMs, ).toBe(0); expect( normalizeCanvasImageTaskQueueConfig( { concurrency: 3, batchDispatchIntervalMs: 1000 }, { envConcurrency: "2" }, ), ).toEqual({ concurrency: 0, batchDispatchIntervalMs: 0 }); }); test("provider preview resolves while a bucket upload is still blocked", async () => { const bucket = deferred(); let uploadCalls = 0; let downloadCalls = 0; let workerCalls = 0; const registered: string[] = []; const module = serverLoader( { "@/lib/observability/logger": loggerMock, "@/lib/storage/image-archive-db": { enqueueImageArchiveTasks: async ({ urls }: { urls: string[] }) => { registered.push(...urls); return []; }, getImageArchiveDisplayHoldMs: () => 0, imageArchiveSourceKey: (url: string) => url, }, "./image-archive-worker": { scheduleImageArchiveWorker: () => { workerCalls += 1; }, }, "@/lib/storage/service": { uploadFile: async () => { uploadCalls += 1; return bucket.promise; }, }, fetch: async () => { downloadCalls += 1; return new Response(png, { headers: { "content-type": "image/png" } }); }, }, { AI_IMAGE_IMMEDIATE_ARCHIVE: "true", AI_IMAGE_DEFERRED_ARCHIVE: "false" }, )("lib/storage/image-archive.ts"); const archive = module.archiveTemporaryImage(raw); await expect.poll(() => uploadCalls).toBe(1); const preview = await module.createDeferredUploadedImage(raw); expect(preview).toEqual({ original: raw, thumbnail: raw }); expect(uploadCalls).toBe(1); expect(downloadCalls).toBe(1); expect(registered).toEqual([raw]); expect(workerCalls).toBe(1); bucket.resolve({ url: permanent, pathname: "result.png", contentType: "image/png" }); expect((await archive).url).toBe(permanent); }); test("inline outputs have short, stable preview identities and durable queue payloads", async () => { let inserted: any; const db = { insert: () => ({ values: (value: unknown) => { inserted = value; return { onConflictDoUpdate: () => ({ returning: async () => [inserted] }) }; }, }), }; const module = serverLoader( { "@/lib/db": { db }, "@/lib/db/schema": { imageArchiveTask: { sourceUrl: "sourceUrl" } }, }, { NEXT_PUBLIC_APP_URL: "https://app.example/subpath", IMAGE_ARCHIVE_DISPLAY_HOLD_HOURS: "1" }, )("lib/storage/image-archive-db.ts"); const data = `data:image/png;base64,${png.toString("base64")}`; await module.enqueueImageArchiveTask({ sourceUrl: data, metadata: { providerType: "gemini-image" }, }); expect(inserted.sourceUrl).toMatch( /^https:\/\/app\.example\/api\/generated-image-preview\/[a-f0-9]{64}$/, ); expect(inserted.metadata.inlineSource).toBe(data); expect(module.imageArchiveSourceKey(data)).toBe(inserted.sourceUrl); expect(module.getImageArchiveDisplayHoldMs()).toBe(0); }); test("background storage starts after delivery and coalesces repeated wakeups", async () => { const callbacks: Array<() => Promise> = []; let runs = 0; const module = serverLoader({ "next/server": { after: (fn: () => Promise) => { callbacks.push(fn); }, }, "@/lib/jobs/image-archive-job": { runImageArchiveJob: async () => { runs += 1; return { store: { total: runs === 1 ? 1 : 0 }, replace: { total: 0 }, stats: { pendingStore: 0, pendingReplace: 0 }, }; }, }, })("lib/storage/image-archive-worker.ts"); module.scheduleImageArchiveWorker(); module.scheduleImageArchiveWorker(); expect(callbacks).toHaveLength(1); expect(runs).toBe(0); await callbacks[0](); expect(runs).toBe(2); }); test("bucket failure retains the provider URL and retry task", async () => { let failed = 0; let stored = 0; let replaced = 0; const module = serverLoader({ "@/lib/storage/image-archive": { archiveTemporaryImage: async () => { throw new Error("Bucket unavailable"); }, }, "@/lib/storage/image-archive-db": { recoverStaleImageArchiveTasks: async () => {}, getImageArchiveTasksToStore: async () => [{ id: "task", sourceUrl: raw, metadata: null }], markImageArchiveTaskArchiving: async () => true, markImageArchiveTaskStored: async () => { stored += 1; }, markImageArchiveTaskFailed: async () => { failed += 1; }, getImageArchiveTasksToReplace: async () => [], getImageArchiveTaskStats: async () => ({ pendingStore: 1, pendingReplace: 0 }), }, "@/lib/storage/image-url-replacement": { replaceImageUrlInDatabase: async () => { replaced += 1; }, }, })("lib/jobs/image-archive-job.ts"); const result = await module.runImageArchiveJob(); expect(result.store.failed).toBe(1); expect(failed).toBe(1); expect(stored).toBe(0); expect(replaced).toBe(0); }); test("URL replacement preserves canvas geometry, selection, dates and undo position", () => { const time = new Date("2026-10-11T01:00:00Z"); const node = { id: "node", type: "image", position: { x: 300, y: 40 }, size: { width: 300, height: 500 }, data: { src: raw, originalUrl: raw }, createdAt: time, }; const unaffected = { id: "text", type: "text", data: { text: "unchanged" } }; const nodes = [node, unaffected]; useCanvasStore.setState({ projectId: "project", nodes: nodes as any, history: [{ nodes } as any], historyIndex: 0, selectedNodeIds: ["node"], }); useCanvasStore.getState().replaceArchivedImageUrls({ [raw]: permanent }); const state = useCanvasStore.getState(); expect((state.nodes[0].data as ImageNodeData).src).toBe(permanent); expect(state.nodes[0].position).toEqual(node.position); expect(state.nodes[0].size).toEqual(node.size); expect(state.nodes[0].createdAt).toBe(time); expect(state.nodes[1]).toBe(unaffected); expect((state.history[0].nodes[0].data as ImageNodeData).src).toBe(permanent); expect(state.historyIndex).toBe(0); expect(state.selectedNodeIds).toEqual(["node"]); const markdown = `![thumb|||${raw}|||2K](${raw})`; expect(replaceArchivedImageUrls(markdown, { [raw]: permanent })).toBe( `![thumb|||${permanent}|||2K](${permanent})`, ); expect(collectImageArchiveSources({ nodes, markdown })).toEqual([raw]); }); test("durable canvas queue starts all same-user jobs and claims each only once", async () => { const tasks = Array.from({ length: 20 }, (_, index) => ({ id: `task-${index}`, userId: "one-user", status: "pending", attempts: 0, request: { model: "one-model", prompt: "test" }, createdAt: new Date(), startedAt: null, })) as any[]; const gate = deferred(); let started = 0; const projection = (rows: any[], shape?: Record) => shape ? rows.map((row) => Object.fromEntries(Object.entries(shape).map(([key, field]) => [key, row[field]])), ) : rows; const query = (shape?: Record, updates?: any) => { let predicate = (_row: any) => true; let limit = Infinity; const execute = () => { const rows = tasks.filter(predicate).slice(0, limit); if (updates) for (const row of rows) Object.assign(row, updates, { attempts: updates.status === "running" ? row.attempts + 1 : row.attempts, }); return rows; }; const builder: any = { from: () => builder, where: (condition: any) => { predicate = condition; return builder; }, orderBy: () => builder, limit: (count: number) => { limit = count; return builder; }, returning: async (returnShape: any) => projection(execute(), returnShape), then: (resolve: any, reject: any) => Promise.resolve(projection(execute(), shape)).then(resolve, reject), }; return builder; }; const sqlMock = (_parts: TemplateStringsArray, ..._args: unknown[]) => ({}); const taskTable = Object.fromEntries( [ "id", "request", "userId", "status", "attempts", "createdAt", "startedAt", "completedAt", "result", "error", "updatedAt", ].map((field) => [field, field]), ); const module = serverLoader({ "@/lib/db": { db: { select: (shape: any) => query(shape), update: () => ({ set: (updates: any) => query(undefined, updates) }), }, }, "@/lib/db/schema": { canvasImageTask: taskTable }, "drizzle-orm": { eq: (field: string, value: unknown) => (row: any) => row[field] === value, and: (...predicates: any[]) => (row: any) => predicates.every((fn) => fn(row)), lt: (field: string, value: any) => (row: any) => row[field] != null && row[field] < value, asc: (field: unknown) => field, notInArray: (field: string, values: unknown[]) => (row: any) => !values.includes(row[field]), sql: sqlMock, }, "./generate-image": { generateCanvasImage: async () => { started += 1; return gate.promise; }, }, "./scale-adjust-model-relight": { applyScaleAdjustModelRelight: async ({ initialResult }: any) => initialResult, }, "@/lib/style-pro/task": { getStyleProTaskPrimaryModelKey: () => "style", executeStyleProTask: async () => { throw new Error("Unexpected style task"); }, }, "@/lib/canvas/layout-replace-task": { isCanvasLayoutReplaceTaskRequest: () => false }, "@/lib/canvas/layout-replace-batch": {}, "@/lib/agents/agent-project-result-persistence": {}, "@/lib/storage/image-archive-db": { resolveArchivedImageUrls: async (value: unknown) => value }, })("lib/canvas/image-task-queue.ts"); await module.enqueueCanvasImageTask(tasks[0].id); await expect.poll(() => started).toBe(20); await module.enqueueCanvasImageTask(tasks[0].id); expect(started).toBe(20); expect(tasks.every((task) => task.status === "running" && task.attempts === 1)).toBe(true); gate.resolve({ images: [{ originalUrl: raw, thumbnailUrl: raw }], model: "one-model", prompt: "test", }); await expect.poll(() => tasks.filter((task) => task.status === "succeeded").length).toBe(20); }); test("successful background archive replaces saved references immediately", async () => { let stored = false; let claimed = false; let completed = false; const calls: string[][] = []; const module = serverLoader({ "@/lib/storage/image-archive": { archiveTemporaryImage: async () => ({ url: permanent, bytes: png.length }), }, "@/lib/storage/image-archive-db": { recoverStaleImageArchiveTasks: async () => {}, getImageArchiveTasksToStore: async () => [{ id: "task", sourceUrl: raw, metadata: null }], markImageArchiveTaskArchiving: async () => { if (claimed) return false; claimed = true; return true; }, markImageArchiveTaskStored: async () => { stored = true; }, markImageArchiveTaskFailed: async () => { throw new Error("Unexpected storage failure"); }, getImageArchiveTasksToReplace: async () => stored && !completed ? [{ id: "task", sourceUrl: raw, targetUrl: permanent }] : [], markImageArchiveTaskReplacing: async () => true, markImageArchiveTaskCompleted: async () => { completed = true; }, getImageArchiveTaskStats: async () => ({ pendingStore: 0, pendingReplace: 0 }), }, "@/lib/storage/image-url-replacement": { replaceImageUrlInDatabase: async (...urls: string[]) => { calls.push(urls); return { changed: 2 }; }, }, })("lib/jobs/image-archive-job.ts"); const result = await module.runImageArchiveJob(); expect(result.store.succeeded).toBe(1); expect(result.replace.succeeded).toBe(1); expect(calls).toEqual([[raw, permanent]]); await module.runImageArchiveJob(); expect(calls).toHaveLength(1); }); test("inline preview serves exact image bytes then redirects to the stored object", async () => { const key = "a".repeat(64); let archived = false; const rows = () => [ { targetUrl: archived ? permanent : null, metadata: { inlineSource: `data:image/png;base64,${png.toString("base64")}` }, }, ]; const module = serverLoader( { "@/lib/db": { db: { select: () => ({ from: () => ({ where: () => ({ limit: async () => rows() }) }) }) }, }, "@/lib/db/schema": { imageArchiveTask: { sourceUrl: "sourceUrl" } }, "next/server": { NextResponse: class extends Response { static redirect(url: string, status: number) { return new Response(null, { status, headers: { Location: url } }); } }, }, }, { NEXT_PUBLIC_APP_URL: "https://app.example" }, )("app/api/generated-image-preview/[key]/route.ts"); const response = await module.GET({}, { params: Promise.resolve({ key }) }); expect(response.headers.get("Content-Type")).toBe("image/png"); expect(Buffer.from(await response.arrayBuffer())).toEqual(png); archived = true; const redirect = await module.GET({}, { params: Promise.resolve({ key }) }); expect(redirect.status).toBe(307); expect(redirect.headers.get("Location")).toBe(permanent); const invalid = await module.GET({}, { params: Promise.resolve({ key: "guess" }) }); expect(invalid.status).toBe(404); }); test("archive lookup requires login and an owned canvas before reading image mappings", async () => { let signedIn = false; let reads = 0; const module = serverLoader({ "@/app/(auth)/auth": { auth: async () => (signedIn ? { user: { id: "owner" } } : null) }, "@/lib/db": { db: { select: () => { reads += 1; return { from: () => ({ where: () => ({ limit: async () => [] }) }) }; }, }, }, "@/lib/db/schema": { canvasProject: { id: "id", userId: "userId" }, imageArchiveTask: {}, generatedImage: {}, canvasImageTask: {}, }, "@/lib/storage/image-archive-db": { imageArchiveSourceKey: (url: string) => url }, "@/lib/storage/image-archive-worker": { scheduleImageArchiveWorker: () => { throw new Error("Unowned request must not start archiving"); }, }, "next/server": { NextResponse: class extends Response { static json(body: unknown, init?: ResponseInit) { return new Response(JSON.stringify(body), init); } }, }, })("app/api/canvas/[id]/image-archives/route.ts"); const request = { json: async () => ({ urls: [raw] }) }; expect( (await module.POST(request, { params: Promise.resolve({ id: "other-project" }) })).status, ).toBe(401); expect(reads).toBe(0); signedIn = true; expect( (await module.POST(request, { params: Promise.resolve({ id: "other-project" }) })).status, ).toBe(404); expect(reads).toBe(1); }); test("replacement retries reconcile late writes once, without reclaiming completed jobs forever", async () => { const past = new Date(Date.now() - 1000); const future = new Date(Date.now() + 60_000); const rows = [ { id: "stored", status: "stored", replaceAfterAt: past }, { id: "settle", status: "completed", replaceAfterAt: past, metadata: { replacementPasses: 1 } }, { id: "too-soon", status: "completed", replaceAfterAt: future, metadata: { replacementPasses: 1 }, }, { id: "done", status: "completed", replaceAfterAt: past, metadata: { replacementPasses: 2 } }, { id: "legacy", status: "completed", replaceAfterAt: past }, ]; const table = Object.fromEntries( ["id", "status", "replaceAfterAt", "metadata", "createdAt", "updatedAt"].map((key) => [ key, key, ]), ); const query = (updates?: any) => { let predicate = (_row: any) => true; const execute = () => { const found = rows.filter(predicate); if (updates) found.forEach((row) => Object.assign(row, updates)); return found; }; const builder: any = { from: () => builder, orderBy: () => builder, limit: () => builder, where: (value: any) => { predicate = value; return builder; }, returning: async () => execute(), then: (resolve: any) => Promise.resolve(execute()).then(resolve), }; return builder; }; const module = serverLoader({ "@/lib/db": { db: { select: () => query(), update: () => ({ set: (updates: any) => query(updates) }) }, }, "@/lib/db/schema": { imageArchiveTask: table }, "drizzle-orm": { and: (...conditions: any[]) => (row: any) => conditions.every((fn) => fn(row)), or: (...conditions: any[]) => (row: any) => conditions.some((fn) => fn(row)), eq: (field: string, value: unknown) => (row: any) => row[field] === value, lte: (field: string, value: any) => (row: any) => row[field] <= value, asc: (field: unknown) => field, sql: (parts: TemplateStringsArray) => { if (!parts.join("").includes("replacementPasses")) throw new Error("Unexpected predicate"); return (row: any) => row.metadata?.replacementPasses === 1; }, }, })("lib/storage/image-archive-db.ts"); expect( (await module.getImageArchiveTasksToReplace({ limit: 50 })).map((row: any) => row.id), ).toEqual(["stored", "settle"]); expect(await module.markImageArchiveTaskReplacing("settle")).toBe(true); expect(await module.markImageArchiveTaskReplacing("settle")).toBe(false); expect(await module.markImageArchiveTaskReplacing("done")).toBe(false); }); test("detached generation still archives when its original request has already closed", async () => { let runs = 0; const module = serverLoader({ // A closed request may accept a callback that is never invoked. "next/server": { after: () => {} }, "@/lib/jobs/image-archive-job": { runImageArchiveJob: async () => { runs += 1; return { store: { total: 0 }, replace: { total: 0 }, stats: { pendingStore: 0, pendingReplace: 0 }, }; }, }, })("lib/storage/image-archive-worker.ts"); module.scheduleImageArchiveWorker(); expect(runs).toBe(0); await expect.poll(() => runs, { timeout: 5_000 }).toBe(1); });