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>
This commit is contained in:
Codeman maintainer committed 2026-11-06 16:27:33 +01:00
1 parent 73ce6a1819
commit 92c20fe572
9 files changed
+3423 -79

No files matched your search

+7 -4
View File
@@ -167,7 +167,6 @@ const ALLOWED_BRANCHES: Record<string, Allowance> = {
),
"tui/tui-render.ts::mode !== 'claude'": allow(1, 'cosmetic: suppress the mode badge for the default mode'),
// Legacy <Mode>Config plumbing, same category as the `===` entries above.
"web/routes/session-routes.ts::mode !== 'omp'": allow(
2,
@@ -212,8 +211,10 @@ const ID_ALT = IDS.join('|');
*/
const BRANCH_PATTERN = new RegExp(
[
// mode === 'codex' / mode !== 'codex'
`\\b(?:mode|id|agentType)\\s*[!=]==\\s*'(?:${ID_ALT})'`,
// mode === 'codex' / mode !== 'codex'. `cli` is the name the local model catalogue gives
// the same thing (`LocalHarnessEntry.cli`, `Candidate.cli`), so `harness.cli === 'claude'`
// is the same banned branch under another field name.
`\\b(?:mode|id|agentType|cli)\\s*[!=]==\\s*'(?:${ID_ALT})'`,
// case 'codex':
`\\bcase\\s+'(?:${ID_ALT})'\\s*:`,
// ['codex', 'gemini'].includes(mode) — the id list IS the branch, wherever `mode` sits
@@ -267,7 +268,7 @@ function scan(): { findings: Finding[]; filesScanned: number } {
lines.forEach((line, i) => {
BRANCH_PATTERN.lastIndex = 0; // shared /g regex — see utils/regex-patterns.ts
for (const match of line.matchAll(BRANCH_PATTERN)) {
const expression = match[0].replace(/\s+/g, ' ').replace(/^(?:id|agentType)/, 'mode');
const expression = match[0].replace(/\s+/g, ' ').replace(/^(?:id|agentType|cli)/, 'mode');
const posix = rel.split(sep).join('/');
findings.push({ file: posix, expression, line: i + 1, key: `${posix}::${expression}` });
}
@@ -337,6 +338,8 @@ describe('no CLI-id branching outside the stock catalog', () => {
"if (mode !== 'shell' && mode !== 'deepseek') { doSomething(); }",
"switch (mode) { case 'gemini': return 1; }",
"if (['codex', 'gemini'].includes(mode)) { doSomething(); }",
"if (harness.cli === 'claude') { doSomething(); }",
"const others = candidates.filter((c) => c.cli !== 'opencode');",
];
for (const sample of samples) {
BRANCH_PATTERN.lastIndex = 0;
+383
View File
@@ -0,0 +1,383 @@
/**
* @fileoverview Tests for src/config/local-model-catalog.ts: the compiled catalogue of models,
* harnesses and chips, its schema, and the chip lookup.
*
* Three things are pinned:
*
* 1. The stock constant PARSES. `getLocalModelCatalog()` validates it on first use and throws on
* a bad row, so this test is what keeps a bad row from ever reaching the route.
* 2. The schema refuses what must never get in: a model id that could name another registry
* (a slash, a host), an unknown key (a `runtime` block with a download URL), and rows that
* break a rule across rows (a repeated id, contexts in the wrong order, a chip row that
* hides a more specific one).
* 3. The chip lookup never guesses: a chip that no row names has no bandwidth, including one
* whose name merely contains a known name ('Apple M10', 'Apple M6 Pro').
*
* Port: N/A (pure data and functions).
*/
import { describe, expect, it } from 'vitest';
import {
chipBandwidthGbps,
chipNameMatches,
getLocalModelCatalog,
LOCAL_MODEL_ID_PATTERN,
LocalModelCatalogSchema,
STOCK_LOCAL_MODEL_CATALOG,
} from '../src/config/local-model-catalog.js';
import type { LocalModelCatalog, LocalModelEntry } from '../src/types.js';
/** A deep copy of the stock catalogue that a test may break. */
function copy(): LocalModelCatalog {
return structuredClone(STOCK_LOCAL_MODEL_CATALOG);
}
/** The message of every issue the schema raises for `catalog`, or an empty list when it parses. */
function issuesOf(catalog: unknown): string[] {
const result = LocalModelCatalogSchema.safeParse(catalog);
return result.success ? [] : result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
}
/** The stock catalogue with its first model changed. */
/** `url` is no field of a model row: one test passes it to prove the schema refuses an unknown key. */
function withFirstModel(change: Partial<Record<keyof LocalModelEntry | 'url', unknown>>): unknown {
const catalog = copy();
return { ...catalog, models: [{ ...catalog.models[0], ...change }, ...catalog.models.slice(1)] };
}
describe('the stock catalogue', () => {
it('passes its own schema', () => {
expect(issuesOf(STOCK_LOCAL_MODEL_CATALOG)).toEqual([]);
});
it('is what the getter returns, validated once', () => {
const first = getLocalModelCatalog();
expect(first).toEqual(STOCK_LOCAL_MODEL_CATALOG);
// Parsed on first use, then reused: the same object, not a new parse per request.
expect(getLocalModelCatalog()).toBe(first);
});
it('lists the seven models of the design, without flint:40b', () => {
expect(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => m.id)).toEqual([
'qwen3.5:9b',
'qwen3.5:9b-q8_0',
'gpt-oss:20b',
'qwen3.6:35b-a3b',
'qwen3.8:27b',
'qwen3.8:27b-q8_0',
'qwen3.5:122b-a10b',
]);
expect(STOCK_LOCAL_MODEL_CATALOG.models.some((m) => m.id.startsWith('flint'))).toBe(false);
});
it('carries the byte sizes of the registry manifests', () => {
// The sum of the layer sizes of each tag's manifest, as fetched on 2026-11-06.
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.bytes]))).toEqual({
'qwen3.5:9b': 6_550_825_373,
'qwen3.5:9b-q8_0': 10_449_217_949,
'gpt-oss:20b': 13_793_441_427,
'qwen3.6:35b-a3b': 22_621_314_161,
'qwen3.8:27b': 17_741_871_939,
'qwen3.8:27b-q8_0': 29_978_241_837,
'qwen3.5:122b-a10b': 81_370_035_877,
});
});
it('carries the KV cost per token of each model', () => {
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.kvBytesPerToken]))).toEqual({
'qwen3.5:9b': 32_768,
'qwen3.5:9b-q8_0': 32_768,
'gpt-oss:20b': 24_576,
'qwen3.6:35b-a3b': 20_480,
'qwen3.8:27b': 65_536,
'qwen3.8:27b-q8_0': 65_536,
'qwen3.5:122b-a10b': 24_576,
});
});
it('gives every model a parameter count and an empty tested list', () => {
for (const model of STOCK_LOCAL_MODEL_CATALOG.models) {
expect(model.paramsB, model.id).toBeGreaterThan(0);
// Nobody has run any of these with a harness on a Mac yet.
expect(model.tested, model.id).toEqual([]);
expect(model.id).toMatch(LOCAL_MODEL_ID_PATTERN);
expect(model.license).toBe('Apache-2.0');
}
// The two 9B rows are the ones under the 12 billion line of the `limited` verdict.
expect(STOCK_LOCAL_MODEL_CATALOG.models.filter((m) => m.paramsB < 12).map((m) => m.id)).toEqual([
'qwen3.5:9b',
'qwen3.5:9b-q8_0',
]);
});
it('has a speed figure only where one was measured or scaled from a measurement', () => {
expect(Object.fromEntries(STOCK_LOCAL_MODEL_CATALOG.models.map((m) => [m.id, m.speedPer100Gbps ?? null]))).toEqual({
'qwen3.5:9b': 9.3,
'qwen3.5:9b-q8_0': 5.8,
'gpt-oss:20b': null,
'qwen3.6:35b-a3b': 13.6,
'qwen3.8:27b': 2.95,
'qwen3.8:27b-q8_0': 1.7,
'qwen3.5:122b-a10b': null,
});
});
it('lists its harnesses as data rows, one of them gated', () => {
expect(STOCK_LOCAL_MODEL_CATALOG.harnesses).toEqual([
{ cli: 'opencode', minContext: 65536, rank: 3 },
{ cli: 'claude', minContext: 65536, rank: 2, slowFirstTurn: true },
{ cli: 'pi', minContext: 32768, rank: 1, gated: true },
]);
});
it('counts the prompt cache as 512 MiB below 32 GB and 2 GiB from 32 GB', () => {
expect(STOCK_LOCAL_MODEL_CATALOG.promptCacheTiers).toEqual([
{ minMemGb: 0, bytes: 512 * 1024 ** 2 },
{ minMemGb: 32, bytes: 2 * 1024 ** 3 },
]);
expect(STOCK_LOCAL_MODEL_CATALOG.minMacosMajor).toBe(14);
expect(STOCK_LOCAL_MODEL_CATALOG.schema).toBe(1);
});
it('holds no download data: no URL, no digest, no runtime block', () => {
const text = JSON.stringify(STOCK_LOCAL_MODEL_CATALOG);
expect(text).not.toMatch(/https?:/);
expect(text).not.toMatch(/sha256/i);
expect(Object.keys(STOCK_LOCAL_MODEL_CATALOG).sort()).toEqual(
['chips', 'harnesses', 'minMacosMajor', 'models', 'promptCacheTiers', 'schema', 'updated'].sort()
);
});
});
describe('the model id rule', () => {
it('accepts name:tag in lowercase', () => {
for (const id of ['qwen3.5:9b', 'gpt-oss:20b', 'qwen3.8:27b-q8_0', 'a:b', 'x_y.z-1:2.3_4']) {
expect(LOCAL_MODEL_ID_PATTERN.test(id), id).toBe(true);
}
});
it('refuses anything that could name another registry, namespace or no tag at all', () => {
const bad = [
'evil.example/x/y:z',
'library/qwen3.5:9b',
'registry.ollama.ai/library/qwen3.5:9b',
'Qwen3.5:9b',
'qwen3.5:9B',
'qwen3.5',
'qwen3.5:',
':9b',
'qwen3.5:9b:extra',
'qwen 3.5:9b',
'-qwen:9b',
'qwen:-9b',
'qwen3.5:9b\n',
'',
];
for (const id of bad) expect(LOCAL_MODEL_ID_PATTERN.test(id), JSON.stringify(id)).toBe(false);
});
it('makes the schema reject a catalogue with such an id', () => {
for (const id of ['evil.example/x/y:z', 'Qwen3.5:9b', 'qwen3.5', 'qwen3.5:9b ']) {
const issues = issuesOf(withFirstModel({ id }));
expect(issues, id).toHaveLength(1);
expect(issues[0]).toContain('models.0.id');
}
});
});
describe('the schema, field by field', () => {
it('refuses a key it does not know, at the top and inside a row', () => {
// The whole point of having no `runtime` block: a data row can never carry a download URL.
expect(issuesOf({ ...copy(), runtime: { url: 'https://example.com/x.tgz', sha256: 'abc' } })).not.toEqual([]);
expect(issuesOf(withFirstModel({ url: 'https://example.com/model' }))).not.toEqual([]);
const catalog = copy();
expect(issuesOf({ ...catalog, harnesses: [{ ...catalog.harnesses[0], dialect: 'openai' }] })).not.toEqual([]);
expect(issuesOf({ ...catalog, chips: [{ match: 'M4', gbps: 120, cores: 10 }] })).not.toEqual([]);
});
it('requires paramsB and tested on every model row', () => {
for (const field of ['paramsB', 'tested'] as const) {
const row: Record<string, unknown> = { ...copy().models[0] };
delete row[field];
const issues = issuesOf({ ...copy(), models: [row] });
expect(issues, field).toHaveLength(1);
expect(issues[0]).toContain(`models.0.${field}`);
}
});
it('refuses sizes and counts that are not positive whole numbers', () => {
expect(issuesOf(withFirstModel({ bytes: 0 }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ bytes: 1.5 }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ kvBytesPerToken: -1 }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ contexts: [] }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ paramsB: 0 }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ speedPer100Gbps: 0 }))).not.toEqual([]);
});
it('accepts a tested entry and checks its shape', () => {
const good = { chip: 'Apple M4', memGb: 16, cli: 'opencode', date: '2026-11-06' };
expect(issuesOf(withFirstModel({ tested: [good] }))).toEqual([]);
expect(issuesOf(withFirstModel({ tested: [{ ...good, date: '6 November 2026' }] }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ tested: [{ ...good, memGb: 0 }] }))).not.toEqual([]);
expect(issuesOf(withFirstModel({ tested: [{ ...good, result: 'pass' }] }))).not.toEqual([]);
});
it('refuses another schema version and a date that is not ISO', () => {
expect(issuesOf({ ...copy(), schema: 2 })).not.toEqual([]);
expect(issuesOf({ ...copy(), updated: 'today' })).not.toEqual([]);
});
});
describe('the schema, across rows', () => {
it('refuses a repeated model id', () => {
const catalog = copy();
catalog.models[1] = { ...catalog.models[1], id: catalog.models[0].id };
expect(issuesOf(catalog)).toEqual(['models.1.id: model ids must be unique']);
});
it('refuses a repeated model rank, which would leave the order of two models to chance', () => {
const catalog = copy();
catalog.models[1] = { ...catalog.models[1], rank: catalog.models[0].rank };
expect(issuesOf(catalog)).toEqual(['models.1.rank: model ranks must be unique']);
});
it('refuses a harness listed twice and a repeated harness rank', () => {
const twice = copy();
twice.harnesses.push({ cli: twice.harnesses[0].cli, minContext: 32768, rank: 9 });
expect(issuesOf(twice)).toEqual(['harnesses.3.cli: a harness may be listed only once']);
const sameRank = copy();
sameRank.harnesses[1] = { ...sameRank.harnesses[1], rank: sameRank.harnesses[0].rank };
expect(issuesOf(sameRank)).toEqual(['harnesses.1.rank: harness ranks must be unique']);
});
it('refuses contexts that are not listed largest first, or that repeat', () => {
expect(issuesOf(withFirstModel({ contexts: [32768, 65536] }))).toEqual([
'models.0.contexts.1: contexts must be listed largest first, without repeats',
]);
expect(issuesOf(withFirstModel({ contexts: [65536, 65536] }))).toEqual([
'models.0.contexts.1: contexts must be listed largest first, without repeats',
]);
});
it('refuses a tested entry for a harness the catalogue does not list', () => {
const entry = { chip: 'Apple M4', memGb: 16, cli: 'not-a-harness', date: '2026-11-06' };
expect(issuesOf(withFirstModel({ tested: [entry] }))).toEqual([
'models.0.tested.0.cli: a tested entry must name a harness of this catalogue',
]);
});
it('refuses chips that are not listed most specific first', () => {
const catalog = copy();
catalog.chips = [
{ match: 'M4', gbps: 120 },
{ match: 'M4 Max', gbps: 410 },
];
expect(issuesOf(catalog)).toEqual([
"chips.1.match: chips must be listed most specific first: 'M4' comes before 'M4 Max'",
]);
const repeated = copy();
repeated.chips = [
{ match: 'M4', gbps: 120 },
{ match: 'm4', gbps: 130 },
];
expect(issuesOf(repeated)).toHaveLength(1);
});
it('refuses prompt cache tiers that do not start at 0 or do not ascend', () => {
const late = copy();
late.promptCacheTiers = [{ minMemGb: 16, bytes: 1 }];
expect(issuesOf(late)).toEqual(['promptCacheTiers.0.minMemGb: the first prompt cache tier must start at 0']);
const unordered = copy();
unordered.promptCacheTiers = [
{ minMemGb: 0, bytes: 1 },
{ minMemGb: 32, bytes: 2 },
{ minMemGb: 24, bytes: 3 },
];
expect(issuesOf(unordered)).toEqual(['promptCacheTiers.2.minMemGb: prompt cache tiers must ascend by minMemGb']);
});
});
describe('the chips table', () => {
const chips = STOCK_LOCAL_MODEL_CATALOG.chips;
it('holds every chip of the source table that shipped', () => {
// Bandwidth column of https://github.com/ggml-org/llama.cpp/discussions/4167, read on
// 2026-11-06. A name that covers two bandwidths has the lower one.
expect(Object.fromEntries(chips.map((row) => [row.match, row.gbps]))).toEqual({
'M5 Ultra': 1228,
'M3 Ultra': 800,
'M2 Ultra': 800,
'M1 Ultra': 800,
'M5 Max': 460,
'M4 Max': 410,
'M3 Max': 300,
'M2 Max': 400,
'M1 Max': 400,
'M5 Pro': 307,
'M4 Pro': 273,
'M3 Pro': 150,
'M2 Pro': 200,
'M1 Pro': 200,
M5: 154,
M4: 120,
M3: 100,
M2: 100,
M1: 68,
});
});
it('finds each row under the name macOS reports for that chip', () => {
// No row is hidden by an earlier one: 'Apple M4 Max' is 410, not the 120 of 'M4'.
for (const row of chips) {
expect(chipBandwidthGbps(chips, `Apple ${row.match}`), row.match).toBe(row.gbps);
}
});
it('knows the chips of the two test Macs', () => {
expect(chipBandwidthGbps(chips, 'Apple M4')).toBe(120);
expect(chipBandwidthGbps(chips, 'Apple M5 Pro')).toBe(307);
});
it('never guesses: a chip that no row names has no bandwidth', () => {
const unknown = [
// A base chip that is not in the table.
'Apple M6',
// A variant the table has never heard of must not borrow the figure of its base chip.
'Apple M6 Pro',
'Apple M4 Ultra',
'Apple M5 Extreme',
// A name that merely contains a known name.
'Apple M10',
'Apple M12 Pro',
'Apple M1 Max (Virtual)',
'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
'VirtualApple @ 2.50GHz processor',
'Max',
'',
];
for (const chip of unknown) expect(chipBandwidthGbps(chips, chip), chip).toBeUndefined();
expect(chipBandwidthGbps(chips, undefined)).toBeUndefined();
expect(chipBandwidthGbps([], 'Apple M4')).toBeUndefined();
});
});
describe('chipNameMatches', () => {
it('matches the whole chip name at the end of the string, ignoring case and padding', () => {
expect(chipNameMatches('Apple M4 Pro', 'M4 Pro')).toBe(true);
expect(chipNameMatches('Apple M4', 'M4')).toBe(true);
expect(chipNameMatches('M4', 'M4')).toBe(true);
expect(chipNameMatches(' apple m4 pro ', 'M4 PRO')).toBe(true);
});
it('does not match a part of a longer name', () => {
expect(chipNameMatches('Apple M4 Pro', 'M4')).toBe(false);
expect(chipNameMatches('Apple M14', 'M4')).toBe(false);
expect(chipNameMatches('Apple M10', 'M1')).toBe(false);
expect(chipNameMatches('Apple XM4', 'M4')).toBe(false);
expect(chipNameMatches('Apple M4', '')).toBe(false);
expect(chipNameMatches('', 'M4')).toBe(false);
});
});
+86
View File
@@ -0,0 +1,86 @@
/**
* @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);
}
});
});
+749
View File
@@ -0,0 +1,749 @@
/**
* @fileoverview Tests for src/local-model/hardware.ts: the probe that tells the recommendation
* what this Mac is.
*
* Two halves, mirroring the module:
*
* 1. `parseSysctl()`, pure. Fed the REAL output of the probe command on the two test Macs (a
* MacBook Pro with an M5 Pro and 48 GB, a Mac mini with an M4 and 16 GB), copied here
* character for character with the extra lines the capture holds, plus an Intel-shaped
* output (three of the seven keys do not exist there) and outputs cut off mid-line.
* 2. `readHardware()`, with every piece of IO injected: nothing is spawned and no real disk is
* read. What is pinned: a failed spawn is an ANSWER (the `os-fallback` shape), nothing is
* spawned off macOS, the result is cached for 60 s and concurrent callers share one probe.
*
* The IO itself is tested apart, against things this suite controls:
*
* 3. `spawnSysctl()` runs a small shell script of the test in place of sysctl. That proves what
* a fake cannot: the arguments are the seven keys and nothing else, the output of a program
* that exits 1 is still read (an Intel Mac lacks three keys, so sysctl exits 1 there), a
* program that hangs is given up on, and the server's environment is not handed over.
* 4. `dataDirFreeBytes()` with injected probes: a folder that does not exist yet, a mount that
* does not answer, and a `statfs` that never comes back, which must not be started twice.
*
* One test at the end runs the real defaults once (the real spawn and the real `statfs`), to
* prove they cannot reject on whatever machine runs the suite. On Linux `sysctl` knows none of
* the keys, so that run ends in the fallback; on a Mac it ends in a real answer. Both are fine.
*
* Port: N/A (no server).
*/
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { isAbsolute, join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
dataDirFreeBytes,
FREE_DISK_TIMEOUT_MS,
freeDiskBytesOf,
HARDWARE_CACHE_TTL_MS,
parseSysctl,
readHardware,
resetHardwareCache,
spawnSysctl,
SYSCTL_KEYS,
SYSCTL_PATH,
SYSCTL_TIMEOUT_MS,
type HardwareProbeDeps,
} from '../src/local-model/hardware.js';
/**
* The capture of the MacBook Pro (Mac17,13), 2026-11-06. The last four lines are not part of
* the probe: two extra keys the capture asked for, the shell's exit status and a `df -k` line.
*/
const MACBOOK_SYSCTL = `hw.memsize: 51539607552
machdep.cpu.brand_string: Apple M5 Pro
hw.optional.arm64: 1
sysctl.proc_translated: 0
iogpu.wired_limit_mb: 0
kern.osproductversion: 27.2
hw.model: Mac17,13
hw.perflevel0.physicalcpu: 6
hw.perflevel1.physicalcpu: 12
exit=0
/dev/disk3s5 971094180 54383564 890926084 6% 542146 8909260840 0% /System/Volumes/Data
`;
/** The capture of the Mac mini (Mac16,10), 2026-11-06, with the same four extra lines. */
const MACMINI_SYSCTL = `hw.memsize: 17179869184
machdep.cpu.brand_string: Apple M4
hw.optional.arm64: 1
sysctl.proc_translated: 0
iogpu.wired_limit_mb: 0
kern.osproductversion: 27.2
hw.model: Mac16,10
hw.perflevel0.physicalcpu: 4
hw.perflevel1.physicalcpu: 6
exit=0
/dev/disk3s5 239362496 172954124 41752848 81% 2353838 417528480 1% /System/Volumes/Data
`;
/** Free disk of the two Macs: the "available" column of their `df -k` lines, in bytes. */
const MACBOOK_FREE_DISK = 890_926_084 * 1024;
const MACMINI_FREE_DISK = 41_752_848 * 1024;
/**
* What an Intel Mac prints: `hw.optional.arm64`, `sysctl.proc_translated` and
* `iogpu.wired_limit_mb` do not exist there, so sysctl leaves their lines out (and exits 1).
* NOT a capture: no Intel Mac was available, the shape is recalled.
*/
const INTEL_SYSCTL = `hw.memsize: 17179869184
machdep.cpu.brand_string: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
kern.osproductversion: 13.6.7
hw.model: MacBookPro15,1
`;
/** Deps that spawn nothing and read no disk. A test overrides what it is about. */
function deps(overrides: HardwareProbeDeps = {}): HardwareProbeDeps {
return {
platform: 'darwin',
arch: 'arm64',
runSysctl: async () => MACMINI_SYSCTL,
freeDiskBytes: async () => MACMINI_FREE_DISK,
totalMemBytes: () => 8 * 1024 ** 3,
cpuModel: () => 'Fallback CPU',
now: () => 1_000_000,
...overrides,
};
}
beforeEach(() => {
resetHardwareCache();
});
afterEach(() => {
resetHardwareCache();
});
describe('the probe command', () => {
it('is an absolute path and asks for exactly the seven keys, never with -n', () => {
expect(isAbsolute(SYSCTL_PATH)).toBe(true);
expect(SYSCTL_PATH).toBe('/usr/sbin/sysctl');
expect([...SYSCTL_KEYS]).toEqual([
'hw.memsize',
'machdep.cpu.brand_string',
'hw.optional.arm64',
'sysctl.proc_translated',
'iogpu.wired_limit_mb',
'kern.osproductversion',
'hw.model',
]);
// Every argument is a key name. `-n` would print bare values, and a key missing on a
// machine would then shift every value after it onto the wrong key.
for (const key of SYSCTL_KEYS) expect(key).toMatch(/^[a-z][a-z0-9_.]*$/);
});
it('is bounded, and its result is reused for 60 seconds', () => {
expect(SYSCTL_TIMEOUT_MS).toBe(2000);
expect(HARDWARE_CACHE_TTL_MS).toBe(60_000);
});
});
describe('parseSysctl on the two real Macs', () => {
it('reads the MacBook Pro: M5 Pro, 48 GB, macOS 27.2, no wired limit set', () => {
expect(parseSysctl(MACBOOK_SYSCTL)).toEqual({
memBytes: 51_539_607_552,
chip: 'Apple M5 Pro',
arm64: true,
translated: false,
wiredLimitMb: 0,
macosVersion: '27.2',
macosMajor: 27,
model: 'Mac17,13',
});
});
it('reads the Mac mini: M4, 16 GB, macOS 27.2, no wired limit set', () => {
expect(parseSysctl(MACMINI_SYSCTL)).toEqual({
memBytes: 17_179_869_184,
chip: 'Apple M4',
arm64: true,
translated: false,
wiredLimitMb: 0,
macosVersion: '27.2',
macosMajor: 27,
model: 'Mac16,10',
});
});
it('skips the lines it does not know: other keys, the exit status, the df line', () => {
const facts = parseSysctl(MACMINI_SYSCTL);
expect(Object.keys(facts).sort()).toEqual(
['arm64', 'chip', 'macosMajor', 'macosVersion', 'memBytes', 'model', 'translated', 'wiredLimitMb'].sort()
);
});
it('reports a wired limit of 0 as 0 (not set), and a set one as its number', () => {
expect(parseSysctl(MACMINI_SYSCTL).wiredLimitMb).toBe(0);
expect(parseSysctl('iogpu.wired_limit_mb: 14000\n').wiredLimitMb).toBe(14000);
});
});
describe('parseSysctl on other shapes', () => {
it('reads an Intel Mac, whose output lacks three of the keys, without shifting a value', () => {
expect(parseSysctl(INTEL_SYSCTL)).toEqual({
memBytes: 17_179_869_184,
chip: 'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
macosVersion: '13.6.7',
macosMajor: 13,
model: 'MacBookPro15,1',
});
});
it('reads an Intel Mac that does print the arm64 flag, as 0', () => {
const facts = parseSysctl(`hw.optional.arm64: 0\n${INTEL_SYSCTL}`);
expect(facts.arm64).toBe(false);
expect(facts.translated).toBeUndefined();
});
it('reads a process under Rosetta: the machine is arm64 and the process is translated', () => {
const facts = parseSysctl(MACMINI_SYSCTL.replace('sysctl.proc_translated: 0', 'sysctl.proc_translated: 1'));
expect(facts.arm64).toBe(true);
expect(facts.translated).toBe(true);
});
it('drops a line that was cut off: a truncated output never yields half a value', () => {
// Cut in the middle of the chip name. The memory line before it is whole.
const cutInChip = 'hw.memsize: 17179869184\nmachdep.cpu.brand_string: Apple M';
expect(parseSysctl(cutInChip)).toEqual({ memBytes: 17_179_869_184 });
// Cut in the middle of the memory figure: '1717' would be a valid, wrong number.
expect(parseSysctl('hw.memsize: 1717')).toEqual({});
// Cut right after a whole line: everything up to there counts.
const cutAfterLine = MACMINI_SYSCTL.slice(0, MACMINI_SYSCTL.indexOf('hw.optional.arm64'));
expect(parseSysctl(cutAfterLine)).toEqual({ memBytes: 17_179_869_184, chip: 'Apple M4' });
});
it('reads nothing from an empty output or from error text', () => {
expect(parseSysctl('')).toEqual({});
expect(parseSysctl('\n\n')).toEqual({});
expect(
parseSysctl("sysctl: unknown oid 'hw.memsize'\nsysctl: cannot stat /proc/sys/hw/memsize: No such file\n")
).toEqual({});
});
it('leaves out a value that does not parse instead of guessing', () => {
const facts = parseSysctl(
[
'hw.memsize: lots',
'machdep.cpu.brand_string: ',
'hw.optional.arm64: yes',
'sysctl.proc_translated: 2',
'iogpu.wired_limit_mb: -1',
'kern.osproductversion: Sequoia',
`hw.model: ${'x'.repeat(200)}`,
'',
].join('\n')
);
expect(facts).toEqual({});
});
it('refuses a memory size of 0 or one too large to be a number of bytes', () => {
expect(parseSysctl('hw.memsize: 0\n').memBytes).toBeUndefined();
expect(parseSysctl('hw.memsize: 99999999999999999999999\n').memBytes).toBeUndefined();
});
it('accepts Windows line ends and padding, and the first line of a key wins', () => {
const facts = parseSysctl('hw.memsize: 17179869184 \r\nhw.model: Mac16,10\r\nhw.model: Other9,9\r\n');
expect(facts).toEqual({ memBytes: 17_179_869_184, model: 'Mac16,10' });
});
it('reads the macOS major version from one, two or three numbers', () => {
expect(parseSysctl('kern.osproductversion: 14\n')).toMatchObject({ macosVersion: '14', macosMajor: 14 });
expect(parseSysctl('kern.osproductversion: 15.7\n')).toMatchObject({ macosVersion: '15.7', macosMajor: 15 });
expect(parseSysctl('kern.osproductversion: 13.6.7\n')).toMatchObject({ macosVersion: '13.6.7', macosMajor: 13 });
});
});
describe('readHardware on the two real Macs', () => {
it('describes the MacBook Pro', async () => {
const hw = await readHardware(
deps({ runSysctl: async () => MACBOOK_SYSCTL, freeDiskBytes: async () => MACBOOK_FREE_DISK })
);
expect(hw).toEqual({
platform: 'darwin',
arch: 'arm64',
arm64: true,
translated: false,
memBytes: 51_539_607_552,
memGb: '51.5',
memClassGb: 48,
chip: 'Apple M5 Pro',
model: 'Mac17,13',
macosVersion: '27.2',
macosMajor: 27,
wiredLimitMb: 0,
freeDiskBytes: MACBOOK_FREE_DISK,
freeDiskGb: '912',
source: 'sysctl',
});
});
it('describes the Mac mini', async () => {
const hw = await readHardware(deps());
expect(hw).toEqual({
platform: 'darwin',
arch: 'arm64',
arm64: true,
translated: false,
memBytes: 17_179_869_184,
memGb: '17.2',
memClassGb: 16,
chip: 'Apple M4',
model: 'Mac16,10',
macosVersion: '27.2',
macosMajor: 27,
wiredLimitMb: 0,
freeDiskBytes: MACMINI_FREE_DISK,
freeDiskGb: '42.8',
source: 'sysctl',
});
});
it('is plain JSON: it survives a round trip unchanged', async () => {
const hw = await readHardware(deps());
expect(JSON.parse(JSON.stringify(hw))).toEqual(hw);
});
it('describes an Intel Mac from its shorter output: no arm64 flag, an x64 process', async () => {
const hw = await readHardware(deps({ arch: 'x64', runSysctl: async () => INTEL_SYSCTL }));
expect(hw.source).toBe('sysctl');
expect(hw.arch).toBe('x64');
expect(hw.chip).toBe('Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz');
expect(hw.macosMajor).toBe(13);
expect(hw).not.toHaveProperty('arm64');
expect(hw).not.toHaveProperty('translated');
expect(hw).not.toHaveProperty('wiredLimitMb');
});
it('takes the chip name from node:os when sysctl printed everything but that', async () => {
const hw = await readHardware(
deps({ runSysctl: async () => MACMINI_SYSCTL.replace('machdep.cpu.brand_string: Apple M4\n', '') })
);
expect(hw.source).toBe('sysctl');
expect(hw.chip).toBe('Fallback CPU');
});
});
describe('readHardware when the probe fails', () => {
it('answers from node:os when the spawn could not run or timed out', async () => {
const hw = await readHardware(
deps({
runSysctl: async () => undefined,
totalMemBytes: () => 34_359_738_368,
cpuModel: () => 'Apple M2 Max',
})
);
expect(hw).toEqual({
platform: 'darwin',
arch: 'arm64',
memBytes: 34_359_738_368,
memGb: '34.4',
memClassGb: 32,
chip: 'Apple M2 Max',
freeDiskBytes: MACMINI_FREE_DISK,
freeDiskGb: '42.8',
source: 'os-fallback',
});
});
it('never rejects when a dependency throws or rejects', async () => {
const hw = await readHardware(
deps({
runSysctl: () => Promise.reject(new Error('spawn EACCES')),
freeDiskBytes: () => {
throw new Error('statfs failed');
},
cpuModel: () => {
throw new Error('no cpus');
},
})
);
expect(hw.source).toBe('os-fallback');
expect(hw.memBytes).toBe(8 * 1024 ** 3);
expect(hw).not.toHaveProperty('chip');
expect(hw).not.toHaveProperty('freeDiskBytes');
expect(hw).not.toHaveProperty('freeDiskGb');
});
it('still answers when even the memory size cannot be read', async () => {
const hw = await readHardware(
deps({
runSysctl: async () => undefined,
totalMemBytes: () => {
throw new Error('no memory figure');
},
})
);
expect(hw).toMatchObject({ source: 'os-fallback', memBytes: 0, memGb: '0.0', memClassGb: 0 });
});
it('trusts nothing of an output that has no memory size', async () => {
// A truncated or foreign output: the chip and version lines are whole, hw.memsize is not there.
const withoutMemory = MACMINI_SYSCTL.replace('hw.memsize: 17179869184\n', '');
const hw = await readHardware(deps({ runSysctl: async () => withoutMemory }));
expect(hw.source).toBe('os-fallback');
expect(hw.memBytes).toBe(8 * 1024 ** 3);
expect(hw.chip).toBe('Fallback CPU');
expect(hw).not.toHaveProperty('macosVersion');
expect(hw).not.toHaveProperty('macosMajor');
expect(hw).not.toHaveProperty('model');
expect(hw).not.toHaveProperty('wiredLimitMb');
expect(hw).not.toHaveProperty('arm64');
});
it('falls back when the output was cut off inside the memory figure', async () => {
const hw = await readHardware(deps({ runSysctl: async () => 'hw.memsize: 1717' }));
expect(hw.source).toBe('os-fallback');
expect(hw.memBytes).toBe(8 * 1024 ** 3);
});
it('leaves the free disk out when it is unknown, and keeps the rest', async () => {
const hw = await readHardware(deps({ freeDiskBytes: async () => undefined }));
expect(hw.source).toBe('sysctl');
expect(hw).not.toHaveProperty('freeDiskBytes');
expect(hw).not.toHaveProperty('freeDiskGb');
});
it('reports a full disk as 0 bytes free, which is a figure and not "unknown"', async () => {
const hw = await readHardware(deps({ freeDiskBytes: async () => 0 }));
expect(hw.freeDiskBytes).toBe(0);
expect(hw.freeDiskGb).toBe('0.0');
});
});
describe('readHardware off macOS', () => {
it('spawns nothing and reads no disk', async () => {
let spawned = 0;
let diskRead = 0;
const hw = await readHardware(
deps({
platform: 'linux',
arch: 'x64',
runSysctl: async () => {
spawned++;
return MACMINI_SYSCTL;
},
freeDiskBytes: async () => {
diskRead++;
return 1;
},
totalMemBytes: () => 67_430_023_168,
})
);
expect(spawned).toBe(0);
expect(diskRead).toBe(0);
expect(hw).toEqual({
platform: 'linux',
arch: 'x64',
memBytes: 67_430_023_168,
memGb: '67.4',
memClassGb: 63,
source: 'not-probed',
});
});
});
describe('the cache of readHardware', () => {
it('reuses one result for 60 seconds, then probes again', async () => {
let clock = 5_000_000;
let spawned = 0;
const counting = deps({
now: () => clock,
runSysctl: async () => {
spawned++;
return spawned === 1 ? MACMINI_SYSCTL : MACBOOK_SYSCTL;
},
});
const first = await readHardware(counting);
clock += HARDWARE_CACHE_TTL_MS - 1;
const second = await readHardware(counting);
expect(spawned).toBe(1);
expect(second).toBe(first);
clock += 1;
const third = await readHardware(counting);
expect(spawned).toBe(2);
expect(third.chip).toBe('Apple M5 Pro');
});
it('caches a failed probe too: a broken sysctl is not spawned on every request', async () => {
let spawned = 0;
const failing = deps({
runSysctl: async () => {
spawned++;
return undefined;
},
});
expect((await readHardware(failing)).source).toBe('os-fallback');
expect((await readHardware(failing)).source).toBe('os-fallback');
expect(spawned).toBe(1);
});
it('lets concurrent callers share one probe', async () => {
let spawned = 0;
let release: (text: string) => void = () => {};
const slow = deps({
runSysctl: () => {
spawned++;
return new Promise<string>((resolve) => {
release = resolve;
});
},
});
const callers = [readHardware(slow), readHardware(slow), readHardware(slow)];
// Let the three calls reach the spawn before it answers.
await new Promise((resolve) => setImmediate(resolve));
release(MACMINI_SYSCTL);
const results = await Promise.all(callers);
expect(spawned).toBe(1);
expect(results[1]).toBe(results[0]);
expect(results[2]).toBe(results[0]);
});
it('probes again after a reset, and does not trust a clock that ran backwards', async () => {
let clock = 9_000_000;
let spawned = 0;
const counting = deps({
now: () => clock,
runSysctl: async () => {
spawned++;
return MACMINI_SYSCTL;
},
});
await readHardware(counting);
resetHardwareCache();
await readHardware(counting);
expect(spawned).toBe(2);
clock -= 10_000;
await readHardware(counting);
expect(spawned).toBe(3);
});
});
// The scripts below are POSIX shell. Codeman's server does not run on Windows.
describe.skipIf(process.platform === 'win32')('spawnSysctl, against a script of the test', () => {
let folder: string;
beforeEach(() => {
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-spawn-'));
});
afterEach(() => {
rmSync(folder, { recursive: true, force: true });
});
/** An executable shell script standing in for sysctl. */
function script(body: string, mode = 0o755): string {
const path = join(folder, 'fake-sysctl');
writeFileSync(path, `#!/bin/sh\n${body}\n`, { mode });
return path;
}
it('passes the seven keys as arguments and nothing else: no -n, no shell', async () => {
// printf, not echo: echo would swallow a `-n` argument, the very thing this test looks for.
const output = await spawnSysctl(script('printf \'%s\\n\' "$@"'));
expect(output?.trimEnd().split('\n')).toEqual([...SYSCTL_KEYS]);
});
it('reads the output of a program that exits 1, as sysctl does on an Intel Mac', async () => {
// Three keys are unknown there: their errors go to stderr, the other four lines to stdout.
const output = await spawnSysctl(
script(
[
"echo 'hw.memsize: 17179869184'",
"echo 'machdep.cpu.brand_string: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz'",
`echo "sysctl: unknown oid 'hw.optional.arm64'" >&2`,
`echo "sysctl: unknown oid 'sysctl.proc_translated'" >&2`,
`echo "sysctl: unknown oid 'iogpu.wired_limit_mb'" >&2`,
"echo 'kern.osproductversion: 13.6.7'",
"echo 'hw.model: MacBookPro15,1'",
'exit 1',
].join('\n')
)
);
expect(output).toBeDefined();
expect(output).not.toContain('unknown oid');
expect(parseSysctl(output as string)).toEqual({
memBytes: 17_179_869_184,
chip: 'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
macosVersion: '13.6.7',
macosMajor: 13,
model: 'MacBookPro15,1',
});
});
it('answers undefined for a program that does not exist or may not be run', async () => {
expect(await spawnSysctl(join(folder, 'no-such-program'))).toBeUndefined();
expect(await spawnSysctl(script('echo "hw.memsize: 1"', 0o644))).toBeUndefined();
});
it('gives up on a program that hangs, and does not hand over half an output', async () => {
const started = Date.now();
// `exec` so the signal reaches the sleeper itself and nothing is left behind.
const output = await spawnSysctl(script("echo 'hw.memsize: 17179869184'\nexec sleep 20"), 200);
expect(output).toBeUndefined();
expect(Date.now() - started).toBeLessThan(5000);
});
it('answers undefined for an output far larger than seven lines', async () => {
expect(await spawnSysctl(script("head -c 200000 /dev/zero | tr '\\0' 'x'"))).toBeUndefined();
});
it("does not pass the server's environment to the program", async () => {
const name = 'CODEMAN_LOCAL_MODEL_TEST_SECRET';
process.env[name] = 'hunter2';
try {
const output = await spawnSysctl(script(`echo "secret=\${${name}:-unset}"`));
expect(output?.trim()).toBe('secret=unset');
} finally {
delete process.env[name];
}
});
});
describe('dataDirFreeBytes', () => {
let folder: string;
beforeEach(() => {
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-disk-'));
});
afterEach(() => {
rmSync(folder, { recursive: true, force: true });
});
it('reads the free bytes of the data dir with the real probe and the real statfs', async () => {
const free = await dataDirFreeBytes({ dataDir: () => folder });
expect(typeof free).toBe('number');
expect(free).toBeGreaterThanOrEqual(0);
});
it('uses the nearest existing parent when the data dir does not exist yet', async () => {
const asked: string[] = [];
const free = await dataDirFreeBytes({
dataDir: () => join(folder, 'not', 'yet', 'there'),
freeBytesOf: async (path) => {
asked.push(path);
return 123;
},
});
expect(free).toBe(123);
expect(asked).toEqual([folder]);
});
it('does not touch a folder whose mount does not answer, or a path that is a file', async () => {
for (const kind of ['unknown', 'file'] as const) {
let touched = 0;
const free = await dataDirFreeBytes({
dataDir: () => folder,
probeKind: async () => kind,
freeBytesOf: async () => {
touched++;
return 1;
},
});
expect(free, kind).toBeUndefined();
expect(touched, kind).toBe(0);
}
});
it('stops walking up after a few parents', async () => {
let probes = 0;
const free = await dataDirFreeBytes({
dataDir: () => '/a/b/c/d/e/f/g/h/i/j/k/l',
probeKind: async () => {
probes++;
return 'absent';
},
freeBytesOf: async () => 1,
});
expect(free).toBeUndefined();
expect(probes).toBe(7);
});
it('gives up on a statfs that does not come back, and never starts a second one meanwhile', async () => {
let started = 0;
let finish: (bytes: number) => void = () => {};
const hanging = {
dataDir: () => folder,
freeBytesOf: () => {
started++;
return new Promise<number>((resolve) => {
finish = resolve;
});
},
timeoutMs: 20,
};
// The first read waits its timeout and answers "unknown".
expect(await dataDirFreeBytes(hanging)).toBeUndefined();
expect(started).toBe(1);
// While that statfs is still out, later reads answer at once and start nothing.
const before = Date.now();
expect(await dataDirFreeBytes(hanging)).toBeUndefined();
expect(await dataDirFreeBytes({ ...hanging, timeoutMs: 60_000 })).toBeUndefined();
expect(Date.now() - before).toBeLessThan(1000);
expect(started).toBe(1);
// Once it finally answers, the next read works again.
finish(5);
await new Promise((resolve) => setImmediate(resolve));
expect(await dataDirFreeBytes({ ...hanging, freeBytesOf: async () => 42 })).toBe(42);
});
it('never rejects when one of its reads does', async () => {
const failing = async (): Promise<never> => {
throw new Error('EIO');
};
expect(await dataDirFreeBytes({ dataDir: () => folder, freeBytesOf: failing })).toBeUndefined();
expect(await dataDirFreeBytes({ dataDir: () => folder, probeKind: failing })).toBeUndefined();
expect(
await dataDirFreeBytes({
dataDir: () => {
throw new Error('no home');
},
})
).toBeUndefined();
});
it('is bounded by default', () => {
expect(FREE_DISK_TIMEOUT_MS).toBe(2000);
});
});
describe('the real defaults', () => {
let folder: string;
beforeEach(() => {
folder = mkdtempSync(join(tmpdir(), 'codeman-local-model-hw-'));
});
afterEach(() => {
rmSync(folder, { recursive: true, force: true });
});
it('reads the free bytes of a real folder, and undefined for one that does not exist', async () => {
const free = await freeDiskBytesOf(folder);
expect(typeof free).toBe('number');
expect(free).toBeGreaterThanOrEqual(0);
expect(await freeDiskBytesOf(join(folder, 'missing', 'deeper'))).toBeUndefined();
});
it('never rejects with the real spawn and the real disk read, whatever machine this is', async () => {
// Pretend to be a Mac so the spawn and the disk read really run. On Linux /usr/sbin/sysctl
// either does not exist or knows none of the keys: the answer is then the fallback.
const hw = await readHardware({ platform: 'darwin' });
expect(['sysctl', 'os-fallback']).toContain(hw.source);
expect(hw.memBytes).toBeGreaterThan(0);
expect(hw.memGb).toMatch(/^\d+(\.\d)?$/);
if (hw.freeDiskBytes !== undefined) {
expect(hw.freeDiskBytes).toBeGreaterThanOrEqual(0);
expect(hw.freeDiskGb).toMatch(/^\d+(\.\d)?$/);
}
});
});
File diff suppressed because it is too large. Load diff