Files

48 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画布入口的 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);
});