Replace the @huggingface/transformers local embedder (~316MB ONNX cache) with an EmbeddingPort seam: graph.embeddingModel (nullable "provider:id" slug) selects an OpenRouter/OpenAI/Gemini adapter, resolved fresh per call from settings + the auth store so key rotations land immediately. - src/server/embeddings.ts rewritten: slug grammar, injected-fetch adapters (OpenRouter input_type, Gemini taskType batching, OpenAI index-ordered rows), honest resolution (null = skip-with-info, set model with missing key = honest error), embedNewNodes with model-change re-embed wipe + ghost pruning + usage steps, federated catalog (OpenRouter /api/v1/embeddings/models — VERIFIED live, the inverse path 404s; OpenAI text-embedding- filter; Gemini embedContent filter). - GET /api/models/embeddings: federated catalog, empty when no keys. - Extraction tail records stages.embedding usage into the run's money channel; EMBEDDING_PRICES overlay in core/prices.ts (outside the sync-generated block), callCost falls back to it; cost-summary folds the fourth stage; graph-context injection resolves its embedder per run, fails honestly when unconfigured, records query tokens. - Settings: GraphSettings.embeddingModel (null factory), section parser validates slug shape (id may contain colons — OpenRouter :free suffixes, live catalog fact). - Removal: @huggingface/transformers uninstalled, PATHS.modelCache and onnx tracing entries deleted, @huggingface tracing-excludes guard added, stale sharp/libvips comments updated to historical. Live-verified on port 3004: catalog federates 36 rows (33 OpenRouter + 3 OpenAI), embedNewNodes embedded 10 nodes via the free liquid/lfm-2.5 model (dim 1024, 435 prompt tokens, 1 call, $0), settings PUT rejects bad slugs with 400. Gates: typecheck, lint, 895/895 tests, build.
236 lines
9.9 KiB
TypeScript
236 lines
9.9 KiB
TypeScript
/**
|
|
* Tests for the section parsers (settings-storage wave, 2026-09-19):
|
|
* src/server/settings-body.ts — general/agent/appearance/graph strictness.
|
|
* Same discipline as parseSettingsBody (its tests live in
|
|
* default-settings.test.ts): every known field is strict (bad type/shape
|
|
* → 400-style error, never a silent clamp), unknown keys drop, absent
|
|
* fields mean absent overrides. Pure — no fs.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import {
|
|
parseAgentSettingsBody,
|
|
parseModelsSettingsBody,
|
|
parseAppearanceSettingsBody,
|
|
parseGeneralSettingsBody,
|
|
parseGraphSettingsBody,
|
|
SETTINGS_SECTIONS,
|
|
} from "../src/server/settings-body.ts";
|
|
|
|
describe("parseGeneralSettingsBody", () => {
|
|
it("parses {} and {autoUpdate} replace-whole", () => {
|
|
const empty = parseGeneralSettingsBody({});
|
|
assert.ok(empty.ok);
|
|
assert.deepEqual(empty.settings, {});
|
|
const on = parseGeneralSettingsBody({ autoUpdate: true });
|
|
assert.ok(on.ok);
|
|
assert.deepEqual(on.settings, { autoUpdate: true });
|
|
});
|
|
|
|
it("rejects non-objects and non-boolean autoUpdate", () => {
|
|
assert.equal(parseGeneralSettingsBody("x").ok, false);
|
|
assert.equal(parseGeneralSettingsBody({ autoUpdate: "yes" }).ok, false);
|
|
});
|
|
});
|
|
|
|
describe("parseAgentSettingsBody", () => {
|
|
it("models: valid roles with model pair and/or thinkingLevel; unknown role is a 400 with the list", () => {
|
|
const r = parseAgentSettingsBody({
|
|
models: {
|
|
researcher: { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } },
|
|
labeler: { thinkingLevel: "high" },
|
|
},
|
|
});
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings.models, {
|
|
researcher: { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" } },
|
|
labeler: { thinkingLevel: "high" },
|
|
});
|
|
const bad = parseAgentSettingsBody({ models: { teacher: {} } });
|
|
assert.equal(bad.ok, false);
|
|
if (!bad.ok) assert.match(bad.error, /unknown role "teacher" \(supported: researcher, demoBuilder, visualMaker, extractor, auditor, labeler\)/);
|
|
const malformed = parseAgentSettingsBody({ models: { researcher: { model: { provider: 1, id: "x" } } } });
|
|
assert.equal(malformed.ok, false);
|
|
});
|
|
|
|
it("tools and prompts: boolean maps; non-boolean values are 400s", () => {
|
|
const r = parseAgentSettingsBody({ tools: { research: false }, prompts: { "system-prompt.md": true } });
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings.tools, { research: false });
|
|
assert.deepEqual(r.settings.prompts, { "system-prompt.md": true });
|
|
assert.equal(parseAgentSettingsBody({ tools: { quiz: "no" } }).ok, false);
|
|
assert.equal(parseAgentSettingsBody({ prompts: { "x.md": 1 } }).ok, false);
|
|
// switch validity (segment membership, file presence) is the ROUTE's
|
|
// cross-validation — the parser only sees shapes.
|
|
});
|
|
|
|
it("search: engine pin is tavily|brave; anything else is a 400 (search wave, 2026-09-20)", () => {
|
|
const ok = parseAgentSettingsBody({ search: { engine: "tavily" } });
|
|
assert.ok(ok.ok);
|
|
assert.deepEqual(ok.settings.search, { engine: "tavily" });
|
|
const clear = parseAgentSettingsBody({ search: {} });
|
|
assert.ok(clear.ok);
|
|
assert.equal(clear.settings.search, undefined, "absent engine = back to auto-detect");
|
|
assert.equal(parseAgentSettingsBody({ search: { engine: "google" } }).ok, false);
|
|
assert.equal(parseAgentSettingsBody({ search: "tavily" }).ok, false, "search must be an object");
|
|
assert.equal(parseAgentSettingsBody({ search: { engine: 1 } }).ok, false);
|
|
});
|
|
|
|
it("an empty {} agent section parses to {} (replace-whole clear)", () => {
|
|
const r = parseAgentSettingsBody({});
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings, {});
|
|
});
|
|
});
|
|
|
|
describe("parseAppearanceSettingsBody", () => {
|
|
it("parses every field; hex-or-null colors; clamps happen at sanitize, rejects here", () => {
|
|
const r = parseAppearanceSettingsBody({
|
|
mode: "light",
|
|
background: "#ffffff",
|
|
fontFamily: "Some Font",
|
|
proseFontSize: 20,
|
|
sessionNameSerif: false,
|
|
mdRendering: { headingWeight: 700, boldColor: "#ff8800", italicColor: null },
|
|
});
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings, {
|
|
mode: "light",
|
|
background: "#ffffff",
|
|
fontFamily: "Some Font",
|
|
proseFontSize: 20,
|
|
sessionNameSerif: false,
|
|
mdRendering: { headingWeight: 700, boldColor: "#ff8800", italicColor: null },
|
|
});
|
|
});
|
|
|
|
it("rejects bad enums, ranges, and colors — with specific messages, never clamps", () => {
|
|
const cases: unknown[] = [
|
|
{ mode: "sepia" },
|
|
{ background: "blue" },
|
|
{ background: 42 },
|
|
{ fontFamily: "" },
|
|
{ fontFamily: 12 },
|
|
{ proseFontSize: 30 },
|
|
{ proseFontSize: 64.5 },
|
|
{ sessionNameSerif: 1 },
|
|
{ mdRendering: { headingWeight: 1000 } },
|
|
{ mdRendering: { boldColor: "red" } },
|
|
];
|
|
for (const body of cases) {
|
|
assert.equal(parseAppearanceSettingsBody(body).ok, false, JSON.stringify(body));
|
|
}
|
|
// boundary values that MUST pass: 12 and 24, 100 and 900, #fff
|
|
assert.ok(parseAppearanceSettingsBody({ proseFontSize: 12 }));
|
|
assert.ok(parseAppearanceSettingsBody({ proseFontSize: 24 }));
|
|
assert.ok(parseAppearanceSettingsBody({ mdRendering: { headingWeight: 100 } }));
|
|
assert.ok(parseAppearanceSettingsBody({ mdRendering: { headingWeight: 900 } }));
|
|
assert.ok(parseAppearanceSettingsBody({ background: "#abc" }));
|
|
// null = "the mode's default" — explicit, valid
|
|
assert.ok(parseAppearanceSettingsBody({ background: null, fontFamily: null }));
|
|
});
|
|
|
|
it("rejects non-object bodies and non-object mdRendering", () => {
|
|
assert.equal(parseAppearanceSettingsBody([1]).ok, false);
|
|
assert.equal(parseAppearanceSettingsBody({ mdRendering: "big" }).ok, false);
|
|
});
|
|
});
|
|
|
|
describe("parseGraphSettingsBody", () => {
|
|
it("parses the four-rung ladder replace-whole", () => {
|
|
const r = parseGraphSettingsBody({ enabled: false, embedding: false });
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings, { enabled: false, embedding: false });
|
|
assert.equal(parseGraphSettingsBody({}).ok, true);
|
|
});
|
|
|
|
it("rejects non-boolean rungs and unknown bodies", () => {
|
|
assert.equal(parseGraphSettingsBody({ enabled: "off" }).ok, false);
|
|
assert.equal(parseGraphSettingsBody("nope").ok, false);
|
|
});
|
|
|
|
it("the ladder DEPENDENCY (graph → embedding → clustering → labeling) is a UI concern, not a parse concern", () => {
|
|
// Nothing stops a stored {enabled: false, labeling: true} — the
|
|
// values persist but resolve inert (grays out in the panel).
|
|
const r = parseGraphSettingsBody({ enabled: false, clustering: true, labeling: true });
|
|
assert.ok(r.ok);
|
|
});
|
|
|
|
it("embeddingModel (wave 6): null or a shipped-provider slug; anything else is a 400", () => {
|
|
const nulled = parseGraphSettingsBody({ embeddingModel: null });
|
|
assert.ok(nulled.ok);
|
|
assert.deepEqual(nulled.settings, { embeddingModel: null });
|
|
for (const slug of [
|
|
"openai:text-embedding-3-small",
|
|
"openrouter:openai/text-embedding-3-large",
|
|
"google:gemini-embedding-001",
|
|
// OpenRouter ids may carry a :free-style suffix — the provider is
|
|
// the part before the FIRST colon (live catalog fact, 2026-09-23).
|
|
"openrouter:liquid/lfm-2.5-embedding-350m:free",
|
|
]) {
|
|
const r = parseGraphSettingsBody({ embeddingModel: slug });
|
|
assert.ok(r.ok, slug);
|
|
assert.deepEqual(r.settings, { embeddingModel: slug });
|
|
}
|
|
// Bad shapes never become a live model.
|
|
assert.equal(parseGraphSettingsBody({ embeddingModel: "text-embedding-3-small" }).ok, false, "no provider prefix");
|
|
assert.equal(parseGraphSettingsBody({ embeddingModel: "bedrock:whatever" }).ok, false, "unshipped provider");
|
|
assert.equal(parseGraphSettingsBody({ embeddingModel: "openai:" }).ok, false, "empty id");
|
|
assert.equal(parseGraphSettingsBody({ embeddingModel: 42 }).ok, false, "non-string");
|
|
assert.equal(parseGraphSettingsBody({ embeddingModel: "openai:has spaces" }).ok, false, "whitespace id");
|
|
});
|
|
});
|
|
|
|
describe("SETTINGS_SECTIONS (the route's section map)", () => {
|
|
it("maps exactly the six record sections", () => {
|
|
assert.deepEqual(Object.keys(SETTINGS_SECTIONS), ["general", "chat", "agent", "appearance", "graph", "models"]);
|
|
});
|
|
});
|
|
|
|
describe("parseModelsSettingsBody (model slots wave, 2026-09-20)", () => {
|
|
it("parses the three slot tiers — model required, level rides (BYOK, owner 2026-09-22)", () => {
|
|
const r = parseModelsSettingsBody({
|
|
strong: { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" } },
|
|
cheap: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "high" },
|
|
vision: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "low" },
|
|
});
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings, {
|
|
strong: { model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" } },
|
|
cheap: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "high" },
|
|
vision: { model: { provider: "zai", id: "glm-5.3-flash" }, thinkingLevel: "low" },
|
|
});
|
|
// a thinkingLevel WITHOUT a model can never run (no factory floor)
|
|
const levelOnly = parseModelsSettingsBody({ cheap: { thinkingLevel: "high" } });
|
|
assert.equal(levelOnly.ok, false);
|
|
if (!levelOnly.ok) {
|
|
assert.match(levelOnly.error, /slot "cheap" must set a model/);
|
|
}
|
|
});
|
|
|
|
it("{} is the legal clear-slots write", () => {
|
|
const r = parseModelsSettingsBody({});
|
|
assert.ok(r.ok);
|
|
assert.deepEqual(r.settings, {});
|
|
});
|
|
|
|
it("unknown slots are 400s with the supported list", () => {
|
|
const bad = parseModelsSettingsBody({ mega: { thinkingLevel: "high" } });
|
|
assert.equal(bad.ok, false);
|
|
if (!bad.ok) assert.match(bad.error, /unknown slot "mega" \(supported: strong, cheap, vision\)/);
|
|
});
|
|
|
|
it("a present-but-empty slot object is rejected (absent slot = unset)", () => {
|
|
const bad = parseModelsSettingsBody({ strong: {} });
|
|
assert.equal(bad.ok, false);
|
|
if (!bad.ok) assert.match(bad.error, /slot "strong" must set a model/);
|
|
});
|
|
|
|
it("malformed slot shapes are 400s", () => {
|
|
assert.equal(parseModelsSettingsBody({ cheap: { model: { provider: "x" } } }).ok, false);
|
|
assert.equal(parseModelsSettingsBody({ vision: { thinkingLevel: 7 } }).ok, false);
|
|
assert.equal(parseModelsSettingsBody("nope").ok, false);
|
|
});
|
|
});
|