// Run with a disposable migrated database, POSTGRES_MAX_CONNECTIONS=1 and // node --conditions=react-server --import tsx --test . import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { after, before, test } from "node:test"; import { and, eq, sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { syncCanvasNodesForProject } from "@/lib/db/queries"; import { canvasNode, canvasProject, imageArchiveTask, mediaAsset, mediaAssetRelation, user } from "@/lib/db/schema"; import { resolveArchivedImageUrls } from "@/lib/storage/image-archive-db"; const database = new URL(process.env.POSTGRES_URL || "http://missing"); if (!["127.0.0.1", "localhost", "::1"].includes(database.hostname)) throw new Error("Use a disposable local database"); if (!/_test$/.test(database.pathname)) throw new Error("Use a disposable test database"); if (process.env.POSTGRES_MAX_CONNECTIONS !== "1") throw new Error("A single connection makes accidental pool re-entry observable"); const userId = randomUUID(); const projectId = randomUUID(); const nodeId = randomUUID(); const assetId = randomUUID(); const sourceUrl = `https://provider.example/${randomUUID()}.png`; const targetUrl = `https://storage.example/${randomUUID()}.png`; const canvas = (x: number) => ({ nodes: [{ id: nodeId, type: "image", position: { x, y: 0 }, data: { src: sourceUrl } }], edges: [] }); before(async () => { await db.insert(user).values({ id: userId, email: `${userId}@transaction-test.invalid` }); await db.insert(canvasProject).values({ id: projectId, userId, title: "Transaction regression", canvasData: canvas(0) }); await db.insert(mediaAsset).values({ id: assetId, userId, projectId, mediaType: "image", sourceType: "generated", originalUrl: targetUrl, normalizedUrl: targetUrl }); await db.insert(imageArchiveTask).values({ sourceUrl, targetUrl, status: "stored", archiveAfterAt: new Date(), replaceAfterAt: new Date() }); }); after(async () => { await db.delete(imageArchiveTask).where(eq(imageArchiveTask.sourceUrl, sourceUrl)); await db.delete(user).where(eq(user.id, userId)); await db.$client.end({ timeout: 2 }); }); test("archive URL lookup stays on the existing connection", { timeout: 8000 }, async () => { const result = await db.transaction(async (tx) => { await tx.execute(sql`set local idle_in_transaction_session_timeout = '2s'`); await tx.select().from(canvasProject).where(eq(canvasProject.id, projectId)).for("update"); return resolveArchivedImageUrls(canvas(1), tx); }); assert.equal(result.nodes[0].data.src, targetUrl); }); async function save(x: number) { return db.transaction(async (tx) => { await tx.execute(sql`set local idle_in_transaction_session_timeout = '2s'`); await tx.select().from(canvasProject).where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId))).for("update"); const next = await syncCanvasNodesForProject({ projectId, userId, canvasData: canvas(x) }, tx); await tx.update(canvasProject).set({ canvasData: next }).where(eq(canvasProject.id, projectId)); return next; }); } test("canvas saves reuse the transaction for URL lookup, asset lookup and node relations", { timeout: 8000 }, async () => { const result = await save(2); assert.equal(result.nodes[0].data.src, targetUrl); assert.equal(result.nodes[0].data.assetId, assetId); const nodes = await db.select().from(canvasNode).where(eq(canvasNode.projectId, projectId)); assert.equal(nodes.length, 1); assert.equal((nodes[0].data as { src: string }).src, targetUrl); const relations = await db.select().from(mediaAssetRelation).where(eq(mediaAssetRelation.projectId, projectId)); assert.equal(relations.length, 1); assert.equal(relations[0].fromAssetId, assetId); }); test("ten overlapping saves complete with a pool of one connection", { timeout: 8000 }, async () => { const results = await Promise.all(Array.from({ length: 10 }, (_, x) => save(x + 10))); assert.equal(results.length, 10); for (const result of results) assert.equal(result.nodes[0].data.src, targetUrl); const relations = await db.select().from(mediaAssetRelation).where(eq(mediaAssetRelation.projectId, projectId)); assert.equal(relations.length, 1); }); test("a failed save rolls back node changes and asset relations together", { timeout: 8000 }, async () => { await save(30); await assert.rejects(db.transaction(async (tx) => { await tx.execute(sql`set local idle_in_transaction_session_timeout = '2s'`); await tx.select().from(canvasProject).where(eq(canvasProject.id, projectId)).for("update"); await syncCanvasNodesForProject({ projectId, userId, canvasData: canvas(99) }, tx); throw new Error("intentional rollback"); }), /intentional rollback/); const [node] = await db.select().from(canvasNode).where(eq(canvasNode.id, nodeId)); assert.equal((node.position as { x: number }).x, 30); const relations = await db.select().from(mediaAssetRelation).where(eq(mediaAssetRelation.projectId, projectId)); assert.equal(relations.length, 1); }); test("standalone archive normalization remains compatible", async () => { assert.equal(await resolveArchivedImageUrls(sourceUrl), targetUrl); });