import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createFoundryAssets, createFoundryTransport } from '../../src/lib/canvas-service/index.ts'; const id = n => `10000000-0000-4000-8000-${String(n).padStart(12, '0')}`; async function fixture() { const blob = new Blob(['immutable image'], { type: 'image/png' }); const digest = [...new Uint8Array(await crypto.subtle.digest('SHA-256', await blob.arrayBuffer()))].map(value => value.toString(16).padStart(2, '0')).join(''); const resource = { id: id(1), operationId: id(2), name: 'picture.png', byteLength: blob.size, mimeType: blob.type, digest }; const request = { kind: 'asset.upload', protocolVersion: 2, id: id(3), operationIds: [resource.operationId], canvasIds: [], expectedVersions: {}, phase: 'frozen', outcome: 'unresolved', generation: 1, attempt: 0, body: JSON.stringify({ protocol_version: 2, request_id: id(3), resource_id: resource.id, operation_id: resource.operationId, name: resource.name, mime_type: resource.mimeType, byte_length: resource.byteLength, sha256: resource.digest }) }; const calls = [], receipt = { protocol_version: 2, request_id: request.id, status: 'applied', asset: { id: resource.id, bytes: resource.byteLength, mime_type: 'image/png', width: 2, height: 1, url: `https://api.foundry.entroia.com/projects/endoleap/workspaces/${id(4)}/assets/${resource.id}` } }; let response = { status: 201, body: receipt }; const assets = createFoundryAssets({ workspaceId: id(4), transport: async input => { calls.push(input); return response; } }); return { blob, resource, request, calls, receipt, assets, respond: next => { response = next; } }; } test('asset adapter preserves byte identity and headers and validates the scoped applied receipt', async () => { const f = await fixture(), result = await f.assets.upload(f.request, f.blob, new AbortController().signal); assert.equal(result.outcome, 'applied'); assert.equal(f.calls[0].requestId, f.request.id); assert.equal(f.calls[0].resourceId, f.resource.id); assert.equal(f.calls[0].body.get('image').name, f.resource.name); assert.equal(await f.calls[0].body.get('image').text(), await f.blob.text()); assert.equal(f.assets.accept(f.request, result, f.resource).id, f.resource.id); for (const patch of [{ id: id(99) }, { bytes: 100 }, { url: 'https://other.example/image' }, { width: 0 }, { width: 1000000, height: 1000000 }]) { const altered = { ...result, canonical: { ...f.receipt, asset: { ...f.receipt.asset, ...patch } } }; assert.throws(() => f.assets.accept(f.request, altered, f.resource), /asset_receipt_mismatch/); } const changed = await f.assets.upload(f.request, new Blob(['different']), new AbortController().signal); assert.equal(changed.outcome, 'unresolved'); assert.equal(changed.blocked, true); assert.equal(f.calls.length, 1); }); test('missing and rejected upload receipts never become proof of zero writes', async () => { const f = await fixture(), signal = new AbortController().signal; f.respond({ status: 404, body: { protocol_version: 2, request_id: f.request.id, status: 'unknown' } }); const unknown = await f.assets.query(f.request, signal); assert.equal(unknown.outcome, 'unresolved'); assert.equal(unknown.blocked, undefined); assert.ok(f.calls[0].path.endsWith(`/assets/operations/${f.request.id}`)); f.respond({ status: 403, body: { error: 'forbidden' } }); assert.equal((await f.assets.query(f.request, signal)).blocked, true); f.respond({ status: 201, body: { ...f.receipt, request_id: id(99) } }); assert.equal((await f.assets.upload(f.request, f.blob, signal)).outcome, 'unresolved'); }); test('multipart browser transport leaves the boundary to fetch and binds the expected account', async () => { let captured; const transport = createFoundryTransport({ accountId: id(5), isCurrentIdentity: () => true, fetch: async (url, init) => { captured = { url, init }; return Response.json({ status: 'applied' }); } }); const body = new FormData(); body.append('image', new Blob(['image']), 'a.png'); await transport({ method: 'POST', path: 'workspaces/example/assets', body, requestId: id(3), resourceId: id(1) }); const headers = new Headers(captured.init.headers); assert.equal(headers.has('content-type'), false); assert.equal(headers.get('X-Expected-Account'), id(5)); assert.equal(headers.get('X-Resource-Id'), id(1)); assert.equal(headers.get('Idempotency-Key'), id(3)); assert.equal(captured.init.body, body); });