gongxiangp2/scripts/salary-grade-configuration.test.cjs

10 lines
3.3 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),engine=require('./salary-grade-configuration.cjs')
const fixture=()=>[{id:'b9b80000-0000-4000-8000-000000001002',name:'AI策划师-创意部',revision:5,salaryRole:'ai-planning',description:'old',dimensions:[{id:'d',weight:'100',metrics:[]}],personnelCount:7},...['P4','P3'].map((grade,i)=>({id:'live-'+i,name:'AI策划师-创意部-'+grade,revision:1,salaryRole:'ai-planning',description:grade,dimensions:[{id:grade,weight:'100',metrics:[]}],personnelCount:0}))]
function harness(){let rows=fixture(),calls=0;return {get rows(){return rows},get calls(){return calls},request:async(method,id,body)=>{if(method==='GET')return {positions:structuredClone(rows)};calls++;const p=rows.find(p=>p.id===id);assert.equal(body.revision,p.revision);Object.assign(p,body,{revision:p.revision+1});return structuredClone(p)}}}
const options=h=>({environment:'production',request:h.request,save:async()=>{}})
test('uses live scheme IDs and stable parent, preserves scheme weights/sources; repeat apply is a no-op',async()=>{const h=harness(),m=engine.prepare(h.rows,{environment:'production'});assert.equal(m.operations[1].before.id,'live-0');await engine.apply(m,options(h));assert.equal(h.calls,3);await engine.apply(m,options(h));assert.equal(h.calls,3);assert.equal(h.rows[0].dimensions.length,0);assert.equal(h.rows[1].jobGrade,'P4');assert.deepEqual(h.rows[1].dimensions,fixture()[1].dimensions)})
test('preflights all revisions and name conflicts before any write',async()=>{const h=harness(),m=engine.prepare(h.rows,{environment:'production'});h.rows[2].revision++;await assert.rejects(engine.apply(m,options(h)),/Concurrent/);assert.equal(h.calls,0);assert.throws(()=>engine.prepare([...fixture(),{id:'other',name:'策划师岗'}],{environment:'production'}),/conflict/)})
test('lost apply response requires reconciliation and does not repeat a saved write',async()=>{const h=harness(),m=engine.prepare(h.rows,{environment:'production'});let lost=true;await assert.rejects(engine.apply(m,{...options(h),request:async(...args)=>{const value=await h.request(...args);if(args[0]==='PUT'&&lost){lost=false;throw Error('response lost')}return value}}),/lost/);assert.equal(m.operations[0].pending,true);await engine.reconcile(m,options(h));await engine.apply(m,options(h));assert.equal(h.calls,3)})
test('restore is exact and reverse ordered; manual changes stop restoration',async()=>{const h=harness(),m=engine.prepare(h.rows,{environment:'production'});await engine.apply(m,options(h));h.rows[1].revision++;await assert.rejects(engine.restore(m,options(h)),/conflict/);assert.equal(h.calls,3);h.rows[1].revision--;await engine.restore(m,options(h));for(let i=0;i<3;i++)assert.deepEqual(engine.fields(h.rows[i]),engine.fields(fixture()[i]));await assert.rejects(engine.apply(m,options(h)),/restored/)})
test('uncertain restore response reconciles safely without overwriting a new version',async()=>{const h=harness(),m=engine.prepare(h.rows,{environment:'production'});await engine.apply(m,options(h));let lost=true;await assert.rejects(engine.restore(m,{...options(h),request:async(...args)=>{const value=await h.request(...args);if(args[0]==='PUT'&&lost){lost=false;throw Error('lost')}return value}}),/lost/);await engine.reconcile(m,options(h));await engine.restore(m,options(h));assert.equal(h.calls,6)})