import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; import { call, login, fixture, writeFixture, report, work, hash } from './scene-editor-closure-live.mjs'; const data=fixture(), session=await login('root'), token=session.accessToken; const result={time:new Date().toISOString(),status:'PENDING',originals:[],projects:[],users:[],roles:[]}; const originalSnapshots=new Map(); function authoredSnapshot(detail) { const project={...detail.project}; // Dependency creation/removal refreshes these two derived bookkeeping fields. // Keep every authored field, version, asset and dependency in the comparison. delete project.usageCount;delete project.updateTime; return {...detail,project}; } try { const me=(await call('auth/v1/auth/me',token)).data; const originals=new Set(Object.values(data.originals).map(o=>String(o.id))); const owners=new Set([String(me.id),String(data.user?.id),...(data.extraUsers||[]).map(u=>String(u.id))]); for(const [key,original] of Object.entries(data.originals)) { const baseline=JSON.parse(fs.readFileSync(path.join(work,`original-${key}.json`),'utf8')); assert.equal(hash(baseline),original.hash,`Original snapshot integrity ${original.id}`); originalSnapshots.set(String(original.id),baseline); const current=await call(`tran/v1/content/projects/${original.id}`,token); assert.equal(current.status,200);assert.equal(hash(authoredSnapshot(current.data)),hash(authoredSnapshot(baseline)),`Original ${original.id} authored data unchanged`); result.originals.push({id:original.id,name:original.name,version:original.version,unchanged:true,scope:'全部创作字段、版本、资产、依赖;引用统计与其触发的更新时间单独核对', before:{usageCount:baseline.project.usageCount,updateTime:baseline.project.updateTime}}); } // Training copies can depend on scene copies; remove dependents first. const registrations=[...data.projects].sort((a,b)=>Number(b.id)-Number(a.id)); for(const registered of registrations) { const id=String(registered.id);assert.ok(!originals.has(id)); const found=await call(`tran/v1/content/projects/${id}`,token); if(found.status===404){result.projects.push({id,alreadyAbsent:true});continue;} assert.equal(found.status,200);let p=found.data.project; assert.equal(p.code,registered.code,`Registered project identity ${id}`); assert.ok(p.name.startsWith('场景闭环-'),`Review-owned name ${id}`); assert.ok(owners.has(String(p.ownerUserId)),`Review-owned actor ${id}`); assert.ok(p.addTime>=Math.floor(Date.parse(data.createdAt)/1000),`Review creation time ${id}`); let withdrawn=false; if(p.status==='PUBLISHED'){ const revoked=await call(`tran/v1/content/projects/${id}/review`,token,{action:'REVOKE',version:p.version,comment:'场景编辑器闭环验收完成,清理独立测试副本'}); assert.equal(revoked.status,200,`Revoke isolated ${id}: ${revoked.message}`);p=revoked.data.project;withdrawn=true; } const removed=await call(`tran/v1/content/projects/${id}?version=${p.version}`,token,undefined,'DELETE'); assert.equal(removed.status,200,`Remove isolated ${id}: ${removed.message}`); result.projects.push({id,name:p.name,withdrawn,removed:true}); } for(const user of [data.user,...(data.extraUsers||[])].filter(Boolean)){ assert.ok(/^(rv_scene_|scene_perm_)/.test(user.username),'Only registered review users'); const current=await call(`auth/v1/users/${user.id}`,token); if(current.status===404){result.users.push({id:user.id,alreadyAbsent:true});continue;} assert.equal(current.status,200);assert.equal(current.data.username,user.username);assert.equal(current.data.isProtected,false); const removed=await call(`auth/v1/users/${user.id}`,token,{version:current.data.dataVersion??current.data.version},'DELETE'); assert.equal(removed.status,200,`Remove isolated user: ${removed.message}`);result.users.push({id:user.id,removed:true}); } for(const id of data.roleIds||[]){ const current=await call(`auth/v1/roles/${id}`,token); if(current.status===404){result.roles.push({id,alreadyAbsent:true});continue;} assert.equal(current.status,200);assert.ok(String(current.data.code).startsWith('scene_')); const removed=await call(`auth/v1/roles/${id}`,token,{version:current.data.dataVersion??current.data.version},'DELETE'); assert.equal(removed.status,200,`Remove isolated role: ${removed.message}`);result.roles.push({id,removed:true}); } for(const original of result.originals){ const baseline=originalSnapshots.get(String(original.id)),current=await call(`tran/v1/content/projects/${original.id}`,token); assert.equal(current.status,200);assert.equal(hash(authoredSnapshot(current.data)),hash(authoredSnapshot(baseline))); assert.equal(current.data.project.usageCount,baseline.project.usageCount,`Original ${original.id} reference count restored`); original.after={usageCount:current.data.project.usageCount,updateTime:current.data.project.updateTime}; original.fullRecordUnchanged=hash(current.data)===hash(baseline); original.referenceCountRestored=true; } result.note='测试副本增删会刷新原件引用统计及更新时间;创作内容、版本、资产和依赖未改写,引用数量已恢复,未回填或伪造原更新时间。'; result.status='PASS';data.cleanedAt=result.time;writeFixture(data); console.log(JSON.stringify({status:'PASS',originals:result.originals.length,projects:result.projects.length,users:result.users.length,roles:result.roles.length})); }catch(error){result.status='FAIL';result.error=error.message;process.exitCode=1;console.error(error.message);} finally{fs.writeFileSync(path.join(report,'cleanup.json'),JSON.stringify(result,null,2));await call('auth/v1/auth/logout',token,{}).catch(()=>{});}