swarm-ui: drop an unused export and test the classifier settings logic
The runtime list was exported but only its type was used, so the type is now written out. The form's pure logic (draft, checks, request body, change detection) gets tests under node's built-in runner, run by the existing swarm-ui check.
This commit is contained in:
parent
a04096ea19
commit
93ae435287
4 changed files with 126 additions and 3 deletions
|
|
@ -6,7 +6,8 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node ./build.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test test/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hive/shared": "*",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ export type Effort = (typeof EFFORTS)[number];
|
|||
|
||||
/** Which runtime a classification runs on: the claude CLI on the Anthropic
|
||||
* API key, or opencode on an ACP provider. */
|
||||
export const RUNTIMES = ["api-key", "acp"] as const;
|
||||
export type Runtime = (typeof RUNTIMES)[number];
|
||||
export type Runtime = "api-key" | "acp";
|
||||
|
||||
/** What the provider charges, in USD per million tokens. A cache price left
|
||||
* out counts as `input`. */
|
||||
|
|
|
|||
119
frontend/packages/swarm-ui/test/settings-types.test.mjs
Normal file
119
frontend/packages/swarm-ui/test/settings-types.test.mjs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// The classifier settings form's pure logic: the draft a stored document
|
||||
// becomes, the checks before a save, the request body, and what counts as a
|
||||
// change. Run by `npm test`, which Node runs with its built-in test runner and
|
||||
// type stripping, so the `.ts` module is imported as it is.
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
changed,
|
||||
draftOf,
|
||||
maxValue,
|
||||
priceValue,
|
||||
settingsProblem,
|
||||
toSettings,
|
||||
} from "../src/pages/review-settings/SettingsTypes.ts";
|
||||
|
||||
const apiKey = {
|
||||
model: "haiku",
|
||||
effort: null,
|
||||
max_concurrent_reviews: 2,
|
||||
runtime: "api-key",
|
||||
acp: null,
|
||||
};
|
||||
|
||||
const acpStored = {
|
||||
...apiKey,
|
||||
runtime: "acp",
|
||||
acp: {
|
||||
base_url: "https://llm.example/v1",
|
||||
model: "Qwen/Qwen3-35B",
|
||||
prices: { input: 0.5, output: 2, cache_read: 0.1 },
|
||||
},
|
||||
};
|
||||
|
||||
// An ACP draft over the API key settings, with every provider field filled.
|
||||
function acpDraft(over = {}) {
|
||||
return {
|
||||
...draftOf(apiKey),
|
||||
runtime: "acp",
|
||||
acpUrl: "https://llm.example/v1",
|
||||
acpModel: "Qwen/Qwen3-35B",
|
||||
priceInput: "0.5",
|
||||
priceOutput: "2",
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
test("an unedited draft is valid and changes nothing", () => {
|
||||
const d = draftOf(apiKey);
|
||||
assert.equal(settingsProblem(d), null);
|
||||
assert.equal(changed(d, apiKey), false);
|
||||
});
|
||||
|
||||
test("the API key request leaves the provider out, so the stored one stays", () => {
|
||||
const body = toSettings({ ...draftOf(acpStored), runtime: "api-key" });
|
||||
assert.deepEqual(body, {
|
||||
model: "haiku",
|
||||
effort: null,
|
||||
max_concurrent_reviews: 2,
|
||||
runtime: "api-key",
|
||||
});
|
||||
assert.equal("acp" in body, false);
|
||||
});
|
||||
|
||||
test("the ACP request carries the provider and its prices", () => {
|
||||
const body = toSettings(acpDraft({ priceCacheRead: "0.1" }));
|
||||
assert.equal(body.runtime, "acp");
|
||||
assert.deepEqual(body.acp, {
|
||||
base_url: "https://llm.example/v1",
|
||||
model: "Qwen/Qwen3-35B",
|
||||
prices: { input: 0.5, output: 2, cache_read: 0.1 },
|
||||
});
|
||||
});
|
||||
|
||||
test("a cache price left empty is left out of the request", () => {
|
||||
const { prices } = toSettings(acpDraft()).acp;
|
||||
assert.equal("cache_read" in prices, false);
|
||||
assert.equal("cache_write" in prices, false);
|
||||
});
|
||||
|
||||
test("the draft of a stored provider round-trips to the same request", () => {
|
||||
const body = toSettings(draftOf(acpStored));
|
||||
assert.deepEqual(body.acp, acpStored.acp);
|
||||
assert.equal(changed(draftOf(acpStored), acpStored), false);
|
||||
});
|
||||
|
||||
test("the provider fields are checked only on the ACP runtime", () => {
|
||||
const empty = { ...draftOf(apiKey), acpUrl: "http://nope" };
|
||||
assert.equal(settingsProblem(empty), null);
|
||||
assert.match(settingsProblem({ ...empty, runtime: "acp" }), /https:\/\//);
|
||||
});
|
||||
|
||||
test("the ACP checks name the field at fault", () => {
|
||||
assert.match(settingsProblem(acpDraft({ acpUrl: "http://x" })), /https:\/\//);
|
||||
assert.match(settingsProblem(acpDraft({ acpModel: "a b" })), /model/);
|
||||
assert.match(settingsProblem(acpDraft({ priceOutput: "-1" })), /price/);
|
||||
assert.match(settingsProblem(acpDraft({ priceInput: "" })), /price/);
|
||||
assert.match(settingsProblem(acpDraft({ priceCacheWrite: "x" })), /cache/);
|
||||
assert.equal(settingsProblem(acpDraft()), null);
|
||||
});
|
||||
|
||||
test("switching the runtime is a change; edits to the hidden provider are not", () => {
|
||||
const saved = apiKey;
|
||||
assert.equal(changed({ ...draftOf(saved), runtime: "acp" }, saved), true);
|
||||
assert.equal(
|
||||
changed({ ...draftOf(saved), acpUrl: "https://x" }, saved),
|
||||
false,
|
||||
);
|
||||
assert.equal(changed(acpDraft({ priceOutput: "3" }), acpStored), true);
|
||||
});
|
||||
|
||||
test("the reviews limit and a price parse as the form needs", () => {
|
||||
assert.equal(maxValue("8"), 8);
|
||||
assert.equal(maxValue("9"), null);
|
||||
assert.equal(maxValue("1.5"), null);
|
||||
assert.equal(priceValue("0"), 0);
|
||||
assert.equal(priceValue("0.25"), 0.25);
|
||||
assert.equal(priceValue("-1"), null);
|
||||
assert.equal(priceValue("1e3"), null);
|
||||
});
|
||||
Loading…
Reference in a new issue