/** * The host-VM policy, with each rule broken deliberately. * * Per Rule 7.6 a gate nobody has seen fail is not a gate, so every assertion * here is written against the out-of-policy state. The happy cases exist only * to prove the rules do not fire on a VM that is fine — a check that warns * about everything teaches people to ignore it, which is the same outcome as * having no check. */ import { describe, expect, test } from 'bun:test'; import { type HostFacts, type HostVmFacts, evaluateHostVm, parseLimaConfig, parseLimaMemory, readHostVmFacts, recommendedBudget, } from './host-vm'; const M1_PRO: HostFacts = { cpus: 10, memoryGiB: 32 }; function vm(overrides: Partial = {}): HostVmFacts { return { profile: 'default', cpus: 8, memoryGiB: 12, mountType: 'virtiofs', vmType: 'vz', ...overrides, }; } describe('recommendedBudget', () => { test('leaves the host two cores and about a third of its memory', () => { expect(recommendedBudget(M1_PRO, 'vz')).toEqual({ cpus: 8, memoryGiB: 12, mountType: 'virtiofs', }); }); test('scales to a small laptop rather than encoding one machine', () => { expect(recommendedBudget({ cpus: 4, memoryGiB: 16 }, 'vz')).toEqual({ cpus: 2, memoryGiB: 6, mountType: 'virtiofs', }); }); test('caps memory on a large host — the guest has no use for 48 GiB of page cache', () => { expect(recommendedBudget({ cpus: 32, memoryGiB: 128 }, 'vz').memoryGiB).toBe(16); }); test('never proposes fewer than two CPUs', () => { expect(recommendedBudget({ cpus: 2, memoryGiB: 8 }, 'vz').cpus).toBe(2); }); test('qemu cannot have virtiofs, so it is not asked for it', () => { expect(recommendedBudget(M1_PRO, 'qemu').mountType).toBe('sshfs'); }); }); describe('parseLimaMemory', () => { test('reads the form lima actually writes', () => { expect(parseLimaMemory('12288MiB')).toBe(12); }); test('reads GiB and raw byte counts too', () => { expect(parseLimaMemory('24GiB')).toBe(24); expect(parseLimaMemory(12 * 1024 ** 3)).toBe(12); }); test('an unreadable value is 0, not a guess', () => { expect(parseLimaMemory('lots')).toBe(0); expect(parseLimaMemory(undefined)).toBe(0); }); }); describe('evaluateHostVm', () => { const budget = recommendedBudget(M1_PRO, 'vz'); test('a VM inside the policy has nothing to say', () => { expect(evaluateHostVm(vm(), M1_PRO, budget)).toEqual({ problems: [], needsRecreate: false }); }); test('24 of 32 GiB is flagged — the measured state that had the host swapping 21.9 GiB', () => { const verdict = evaluateHostVm(vm({ memoryGiB: 24 }), M1_PRO, budget); expect(verdict.problems).toHaveLength(1); expect(verdict.problems[0]).toContain('24 GiB'); // Memory takes on a restart, so this must not demand the destructive path. expect(verdict.needsRecreate).toBe(false); }); test('exactly half the host is allowed — the line is above it, not at it', () => { expect(evaluateHostVm(vm({ memoryGiB: 16 }), M1_PRO, budget).problems).toEqual([]); }); test('sshfs is flagged AND demands a recreate, because colima discards the change', () => { const verdict = evaluateHostVm(vm({ mountType: 'sshfs' }), M1_PRO, budget); expect(verdict.problems[0]).toContain('sshfs'); expect(verdict.needsRecreate).toBe(true); }); test('an empty mount type reads as unknown rather than as a missing sentence', () => { expect(evaluateHostVm(vm({ mountType: '' }), M1_PRO, budget).problems[0]).toContain( 'an unknown transport', ); }); test('overcommitted CPUs are flagged', () => { expect(evaluateHostVm(vm({ cpus: 16 }), M1_PRO, budget).problems[0]).toContain('16 CPUs'); }); test('half the policy CPUs is flagged — the measured case that read as a match', () => { const verdict = evaluateHostVm(vm({ cpus: 4 }), M1_PRO, budget); expect(verdict.problems).toHaveLength(1); expect(verdict.problems[0]).toContain('4 CPUs'); // colima resizes CPUs on a restart, so this must not demand the destructive path. expect(verdict.needsRecreate).toBe(false); }); test('memory below the policy is flagged', () => { const verdict = evaluateHostVm(vm({ memoryGiB: 4 }), M1_PRO, budget); expect(verdict.problems).toHaveLength(1); expect(verdict.problems[0]).toContain('4 GiB'); expect(verdict.needsRecreate).toBe(false); }); /** * The property that actually failed in celilo#1395: `budget.cpus` was * computed, printed inside another rule's message, and never compared. A * per-rule test cannot express that — it can only break rules that exist. * So drive every budget field away from the budget and require a sentence * back. A field used only in a template string produces none. */ test('every field of the budget is compared, not merely printed', () => { const diverge: Record> = { cpus: { cpus: budget.cpus - 1 }, memoryGiB: { memoryGiB: budget.memoryGiB - 1 }, mountType: { mountType: 'sshfs' }, }; for (const [field, override] of Object.entries(diverge)) { expect( evaluateHostVm(vm(override), M1_PRO, budget).problems, `budget.${field} diverged but no rule fired`, ).not.toEqual([]); } }); test('several problems are all reported, not just the first', () => { const verdict = evaluateHostVm(vm({ memoryGiB: 24, mountType: 'sshfs', cpus: 16 }), M1_PRO, { ...budget, }); expect(verdict.problems).toHaveLength(3); }); }); describe('parseLimaConfig', () => { test("normalizes lima's reverse-sshfs to the name colima's own flag uses", () => { // Reading lima's config rather than colima's saved profile is load-bearing: // colima accepts `--mount-type` on an existing VM, warns that it discarded // it, and rewrites its own profile — so the profile records the request and // lima records what happened. The two disagreed while this was written. expect( parseLimaConfig('vmType: vz\ncpus: 8\nmemory: 12288MiB\nmountType: reverse-sshfs\n', 'probe'), ).toEqual({ profile: 'probe', cpus: 8, memoryGiB: 12, mountType: 'sshfs', vmType: 'vz', }); }); test('missing fields read as absent rather than as a plausible default', () => { expect(parseLimaConfig('vmType: vz\n', 'probe')).toEqual({ profile: 'probe', cpus: 0, memoryGiB: 0, mountType: '', vmType: 'vz', }); }); }); describe('readHostVmFacts', () => { test('no lima instance means no VM to shape, not an error', () => { expect(readHostVmFacts('a-profile-that-does-not-exist')).toBeNull(); }); });