import assert from 'node:assert/strict' import { test } from 'node:test' import { evaluateObservedInstallationBudget as evaluate } from './installation-memory-budget.mjs' const MiB=1024*1024 function fixture() { const containers=['backend','postgres','frontend','portal'].map((role,index)=>({id:String(index+1).repeat(64),role,running:true,imageId:'sha256:'+'a'.repeat(64),startedAt:'2026-10-28T12:00:00Z',memoryLimitBytes:0,reportedLimitBytes:2*1024*MiB})) const roleById=Object.fromEntries(containers.map(row=>[row.id,row.role])) return {inventory:{readOnly:true,containerSetStable:true,environmentRead:false,serviceChanges:false,newFixtureStarted:false,hostMemTotalBytes:2021212*1024,containers},roleById} } test('observed zero configured caps stay unbounded despite positive Docker reported host limits',()=>{ const f=fixture(), result=evaluate(f.inventory,f.roleById) assert.equal(result.budgetFits,false);assert(result.reasons.includes('UNBOUNDED_CONTAINER'));assert.equal(result.configuredContainerBytes,0) assert.equal(result.wholeInstallation2GbAccepted,false);assert.equal(result.roleLabelsAuthenticated,false);assert.equal(result.inventoryFreshnessProven,false) }) test('complete explicit configured caps can fit arithmetic without becoming runtime acceptance',()=>{ const f=fixture();[512,384,160,192].forEach((limit,index)=>{f.inventory.containers[index].memoryLimitBytes=limit*MiB}) const result=evaluate(f.inventory,f.roleById);assert(result.budgetFits);assert.equal(result.plannedBytes,1760*MiB);assert.equal(result.changesAuthorized,false) }) test('uncertain snapshots, started fixtures and missing limit/identity fields refuse',()=>{ for(const mutate of [f=>{f.inventory.containerSetStable=false},f=>{f.inventory.serviceChanges=true},f=>{f.inventory.newFixtureStarted=true},f=>{delete f.inventory.containers[0].memoryLimitBytes},f=>{f.inventory.containers[0].running=false},f=>{f.inventory.containers[0].imageId='unknown'},f=>{f.inventory.containers[0].startedAt='invalid'}]){const f=fixture();mutate(f);assert.throws(()=>evaluate(f.inventory,f.roleById))} }) test('missing, foreign, inherited and accessor role labels cannot silently classify a service',()=>{ for(const mode of ['missing','foreign','inherited','accessor']){ const f=fixture(),key=f.inventory.containers[0].id if(mode==='missing')delete f.roleById[key] if(mode==='foreign')f.roleById['f'.repeat(64)]='other' if(mode==='inherited'){const labels=Object.create({[key]:'backend'});for(const [id,role]of Object.entries(f.roleById))if(id!==key)labels[id]=role;f.roleById=labels} if(mode==='accessor')Object.defineProperty(f.roleById,key,{enumerable:true,get(){assert.fail('Role getter must not execute')}}) assert.throws(()=>evaluate(f.inventory,f.roleById)) } })