mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 12:52:25 +03:00
fix(combos): accept isHidden in updateComboSchema (#12898)
* fix(combos): accept isHidden in updateComboSchema A combo's visibility is stored on the record and honoured by the builder option list and the dashboard grid, but updateComboSchema never listed isHidden. The PUT handler spreads the validated body, so zod stripped the field: a visibility-only update was rejected as "No valid fields to update", and a mixed update succeeded while dropping the visibility change. Closes #12836 * docs(changelog): fragment for #12898
This commit is contained in:
1
changelog.d/fixes/12898-combo-hidden-update-schema.md
Normal file
1
changelog.d/fixes/12898-combo-hidden-update-schema.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(combos):** A combo's visibility can be changed through the API again: `updateComboSchema` accepts `isHidden`, so a visibility-only update is no longer rejected as empty and a mixed update no longer drops it ([#12898](https://github.com/diegosouzapw/OmniRoute/pull/12898), closes [#12836](https://github.com/diegosouzapw/OmniRoute/issues/12836))
|
||||
@@ -419,6 +419,11 @@ export const updateComboSchema = z
|
||||
strategy: comboStrategySchema.optional(),
|
||||
config: comboRuntimeConfigSchema.optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
// Stored on the combo record and honoured by the readers — the builder's
|
||||
// option list and the dashboard grid both filter on it — but omitted here,
|
||||
// so the one endpoint a client can flip it through stripped the field and
|
||||
// a visibility-only update was rejected as empty. #12836
|
||||
isHidden: z.boolean().optional(),
|
||||
allowedProviders: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
|
||||
allowedModelFamilies: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
|
||||
// Nullable like `description` and `context_length` above: an absent field means
|
||||
@@ -443,6 +448,7 @@ export const updateComboSchema = z
|
||||
value.strategy === undefined &&
|
||||
value.config === undefined &&
|
||||
value.isActive === undefined &&
|
||||
value.isHidden === undefined &&
|
||||
value.allowedProviders === undefined &&
|
||||
value.allowedModelFamilies === undefined &&
|
||||
value.system_message === undefined &&
|
||||
|
||||
102
tests/unit/combo-hidden-update-12836.test.ts
Normal file
102
tests/unit/combo-hidden-update-12836.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* #12836 — a combo's visibility must be updatable through the API.
|
||||
*
|
||||
* `isHidden` is stored on the combo record and honoured by its readers: the
|
||||
* builder's option list drops hidden combos (`src/lib/combos/builderOptions.ts`)
|
||||
* and so does the dashboard grid. `createCombo` writes the field explicitly.
|
||||
* The only endpoint a client can flip it through is `PUT /api/combos/[id]`,
|
||||
* which spreads the *validated* body into the update — and `updateComboSchema`
|
||||
* never listed `isHidden`, so zod stripped it. A visibility-only update was
|
||||
* rejected as "No valid fields to update", and pairing it with a recognized
|
||||
* field made the request succeed while silently dropping the visibility change.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-hidden-12836-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { updateComboSchema } = await import("../../src/shared/validation/schemas.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const combosDb = await import("../../src/lib/db/combos.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#12836 a visibility-only update is a valid update", () => {
|
||||
assert.equal(updateComboSchema.parse({ isHidden: true }).isHidden, true);
|
||||
assert.equal(updateComboSchema.parse({ isHidden: false }).isHidden, false);
|
||||
// The empty-update refinement still fires for a genuinely empty body.
|
||||
assert.throws(() => updateComboSchema.parse({}), /No valid fields to update/);
|
||||
});
|
||||
|
||||
test("#12836 visibility survives a mixed update instead of being stripped", () => {
|
||||
const parsed = updateComboSchema.parse({ description: "fixture", isHidden: true });
|
||||
assert.equal(parsed.description, "fixture");
|
||||
assert.equal(parsed.isHidden, true);
|
||||
});
|
||||
|
||||
test("#12836 only a boolean is accepted", () => {
|
||||
for (const bad of ["true", 1, null, {}]) {
|
||||
assert.throws(() => updateComboSchema.parse({ isHidden: bad }));
|
||||
}
|
||||
});
|
||||
|
||||
test("#12836 hiding a combo persists and reads back", async () => {
|
||||
const created = await combosDb.createCombo({
|
||||
name: "Visibility Combo",
|
||||
models: [{ provider: "openai", model: "gpt-4.1" }],
|
||||
});
|
||||
assert.equal(created.isHidden, false);
|
||||
|
||||
const body = updateComboSchema.parse({ isHidden: true });
|
||||
const updated = await combosDb.updateCombo(created.id as string, body);
|
||||
assert.ok(updated);
|
||||
assert.equal(updated!.isHidden, true);
|
||||
|
||||
// Re-read: this is the state the builder and the dashboard grid filter on.
|
||||
const reread = await combosDb.getComboById(created.id as string);
|
||||
assert.ok(reread);
|
||||
assert.equal(reread!.isHidden, true);
|
||||
});
|
||||
|
||||
test("#12836 unhiding it again works, and an unrelated update leaves it alone", async () => {
|
||||
const created = await combosDb.createCombo({
|
||||
name: "Round Trip Combo",
|
||||
models: [{ provider: "openai", model: "gpt-4.1" }],
|
||||
});
|
||||
|
||||
await combosDb.updateCombo(created.id as string, updateComboSchema.parse({ isHidden: true }));
|
||||
const shown = await combosDb.updateCombo(
|
||||
created.id as string,
|
||||
updateComboSchema.parse({ isHidden: false })
|
||||
);
|
||||
assert.ok(shown);
|
||||
assert.equal(shown!.isHidden, false);
|
||||
|
||||
await combosDb.updateCombo(created.id as string, updateComboSchema.parse({ isHidden: true }));
|
||||
const untouched = await combosDb.updateCombo(
|
||||
created.id as string,
|
||||
updateComboSchema.parse({ description: "note" })
|
||||
);
|
||||
assert.ok(untouched);
|
||||
assert.equal(untouched!.description, "note");
|
||||
assert.equal(untouched!.isHidden, true);
|
||||
});
|
||||
Reference in New Issue
Block a user