mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 05:32:19 +03:00
* feat(combos): add select all / unselect all in Browse Catalog * fix(dashboard): guard combo Select all + test the real batch handlers (#8526) Select all had no cap — with "Show configured only" off, or a large provider catalog, one click could add hundreds of models to a combo. ModelSelectModal now confirms above SELECT_ALL_CONFIRM_THRESHOLD (20) before batch-adding, matching the native confirm() pattern already used for bulk/destructive actions elsewhere in the dashboard. Also extracts ComboFormModal's handleAddModels/handleDeselectModels batching logic into computeBatchAddModelSteps/computeBatchDeselectModelSteps (src/lib/combos/builderDraft.ts) so unit tests exercise the real implementation instead of a hand-maintained mirror that could drift from the component and stay green while production code broke. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
// Regression guard for #8526 (combo Browse Catalog "Select all"/"Unselect
|
|
// all"). This is the BLOCKING copy — tests/unit/*.test.ts is collected by
|
|
// `npm run test:unit` (node:test), unlike tests/unit/ui/*.test.tsx which is
|
|
// only collected by the advisory `test:vitest:ui`
|
|
// (see CONTRIBUTING.md "Both test runners must pass").
|
|
//
|
|
// It exercises the exact functions `ComboFormModal::handleAddModels` /
|
|
// `handleDeselectModels` (src/app/(dashboard)/dashboard/combos/page.tsx)
|
|
// delegate to — not a hand-maintained mirror of the batching logic — so a
|
|
// broken extraction fails this suite, not just an advisory one.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const builderDraft = await import("../../src/lib/combos/builderDraft.ts");
|
|
|
|
test("computeBatchAddModelSteps adds every candidate against a growing list in one pass", () => {
|
|
const { next, addedAny } = builderDraft.computeBatchAddModelSteps(
|
|
[],
|
|
[
|
|
{ value: "openai/gpt-4o" },
|
|
{ value: "openai/gpt-4o-mini" },
|
|
{ value: "anthropic/claude-3-5-sonnet" },
|
|
],
|
|
[]
|
|
);
|
|
assert.equal(addedAny, true);
|
|
assert.deepEqual(
|
|
next.map((m) => m.model),
|
|
["openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-3-5-sonnet"]
|
|
);
|
|
});
|
|
|
|
test("computeBatchAddModelSteps skips exact provider/model/account duplicates", () => {
|
|
const existing = [{ model: "openai/gpt-4o", providerId: "openai", weight: 0 }];
|
|
const { next, addedAny } = builderDraft.computeBatchAddModelSteps(
|
|
existing,
|
|
[{ value: "openai/gpt-4o", providerId: "openai" }, { value: "openai/gpt-4o-mini" }],
|
|
[]
|
|
);
|
|
assert.equal(addedAny, true);
|
|
assert.deepEqual(
|
|
next.map((m) => m.model),
|
|
["openai/gpt-4o", "openai/gpt-4o-mini"]
|
|
);
|
|
});
|
|
|
|
test("computeBatchAddModelSteps resolves providerId via builderProviders alias/prefix", () => {
|
|
const builderProviders = [{ providerId: "openai", alias: "oa", prefix: "gpt" }];
|
|
const { next } = builderDraft.computeBatchAddModelSteps(
|
|
[],
|
|
[{ value: "gpt/gpt-4o", providerId: "gpt" }],
|
|
builderProviders
|
|
);
|
|
assert.equal(next[0].providerId, "openai");
|
|
});
|
|
|
|
test("computeBatchAddModelSteps ignores empty/missing values and is a no-op with nothing selected", () => {
|
|
const { next, addedAny } = builderDraft.computeBatchAddModelSteps(
|
|
[],
|
|
[{ value: "" }, {}, { value: "openai/gpt-4o" }],
|
|
[]
|
|
);
|
|
assert.equal(addedAny, true);
|
|
// No matching entry in builderProviders, so providerId falls back to the
|
|
// provider prefix parsed out of the qualified model string.
|
|
assert.deepEqual(next, [{ model: "openai/gpt-4o", providerId: "openai", weight: 0 }]);
|
|
|
|
const existing = [{ model: "openai/gpt-4o", weight: 0 }];
|
|
const noop = builderDraft.computeBatchAddModelSteps(existing, [], []);
|
|
assert.equal(noop.addedAny, false);
|
|
assert.equal(noop.next, existing);
|
|
});
|
|
|
|
test("computeBatchDeselectModelSteps removes every matching qualified model in one pass", () => {
|
|
const models = [
|
|
{ model: "openai/gpt-4o", weight: 0 },
|
|
{ model: "openai/gpt-4o-mini", weight: 0 },
|
|
{ model: "anthropic/claude-3-5-sonnet", weight: 0 },
|
|
];
|
|
const next = builderDraft.computeBatchDeselectModelSteps(models, [
|
|
{ value: "openai/gpt-4o" },
|
|
{ value: "openai/gpt-4o-mini" },
|
|
]);
|
|
assert.deepEqual(next, [{ model: "anthropic/claude-3-5-sonnet", weight: 0 }]);
|
|
});
|
|
|
|
test("computeBatchDeselectModelSteps accepts raw string identifiers", () => {
|
|
const models = [
|
|
{ model: "openai/gpt-4o", weight: 0 },
|
|
{ model: "openai/gpt-4o-mini", weight: 0 },
|
|
];
|
|
const next = builderDraft.computeBatchDeselectModelSteps(models, ["openai/gpt-4o"]);
|
|
assert.deepEqual(next, [{ model: "openai/gpt-4o-mini", weight: 0 }]);
|
|
});
|
|
|
|
test("computeBatchDeselectModelSteps is a no-op (same reference) when there is nothing to remove", () => {
|
|
const models = [{ model: "openai/gpt-4o", weight: 0 }];
|
|
assert.equal(builderDraft.computeBatchDeselectModelSteps(models, []), models);
|
|
assert.equal(builderDraft.computeBatchDeselectModelSteps(models, [{ value: "" }]), models);
|
|
});
|