import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promotionPlan, promoteStableRelease } from './promote-stable-release.mjs'; const version='3.29.0.20261014.2'; const manifest={schemaVersion:1,channel:'versioned',productVersion:version,platform:'linux/amd64',releaseId:'release-id',managedUpdateAllowed:true,scheduledUpdateAllowed:true,rollbackSafe:false,publishedAt:'2026-10-14T00:00:00Z', components:[ ['server-main','comfy-ai/comfy-server',''],['web','comfy-ai/one-ai',''], ['web-no-ssr','comfy-ai/one-ai','-no-ssr'],['ws-gateway','easyaigc/wsgateway',''], ['model-conversion-worker','easyaigc/model-conversion-worker',''], ].map(([key,image,suffix],index)=>({key,image:'registry.cn-shanghai.aliyuncs.com/'+image,tag:version+suffix,digest:'sha256:'+String(index+1).repeat(64),buildId:'source-sha'}))}; function fixture() { const plan=promotionPlan(manifest,version,version); const tags=new Map(plan.flatMap(image=>[[image.source,image.digest],[image.stable,'sha256:'+'9'.repeat(64)]])); const calls=[]; const run=args=>{ calls.push(args); if (args[0]==='manifest') return JSON.stringify({Descriptor:{platform:{os:'linux',architecture:'amd64'}}}); if (args[2]==='inspect') { const ref=args[3]; // 真实单平台 imagetools 输出没有 platform,需读取 verbose manifest。 return JSON.stringify({digest:tags.get(ref),...(ref.includes('comfy-server') ? {} : {manifests:[{platform:{os:'linux',architecture:'amd64'}}]})}); } assert.equal(args[2],'create'); assert.ok(args.includes('--prefer-index=false')); const ref=args[args.indexOf('--tag')+1]; const digest=args.at(-1).split('@')[1]; tags.set(ref,digest); return ''; }; return {plan,tags,calls,run}; } test('未明确确认同一个版本及浮动版本都不调用Docker',()=>{ let called=false; for (const [candidate,confirm] of [[version,undefined],[version,'3.29.0.20261014.3'],['latest','latest']]) { assert.throws(()=>promoteStableRelease({manifest,version:candidate,confirmedVersion:confirm,run:()=>{called=true;}}),/人工指定/); } assert.equal(called,false); }); test('候选必须包括SSR/noSSR,并拒绝重复、错误镜像和浮动源标签',()=>{ const variant = {...manifest.components[1],key:'hmg-ai',tag:version+'-hmg-ai'}; assert.equal(promotionPlan({...manifest,components:[...manifest.components,variant]},version,version).at(-1).stable,variant.image+':latest-hmg-ai'); for (const components of [manifest.components.filter(item=>item.key!=='web-no-ssr'), [...manifest.components,manifest.components[0]], manifest.components.map((item,index)=>index?item:{...item,image:'wrong/image'}), manifest.components.map((item,index)=>index?item:{...item,tag:'latest'})]) { assert.throws(()=>promotionPlan({...manifest,components},version,version)); } }); test('全部固定产物先核验;任一digest不匹配时稳定标签和清单保持',()=>{ const f=fixture(); f.tags.set(f.plan.at(-1).source,'sha256:'+'8'.repeat(64)); assert.throws(()=>promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:'unused',run:f.run}),/核验失败/); assert.ok(f.calls.every(args=>args[2]!=='create')); assert.ok(f.plan.every(image=>f.tags.get(image.stable)==='sha256:'+'9'.repeat(64))); }); test('dry-run核验固定版本且不写任何稳定标签或文件',()=>{ const f=fixture(); assert.equal(promoteStableRelease({manifest,version,confirmedVersion:version,dryRun:true,run:f.run}).status,'DRY_RUN'); assert.ok(f.calls.every(args=>args[2]!=='create')); }); test('人工提升只按已核验digest补稳定标签,两种前端分别保留;不构建业务或改源版本',()=>{ const dir=mkdtempSync(join(tmpdir(),'easyai-promote-')); try { const path=join(dir,'stable.json'); const f=fixture(); const result=promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:path,run:f.run}); assert.equal(result.status,'PROMOTED'); assert.ok(f.plan.every(image=>f.tags.get(image.stable)===image.digest)); assert.ok(f.plan.every(image=>f.tags.get(image.source)===image.digest)); assert.equal(JSON.parse(readFileSync(path)).channel,'stable'); assert.deepEqual(JSON.parse(readFileSync(path)).components.map(item=>item.key),['server-main','web','ws-gateway']); assert.ok(f.calls.every(args=>args[0]!=='build' && args[0]!=='push')); } finally { rmSync(dir,{recursive:true,force:true}); } }); test('稳定别名写入后回执失败时恢复原指针,稳定清单不提前写入',()=>{ const dir=mkdtempSync(join(tmpdir(),'easyai-promote-fail-')); try { const path=join(dir,'stable.json'); writeFileSync(path,'old stable manifest'); const f=fixture(); let once=true; const run=args=>{ const output=f.run(args); if (args[2]==='create' && args.includes(f.plan[2].stable) && once) { once=false; throw new Error('write succeeded but response lost'); } return output; }; assert.throws(()=>promoteStableRelease({manifest,version,confirmedVersion:version,outputPath:path,run}),/未完成.*已恢复/); assert.ok(f.plan.every(image=>f.tags.get(image.stable)==='sha256:'+'9'.repeat(64))); assert.equal(readFileSync(path,'utf8'),'old stable manifest'); } finally { rmSync(dir,{recursive:true,force:true}); } });