Files
MikrotikManager/backend/scripts/acceptance/installation-memory-budget.test.mjs
T

47 lines
3.9 KiB
JavaScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { evaluateInstallationMemoryBudget as evaluate } from './installation-memory-budget.mjs'
const MiB = 1024 * 1024
function fixture() {
const containers = [['backend',512],['postgres',384],['frontend',160],['portal',192]].map(([role, limit], i) => ({ id: String(i+1).repeat(64), role, configuredLimitBytes: limit * MiB }))
return { hostMemTotalBytes: 2021212 * 1024, osReserveBytes: 512 * MiB, observedContainerIds: containers.map(row => row.id), containers }
}
test('complete four-service candidate fits arithmetic, never runtime acceptance or change permission', () => {
const input = fixture(), before = structuredClone(input), result = evaluate(input)
assert.equal(result.budgetFits,true); assert.equal(result.plannedBytes,1760 * MiB)
assert.equal(result.wholeInstallation2GbAccepted,false); assert.equal(result.runtimeAcceptanceClaimed,false); assert.equal(result.changesAuthorized,false)
assert(Object.isFrozen(result)); assert(Object.isFrozen(result.reasons)); assert.deepEqual(input,before)
})
test('coexisting baseline and owned app/PG cannot represent one installation', () => {
const input = fixture()
for (const [i,role,limit] of [[5,'backend',320],[6,'postgres',192]]) { input.containers.push({id:String(i).repeat(64),role,configuredLimitBytes:limit*MiB}); input.observedContainerIds.push(String(i).repeat(64)) }
const result = evaluate(input)
assert.equal(result.budgetFits,false); assert(result.reasons.includes('DUPLICATE_APPLICATION_STACK')); assert(result.reasons.includes('INSTALLATION_BUDGET_EXCEEDS_HOST'))
})
test('unknown host containers, missing and foreign inventory entries refuse', () => {
for (const mode of ['omitted','foreign','missing-service']) {
const input = fixture()
if (mode === 'omitted') input.observedContainerIds.push('a'.repeat(64))
if (mode === 'foreign') input.containers[0].id='b'.repeat(64)
if (mode === 'missing-service') { input.containers.splice(0,1); input.observedContainerIds.splice(0,1) }
const result = evaluate(input); assert.equal(result.budgetFits,false)
assert(result.reasons.includes(mode === 'missing-service'?'REQUIRED_SERVICE_MISSING':'INCOMPLETE_HOST_CONTAINER_INVENTORY'))
}
})
test('unlimited cap is distinct from observed idle RSS or Docker host limit display', () => {
const input = fixture(); input.containers[0].configuredLimitBytes=0
assert(evaluate(input).reasons.includes('UNBOUNDED_CONTAINER'))
input.containers[0].configuredLimitBytes=3*1024*MiB
assert(evaluate(input).reasons.includes('CONTAINER_LIMIT_EXCEEDS_HOST'))
})
test('extra service, reserve and physical host all count toward the same budget', () => {
const input = fixture(); input.containers.push({id:'a'.repeat(64),role:'other',configuredLimitBytes:256*MiB}); input.observedContainerIds.push('a'.repeat(64))
assert(evaluate(input).reasons.includes('INSTALLATION_BUDGET_EXCEEDS_HOST'))
const low = fixture(); low.osReserveBytes=128*MiB; assert(evaluate(low).reasons.includes('OS_RESERVE_BELOW_CANDIDATE_FLOOR'))
const large = fixture(); large.hostMemTotalBytes=4*1024*MiB; assert(evaluate(large).reasons.includes('HOST_OUTSIDE_2GIB_TARGET'))
})
test('malformed, duplicate, fractional, oversized or unsafe inventory rejects without I/O', () => {
const mutations = [input=>{input.inject='unknown'},input=>{input.containers[0].role='invented'},input=>{input.containers[0].configuredLimitBytes=-1},input=>{input.containers[0].configuredLimitBytes=0.5},input=>{input.containers[0].id='../foreign'},input=>{input.containers[1].id=input.containers[0].id},input=>{input.observedContainerIds.push(input.observedContainerIds[0])},input=>{input.containers[0].configuredLimitBytes=Number.MAX_SAFE_INTEGER},input=>{input.observedContainerIds=Array(129).fill('a'.repeat(64))}]
for (const mutate of mutations) { const input=fixture();mutate(input);assert.throws(()=>evaluate(input)) }
})