Files
Codeman/test/local-model-format.test.ts
T
Codeman maintainer 92c20fe572 feat(local-model): hardware probe, model catalogue and the pure recommendation
The core of the read-only recommendation: what this Mac is, what the catalogue
knows, and which model fits. Nothing is downloaded, installed or written.

- hardware.ts: one bounded async spawn of /usr/sbin/sysctl with seven keys,
  without -n, parsed as "key: value" lines by the pure parseSysctl(). Free disk
  of the data dir through statfs behind the bounded path probe. Fallbacks from
  node:os, a 60 s cache, never throws, never system_profiler. A wired limit of
  0 means "not set".
- local-model-catalog.ts: seven models, three harness rows (pi gated) and the
  chips table as a compiled constant, validated by a Zod schema on first use.
  No override file. Model ids are name:tag in lowercase without a slash; every
  model carries paramsB and an (empty) tested list. Chip names match on the
  end of the chip string, so 'M4' is not the chip of 'Apple M4 Pro'.
- recommend.ts: device budget (the wired limit when above 0, else 2/3 of
  memory below 36 GB and 3/4 from 36 GB, less the reserve), need = weights +
  KV cache + 1 GiB + prompt cache, fit good up to 0.85 and tight up to 0.98,
  speed ok, unknown or slow (a chip in no chips row is unknown), and the
  ranking. Verdicts ok, limited (best model under 12 billion parameters) and
  none with its reason. "Recommended" only for a pick with a tested entry for
  this Mac, which no row has today.
- types/local-model.ts: comments brought in line with the code (chip matching,
  budgetShare rounding, a budget of 0 on an Intel Mac).
- cli-registry-no-id-branching.test.ts: the guard also catches a comparison of
  a `cli` field with a CLI id (plus one stray blank line Prettier refused).

Tests: test/local-model-format.test.ts, test/local-model-hardware.test.ts
(the captured output of two real Macs), test/local-model-catalog.test.ts,
test/local-model-recommend.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 16:27:33 +01:00

87 lines
3.2 KiB
TypeScript

/**
* @fileoverview Tests for src/local-model/format.ts: the ONE place a byte count becomes a size a
* person reads, and the memory class a Mac is sold as.
*
* What is pinned here is a contract with the browser: it prints the text it was sent and adds
* ' GB' itself, so the text must be decimal GB, carry no unit, and round the same way everywhere.
*
* Port: N/A (pure functions).
*/
import { describe, expect, it } from 'vitest';
import { formatGb, GB, GIB, memClassGb, MIB } from '../src/local-model/format.js';
describe('byte units', () => {
it('keeps decimal GB (what a person reads) apart from the binary units of the budget math', () => {
expect(GB).toBe(1_000_000_000);
expect(GIB).toBe(1_073_741_824);
expect(MIB).toBe(1_048_576);
});
});
describe('formatGb', () => {
it('prints decimal GB, so a Mac sold as 16 GB reads 17.2 and one sold as 48 GB reads 51.5', () => {
expect(formatGb(17_179_869_184)).toBe('17.2');
expect(formatGb(51_539_607_552)).toBe('51.5');
});
it('prints the registry sizes of the catalogue models as the registry shows them', () => {
expect(formatGb(6_550_825_373)).toBe('6.6');
expect(formatGb(13_793_441_427)).toBe('13.8');
expect(formatGb(17_741_871_939)).toBe('17.7');
expect(formatGb(22_621_314_161)).toBe('22.6');
expect(formatGb(81_370_035_877)).toBe('81.4');
});
it('keeps one decimal below 100 and none from 100', () => {
expect(formatGb(1 * GB)).toBe('1.0');
expect(formatGb(99.94 * GB)).toBe('99.9');
// Rounds to 100.0, which is already "from 100".
expect(formatGb(99.96 * GB)).toBe('100');
expect(formatGb(100 * GB)).toBe('100');
// The free disk of the two test Macs, from their `df -k` lines.
expect(formatGb(890_926_084 * 1024)).toBe('912');
expect(formatGb(41_752_848 * 1024)).toBe('42.8');
});
it('never prints a unit, a thousands separator or an exponent', () => {
for (const bytes of [1, 5 * MIB, 3 * GIB, 250 * GB, 4000 * GB, 2.5e15]) {
expect(formatGb(bytes)).toMatch(/^\d+(\.\d)?$/);
}
});
it("reads '0.0' for anything that is not a positive size", () => {
for (const bytes of [0, -1, -5 * GB, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) {
expect(formatGb(bytes)).toBe('0.0');
}
});
it('shows a size too small for one decimal as 0.0, never as an empty text', () => {
expect(formatGb(10 * MIB)).toBe('0.0');
expect(formatGb(512 * MIB)).toBe('0.5');
});
});
describe('memClassGb', () => {
it('names the size a Mac is sold as: whole binary gigabytes', () => {
for (const sold of [8, 16, 18, 24, 32, 36, 48, 64, 96, 128, 192]) {
expect(memClassGb(sold * GIB)).toBe(sold);
}
// The exact `hw.memsize` of the two test Macs.
expect(memClassGb(17_179_869_184)).toBe(16);
expect(memClassGb(51_539_607_552)).toBe(48);
});
it('rounds a size that is not a whole number of gigabytes', () => {
// `hw.memsize_usable` of the 16 GB test Mac is a little below 16 GiB.
expect(memClassGb(16_631_808_000)).toBe(15);
expect(memClassGb(16 * GIB - 100 * MIB)).toBe(16);
});
it('reads 0 for anything that is not a positive size', () => {
for (const bytes of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
expect(memClassGb(bytes)).toBe(0);
}
});
});