feat(canvas): retain explicit migration eligibility at the entry boundary

This commit is contained in:
2026-11-11 16:50:41 +08:00
parent 9d6d0775a7
commit f1c3b63a32
2 changed files with 16 additions and 1 deletions
+5 -1
View File
@@ -14,15 +14,19 @@ export async function loadCanvasEntry(options: { request: CanvasRequest; workspa
options.signal?.throwIfAborted();
if (board.kind !== 'board' || !matchesRef(board, options.boardRef) || !board.workspace_id || !profile.id) throw new Error('invalid_reference');
if(!options.readModel&&!options.transport)throw new Error('model_loader_unavailable');
let migration:Extract<ModelLoad,{status:'migration-required'}>|undefined;
try {
const model = options.readModel?await options.readModel({accountId:profile.id,workspaceId:board.workspace_id,canvasId:board.id,signal:options.signal,navigationGeneration:options.navigationGeneration??0,...(options.onPreview?{onPreview:(preview:LiveModelPreview)=>options.onPreview!(preview,board,profile)}:{})}):await loadFoundryModel(options.transport!(profile.id), board.workspace_id, board.id, options.registry, options.signal);
options.signal?.throwIfAborted();
if (model.status === 'ready') return { mode: 'headless' as const, board, profile, model };
// Existing projects keep their current editor until the user explicitly migrates them.
// This read describes eligibility, not a frozen write request: the migration
// action must drain legacy writers and refresh source versions before sending.
migration=model;
} catch (error) {
options.signal?.throwIfAborted();
if ((error as { code?: string }).code !== 'not_found') throw error;
}
const snapshot = decodeSnapshot(await options.request(path + '/canvas', { signal: options.signal, headers: { Accept: CANVAS_ACCEPT } }));
return { mode: 'legacy' as const, board, profile, snapshot };
return { mode: 'legacy' as const, board, profile, snapshot,...(migration?{migration}:{}) };
}
+11
View File
@@ -23,3 +23,14 @@ test('explicit migration requirement or missing deployment keeps legacy reads; p
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);
});
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);
});