/** * 画布入口的 canonical-first 选择测试。已迁移项目直接进入 headless,只有明确迁移需求或未部署接口可走旧快照。 * 权限失败不能降级成旧数据读取;入口只保留已验证迁移材料,不自动发迁移请求。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { loadCanvasEntry } from '../../src/lib/canvas-entry.ts'; import { createRegistry, builtinComponents } from '../../src/lib/canvas-core/index.ts'; // UUID 工厂区分 board、workspace、profile,供入口解析和 service 身份验证。 const id = n => `10000000-0000-4000-8000-${String(n).padStart(12, '0')}`; // 模拟已解析 board,workspaceRef/boardRef 以明确 ID 输入,避免把 slug 解析与模式选择混成同一个断言。 const board = { id: id(1), workspace_id: id(2), kind: 'board', title: 'Board', version: 1 }; // 当前账户与内置 registry,transport 工厂必须收到该 profile 身份。 const profile = { id: id(3) }, registry = createRegistry(builtinComponents()); // request 提供 profile/board/旧 canvas,transport 返回注入 canonical 响应;所有路径记录到 calls 检查不应发生的 fallback。 function fixture(response) { const calls = [], request = async path => { calls.push(path); if (path === 'profile') return profile; if (path.endsWith('/canvas')) return { version: 1, document: { schema_version: 2, nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } } }; return board; }; return { calls, options: { request, workspaceRef: board.workspace_id, boardRef: board.id, registry, transport: accountId => { assert.equal(accountId, profile.id); return async input => { calls.push(input.path); return response; }; } } }; } // canonical 完整模型已可用时只进入 headless,不能再读取旧 canvas 快照制造第二份文档来源。 test('canonical-first entry never reads a legacy snapshot for an adopted project', async () => { const f = fixture({ status: 200, body: { model: { documentFormatVersion: 1, projectId: board.id, complete: true, canvases: [{ id: board.id, parent: null }], elements: [], tombstones: {}, versions: { [board.id]: 1 }, removedCanvasIds: [] } } }); const result = await loadCanvasEntry(f.options); assert.equal(result.mode, 'headless'); assert.equal(result.model.document.projectId, board.id); assert.equal(f.calls.some(path => path.endsWith('/canvas')), false); }); // 404 未部署或合法 migration_required 允许读取旧快照;403 权限失败直接报错,绝不退回 legacy。 test('explicit migration requirement or missing deployment keeps legacy reads; permission failure does not fall back', async () => { for (const response of [{ status: 404, body: { error: 'not_found' } }, { status: 409, body: { error: 'canonical_migration_required', root_id: board.id, document_format_version: 1, source_versions: { [board.id]: 1 } } }]) { const f = fixture(response); assert.equal((await loadCanvasEntry(f.options)).mode, 'legacy'); assert.equal(f.calls.filter(path => path.endsWith('/canvas')).length, 1); } const f = fixture({ status: 403, body: { error: 'forbidden' } }); await assert.rejects(() => loadCanvasEntry(f.options), /forbidden/); assert.equal(f.calls.some(path => path.endsWith('/canvas')), false); }); // legacy 入口保留验证过的 root/source 版本向量及 profile/snapshot,不发 migrate;404 未部署则不提供虚构迁移材料。 // 固定响应只检验入口如何选择模式、保留材料和安排请求,不执行模型迁移或旧画布写入。 test('legacy entry retains verified migration root and the full source vector without offering migration for an unavailable API',async()=>{ const child=id(4),f=fixture({status:409,body:{error:'canonical_migration_required',root_id:board.id,document_format_version:1,source_versions:{[board.id]:3,[child]:7}}}); const result=await loadCanvasEntry(f.options); assert.equal(result.mode,'legacy');assert.equal(result.migration.rootId,board.id); assert.deepEqual([...result.migration.sourceVersions],[[board.id,3],[child,7]]);assert.equal(result.migration.sourceVersions.set,undefined); assert.equal(result.profile.id,profile.id);assert.ok(result.snapshot); assert.equal(f.calls.some(path=>path.endsWith('/migrate')),false); const unavailable=await loadCanvasEntry(fixture({status:404,body:{error:'not_found'}}).options); assert.equal(unavailable.mode,'legacy');assert.equal(unavailable.migration,undefined); });