Files
learnharness/tests/embedding-settings.test.tsx
T

91 lines
3.8 KiB
TypeScript
Raw Normal View History

import React from "react";
(globalThis as Record<string, unknown>).React = React;
import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
embeddingPriceLabel,
GraphSection,
needsEmbeddingModel,
} from "../components/settings/appearance-section.tsx";
import { EMBEDDING_PRICES, type GraphSettings } from "../src/core/index.ts";
/**
* Wave 6 chunk B — the embeddings settings seams: the price line for
* catalog rows (EMBEDDING_PRICES lookup, honestly "—" when unlisted),
* the model-dependency predicate that drives the ladder gray-out, and
* GraphSection's SSR shape with and without a configured model.
*
* The fetches (catalog, node count) live in effects — dead under
* renderToStaticMarkup, so the render pins the STORED-state markup only;
* the interactive round-trips are live-verified on the dev server.
*/
describe("embeddingPriceLabel", () => {
it("prices a listed model: USD per 1M input tokens", () => {
// Straight from the hand-maintained table (openai small = $0.02).
assert.equal(embeddingPriceLabel("openai:text-embedding-3-small"), "$0.02/1M in");
});
it("prices every entry in EMBEDDING_PRICES without zero or 'NaN' leaking", () => {
for (const id of Object.keys(EMBEDDING_PRICES)) {
const label = embeddingPriceLabel(id);
assert.match(label, /^\$\d+(\.\d+)?\/1M in$/);
assert.notEqual(label, "$0/1M in");
}
});
it("trims a trailing zero the model-menu way ($0.2, not $0.20)", () => {
assert.equal(embeddingPriceLabel("google:gemini-embedding-2"), "$0.2/1M in");
});
it("stays honestly '—' for unlisted models (never zero)", () => {
assert.equal(embeddingPriceLabel("openrouter:some/model-not-in-table"), "—");
});
});
describe("needsEmbeddingModel", () => {
it("spares only the master switch", () => {
assert.equal(needsEmbeddingModel("enabled"), false);
assert.equal(needsEmbeddingModel("embedding"), true);
assert.equal(needsEmbeddingModel("clustering"), true);
assert.equal(needsEmbeddingModel("labeling"), true);
});
});
describe("GraphSection (SSR)", () => {
const render = (graph: GraphSettings | undefined) =>
renderToStaticMarkup(<GraphSection graph={graph} onSectionChange={() => {}} />);
it("grays out + hints the model-dependent rungs when no model is set", () => {
const html = render({ enabled: true } as GraphSettings);
// Master switch: never model-dimmed.
const enabledRow = html.split('data-testid="graph-row-enabled"')[1]?.split("</div>")[0] ?? "";
assert.ok(!enabledRow.includes("opacity-40"), "master switch must not dim on unset model");
// The three dependent rungs dim and each carries the hint.
for (const key of ["embedding", "clustering", "labeling"] as const) {
const row = html.split(`data-testid="graph-row-${key}"`)[1]?.split('data-testid="graph-row-')[0] ?? "";
assert.ok(row.includes("opacity-40"), `${key} row dims without a model`);
assert.ok(row.includes("Needs an embedding model"), `${key} row carries the hint`);
}
assert.ok(html.includes('data-testid="graph-row-embedding-model"'));
});
it("keeps the ladder touchable once a model is set", () => {
const html = render({
enabled: true,
embeddingModel: "openai:text-embedding-3-small",
} as GraphSettings);
for (const key of ["embedding", "clustering", "labeling"] as const) {
const row = html.split(`data-testid="graph-row-${key}"`)[1]?.split('data-testid="graph-row-')[0] ?? "";
assert.ok(!row.includes("opacity-40"), `${key} row stays lit with a model`);
assert.ok(!row.includes("Needs an embedding model"), "no hint with a model");
}
});
it("renders the picker row (catalog fetch is an effect — inert in SSR)", () => {
const html = render(undefined);
assert.ok(html.includes("Loading…"), "pre-fetch trigger state renders");
});
});