85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
155 lines
5.6 KiB
JavaScript
155 lines
5.6 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import { test } from "node:test";
|
|
import {
|
|
listModels,
|
|
meetsSpecs,
|
|
selectModel,
|
|
describeModelLadder,
|
|
CAPABILITIES,
|
|
} from "./local-models.mjs";
|
|
|
|
const TIERS = ["small", "medium", "large", "xlarge"];
|
|
|
|
const strongGpu = {
|
|
ramMB: 64000,
|
|
gpu: { present: true, kind: "nvidia", vramMB: 24000 },
|
|
appleSilicon: false,
|
|
};
|
|
const cpuOnly = { ramMB: 16000, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
|
|
const tiny = { ramMB: 1024, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
|
|
|
|
test("every capability table is non-empty and well-formed", () => {
|
|
for (const cap of CAPABILITIES) {
|
|
const models = listModels(cap);
|
|
assert.ok(models.length > 0, `no models for ${cap}`);
|
|
for (const m of models) {
|
|
assert.ok(m.id && m.tier && m.needs, `${cap}/${m.id} missing fields`);
|
|
assert.ok(TIERS.includes(m.tier), `${cap}/${m.id} bad tier: ${m.tier}`);
|
|
assert.equal(typeof m.install, "string", `${cap}/${m.id} needs an install command`);
|
|
assert.equal(typeof m.invoke, "string", `${cap}/${m.id} needs an invoke command`);
|
|
// user-installed, local-use-only: there is NO license gate on selection
|
|
assert.equal("license" in m, false, `${cap}/${m.id} must not carry a license gate`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("meetsSpecs enforces RAM, GPU presence, and VRAM", () => {
|
|
const gpuModel = { needs: { ramMB: 8000, gpu: true, vramMB: 12000 } };
|
|
assert.equal(meetsSpecs(gpuModel, strongGpu), true);
|
|
assert.equal(meetsSpecs(gpuModel, cpuOnly), false, "no GPU -> fails a GPU model");
|
|
const cpuModel = { needs: { ramMB: 2000, gpu: false } };
|
|
assert.equal(meetsSpecs(cpuModel, cpuOnly), true);
|
|
assert.equal(meetsSpecs(cpuModel, tiny), false, "too little RAM");
|
|
});
|
|
|
|
test("Apple Silicon unified memory counts as VRAM", () => {
|
|
const apple = {
|
|
ramMB: 24000,
|
|
appleSilicon: true,
|
|
gpu: { present: true, kind: "apple", vramMB: 24000 },
|
|
};
|
|
const gpuModel = { needs: { ramMB: 8000, gpu: true, vramMB: 16000 } };
|
|
assert.equal(meetsSpecs(gpuModel, apple), true);
|
|
});
|
|
|
|
test("selectModel picks the large tier on a strong machine", () => {
|
|
const r = selectModel("tts", strongGpu);
|
|
assert.equal(r.tier, "large");
|
|
assert.ok(r.model.id);
|
|
});
|
|
|
|
test("selectModel falls back to medium on a CPU-only machine", () => {
|
|
const r = selectModel("tts", cpuOnly);
|
|
assert.equal(r.tier, "medium");
|
|
assert.equal(r.model.id, "kokoro", "Kokoro is the CPU/medium default (native word timestamps)");
|
|
});
|
|
|
|
test("selectModel recommends the CLI path when no tier fits", () => {
|
|
const r = selectModel("tts", tiny);
|
|
assert.equal(r.recommend, "cli");
|
|
assert.ok(r.reason && /spec/i.test(r.reason));
|
|
assert.equal(r.model, undefined);
|
|
});
|
|
|
|
test("preferTier:'medium' avoids the large model even on a strong machine", () => {
|
|
const r = selectModel("tts", strongGpu, { preferTier: "medium" });
|
|
assert.equal(r.tier, "medium");
|
|
});
|
|
|
|
test("selectModel gates on AVAILABLE RAM, not total, when both are present", () => {
|
|
// 64GB total but only 6GB free right now -> the large tier must not be chosen.
|
|
const busy = {
|
|
ramMB: 64000,
|
|
availableRamMB: 6000,
|
|
appleSilicon: true,
|
|
gpu: { present: true, kind: "apple", vramMB: 64000 },
|
|
};
|
|
const r = selectModel("tts", busy);
|
|
assert.equal(r.tier, "medium", "available RAM (6GB) rules out the 16GB large tier");
|
|
});
|
|
|
|
test("imagegen is a RAM-graduated ladder; agent picks the best that fits", () => {
|
|
const ladder = describeModelLadder("imagegen", {
|
|
ramMB: 24000,
|
|
availableRamMB: 12000,
|
|
appleSilicon: true,
|
|
gpu: { present: true, kind: "apple", vramMB: 24000 },
|
|
});
|
|
// best-first order, each flagged with fit
|
|
assert.ok(ladder.length >= 3, "imagegen offers multiple RAM tiers");
|
|
assert.ok(
|
|
ladder[0].needsRamMB >= ladder[ladder.length - 1].needsRamMB,
|
|
"ladder is ordered best (biggest) first",
|
|
);
|
|
// on 24GB / 12GB-free the schnell --low-ram tier fits, the 32GB+ tiers do not
|
|
const fitting = ladder.filter((m) => m.fits);
|
|
assert.ok(fitting.length >= 1, "at least the low-ram tier fits a 24GB Mac");
|
|
assert.ok(
|
|
fitting.every((m) => m.needsRamMB <= 12000),
|
|
"only sub-budget models flagged as fitting",
|
|
);
|
|
const pick = selectModel("imagegen", {
|
|
ramMB: 24000,
|
|
availableRamMB: 12000,
|
|
gpu: { present: true, vramMB: 24000 },
|
|
});
|
|
assert.equal(
|
|
pick.model.id,
|
|
"flux-schnell-mflux-q4",
|
|
"best fit on 24GB is the low-ram schnell tier",
|
|
);
|
|
});
|
|
|
|
test("imagegen on a 64GB Mac steps up to the higher-quality tier", () => {
|
|
const pick = selectModel("imagegen", {
|
|
ramMB: 96000,
|
|
availableRamMB: 80000,
|
|
gpu: { present: true, vramMB: 96000 },
|
|
});
|
|
assert.equal(pick.tier, "xlarge", "80GB free unlocks the top-quality model");
|
|
});
|
|
|
|
test("ASR prefers Parakeet by rank even though it is smaller than whisper", () => {
|
|
// quality != size for ASR: Parakeet 0.6B beats whisper-1.5B, so `rank` wins
|
|
// over footprint. On a capable machine both fit; Parakeet must be chosen.
|
|
const capable = {
|
|
ramMB: 24000,
|
|
availableRamMB: 12000,
|
|
appleSilicon: true,
|
|
gpu: { present: true, kind: "apple", vramMB: 24000 },
|
|
};
|
|
const pick = selectModel("asr", capable);
|
|
assert.equal(pick.model.id, "parakeet-mlx", "Parakeet is the rank-0 preferred ASR");
|
|
// whisperx (rank 1, CPU-only) is the fallback when no GPU
|
|
const cpu = { ramMB: 16000, availableRamMB: 12000, gpu: { present: false, vramMB: 0 } };
|
|
assert.equal(selectModel("asr", cpu).model.id, "whisperx", "CPU-only falls back to whisperx");
|
|
});
|
|
|
|
test("ASR offers word-timestamp-capable models (better than plain whisper)", () => {
|
|
const asr = listModels("asr");
|
|
assert.ok(
|
|
asr.every((m) => m.wordTimestamps),
|
|
"every ASR model must support word timestamps",
|
|
);
|
|
});
|