import { describe, expect, test } from 'bun:test'; import { type ResizeContext, computeFloor, validateResize } from './proxmox-resize-guards'; const ctx = (over: Partial = {}): ResizeContext => ({ current: { cpu: 4, memory: 8192, disk: 80 }, floor: { cpu: 4, memory: 8192, disk: 80 }, nodeFreeMemMb: 20000, nodeTotalCores: 6, nodeFreeDiskGb: 500, ...over, }); describe('validateResize (ISS-0150)', () => { test('a valid memory bump within headroom passes and needs a reboot', () => { const d = validateResize({ memory: 16384 }, ctx()); expect(d.errors).toEqual([]); expect(d.needsReboot).toBe(true); expect(d.effective).toEqual({ cpu: 4, memory: 16384, disk: 80 }); }); test('below the floor is rejected (hard, not overridable)', () => { const d = validateResize({ memory: 4096 }, ctx(), { force: true }); expect(d.errors.length).toBe(1); expect(d.errors[0]).toContain('below the module minimum'); }); test('memory growth beyond node free RAM is a capacity error, overridable with --force', () => { const tight = ctx({ nodeFreeMemMb: 1000 }); // only 1 GB free; +8 GB requested const blocked = validateResize({ memory: 16384 }, tight, { force: false }); expect(blocked.errors.length).toBe(1); expect(blocked.capacityOnly).toBe(true); const forced = validateResize({ memory: 16384 }, tight, { force: true }); expect(forced.errors).toEqual([]); }); test('cpu above physical cores is a capacity error', () => { const d = validateResize({ cpu: 8 }, ctx(), { force: false }); expect(d.errors[0]).toContain('exceeds node physical cores'); expect(validateResize({ cpu: 8 }, ctx(), { force: true }).errors).toEqual([]); }); }); describe('validateResize — disk (celilo#1133)', () => { // The whole point of the verb: the registry filled up and celilo had no cure. test('growing the disk passes and needs NO reboot', () => { const d = validateResize({ disk: 160 }, ctx()); expect(d.errors).toEqual([]); expect(d.effective.disk).toBe(160); // Load-bearing. The instances that fill up are the registry, the forge and // the firewall; a resize that needlessly restarts one of those is a worse // cure than the disease, and it would be discovered at the worst moment. expect(d.needsReboot).toBe(false); }); test('a cpu or memory change alongside a disk change still reboots', () => { expect(validateResize({ disk: 160, memory: 16384 }, ctx()).needsReboot).toBe(true); expect(validateResize({ disk: 160, cpu: 6 }, ctx()).needsReboot).toBe(true); }); test('shrink is refused even well above the floor, and even with --force', () => { // 60GB clears the 40GB floor, so this is NOT a floor violation — it is a // shrink, and Proxmox cannot do one safely. The two need different errors // because the operator's next move differs. const roomy = ctx({ floor: { cpu: 4, memory: 8192, disk: 40 } }); const d = validateResize({ disk: 60 }, roomy, { force: true }); expect(d.errors.length).toBe(1); expect(d.errors[0]).toContain('cannot shrink'); expect(d.capacityOnly).toBe(false); }); test('shrinking below the floor reports the floor too', () => { const d = validateResize({ disk: 10 }, ctx(), { force: true }); expect(d.errors.length).toBe(2); expect(d.errors.some((e) => e.includes('below the module minimum'))).toBe(true); expect(d.errors.some((e) => e.includes('cannot shrink'))).toBe(true); }); test('growth beyond node free storage is a capacity error, overridable with --force', () => { const tight = ctx({ nodeFreeDiskGb: 5 }); // 5GB free; +20GB requested const blocked = validateResize({ disk: 100 }, tight, { force: false }); expect(blocked.errors.length).toBe(1); expect(blocked.errors[0]).toContain('exceeds node free storage'); expect(blocked.capacityOnly).toBe(true); expect(validateResize({ disk: 100 }, tight, { force: true }).errors).toEqual([]); }); test('an equal-size disk request is neither shrink nor growth', () => { const d = validateResize({ disk: 80 }, ctx({ nodeFreeDiskGb: 0 })); expect(d.errors).toEqual([]); }); }); describe('computeFloor', () => { test('takes the max of each field across co-hosted modules', () => { expect( computeFloor([ { cpu: 2, memory: 4096, disk: 40 }, { cpu: 4, memory: 2048, disk: 80 }, ]), ).toEqual({ cpu: 4, memory: 4096, disk: 80 }); }); test('treats missing minimums as 0', () => { expect(computeFloor([{ memory: 8192 }, {}])).toEqual({ cpu: 0, memory: 8192, disk: 0 }); }); });