mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 22:02:08 +03:00
The named-combos pipeline dropdown offered four engines (headroom, session-dedup, ccr, llmlingua) that stackedPipelineStepSchema rejects, so selecting one made PUT /api/context/combos/[id] return HTTP 400 while saveCombo swallowed the non-OK response (if (!res.ok) return). Editing the default 'Standard Savings' combo and changing an engine reproduced the 400. - Add canonical STACKED_PIPELINE_ENGINE_INTENSITIES next to the schema as the single source of truth; the client dropdown imports it so it can never drift from the discriminated union the API validates against. - Surface save errors and empty-name/empty-pipeline validation in the editor instead of failing silently. - Add a parity unit test asserting the UI engine map equals the schema union and that every (engine, intensity) the UI emits is accepted.
This commit is contained in:
committed by
GitHub
parent
d5ffdf2a5e
commit
ba6f2e034c
10
CHANGELOG.md
10
CHANGELOG.md
@@ -8,6 +8,16 @@
|
||||
|
||||
_In development — bullets added per PR; finalized at release._
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **Engine Combos editor: saving a pipeline no longer fails silently with HTTP 400 (#4955).** The
|
||||
named-combos pipeline dropdown offered four engines (`headroom`, `session-dedup`, `ccr`,
|
||||
`llmlingua`) that the `PUT /api/context/combos/[id]` schema rejects, so selecting one made the
|
||||
save return 400 while the UI swallowed the error. The dropdown is now sourced from a single
|
||||
canonical engine map shared with `stackedPipelineStepSchema` (parity guarded by a unit test), and
|
||||
the editor surfaces save errors and empty-name/empty-pipeline validation instead of failing
|
||||
quietly.
|
||||
|
||||
---
|
||||
|
||||
## [3.8.36] — 2026-06-25
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// matching `EngineConfigPage` / `CompressionHub`, both of which hydrate cleanly.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { STACKED_PIPELINE_ENGINE_INTENSITIES } from "@/shared/validation/compressionConfigSchemas";
|
||||
import CompressionHub from "./CompressionHub";
|
||||
|
||||
type PipelineStep = { engine: string; intensity?: string };
|
||||
@@ -29,17 +30,9 @@ const EMPTY_PIPELINE: PipelineStep[] = [
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
];
|
||||
|
||||
const ENGINE_INTENSITIES: Record<string, string[]> = {
|
||||
rtk: ["minimal", "standard", "aggressive"],
|
||||
caveman: ["lite", "full", "ultra"],
|
||||
lite: ["lite"],
|
||||
aggressive: ["standard"],
|
||||
ultra: ["ultra"],
|
||||
headroom: ["standard"],
|
||||
"session-dedup": ["standard"],
|
||||
ccr: ["standard"],
|
||||
llmlingua: ["standard"],
|
||||
};
|
||||
// Engine list is sourced from the API schema so the dropdown can never offer an engine
|
||||
// the `PUT /api/context/combos/[id]` route would reject with HTTP 400 (#4955).
|
||||
const ENGINE_INTENSITIES: Record<string, readonly string[]> = STACKED_PIPELINE_ENGINE_INTENSITIES;
|
||||
|
||||
function NamedCombosManager() {
|
||||
const [combos, setCombos] = useState<CompressionCombo[]>([]);
|
||||
@@ -55,6 +48,7 @@ function NamedCombosManager() {
|
||||
const [assignmentIds, setAssignmentIds] = useState<string[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [activeComboId, setActiveComboId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = () => {
|
||||
fetch("/api/context/combos")
|
||||
@@ -88,6 +82,7 @@ function NamedCombosManager() {
|
||||
setOutputMode(false);
|
||||
setOutputModeIntensity("full");
|
||||
setAssignmentIds([]);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const loadAssignments = async (id: string) => {
|
||||
@@ -112,7 +107,15 @@ function NamedCombosManager() {
|
||||
|
||||
const saveCombo = async () => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
if (!trimmed) {
|
||||
setError("Enter a combo name before saving.");
|
||||
return;
|
||||
}
|
||||
if (pipeline.length === 0) {
|
||||
setError("Add at least one pipeline step before saving.");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
@@ -131,7 +134,11 @@ function NamedCombosManager() {
|
||||
body: JSON.stringify(payload),
|
||||
}
|
||||
);
|
||||
if (!res.ok) return;
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null);
|
||||
setError(body?.error || `Failed to save combo (HTTP ${res.status}).`);
|
||||
return;
|
||||
}
|
||||
const combo = await res.json();
|
||||
await fetch(`/api/context/combos/${combo.id}/assignments`, {
|
||||
method: "PUT",
|
||||
@@ -315,6 +322,12 @@ function NamedCombosManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mt-4 text-sm text-danger" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={saveCombo}
|
||||
|
||||
@@ -177,6 +177,26 @@ export const stackedPipelineStepSchema = z.discriminatedUnion("engine", [
|
||||
.strict(),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Canonical engine → selectable-intensities map for the named-combos pipeline editor
|
||||
* (Engine Combos UI). This is the SINGLE source of truth shared by the dashboard
|
||||
* dropdowns and `stackedPipelineStepSchema`: every engine/intensity offered here is,
|
||||
* by construction, accepted by the API update schema.
|
||||
*
|
||||
* Do NOT add an engine here that is not a branch of `stackedPipelineStepSchema` — the
|
||||
* `PUT /api/context/combos/[id]` route validates against that discriminated union and
|
||||
* would reject the payload with HTTP 400 (#4955: the UI previously offered `headroom`,
|
||||
* `session-dedup`, `ccr`, `llmlingua`, none of which the union accepts, so selecting
|
||||
* one silently failed the save). The parity is guarded by a unit test.
|
||||
*/
|
||||
export const STACKED_PIPELINE_ENGINE_INTENSITIES: Record<string, readonly string[]> = {
|
||||
rtk: ["minimal", "standard", "aggressive"],
|
||||
caveman: ["lite", "full", "ultra"],
|
||||
lite: ["lite"],
|
||||
aggressive: ["standard"],
|
||||
ultra: ["ultra"],
|
||||
};
|
||||
|
||||
export const engineToggleSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
level: z.string().optional(),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
STACKED_PIPELINE_ENGINE_INTENSITIES,
|
||||
stackedPipelineStepSchema,
|
||||
} from "../../../src/shared/validation/compressionConfigSchemas.ts";
|
||||
|
||||
// Regression guard for #4955: the Engine Combos pipeline editor used to offer engines
|
||||
// (headroom, session-dedup, ccr, llmlingua) that `stackedPipelineStepSchema` rejects, so
|
||||
// selecting one made `PUT /api/context/combos/[id]` fail with HTTP 400 and the UI swallowed
|
||||
// it. The fix routes the dropdown through STACKED_PIPELINE_ENGINE_INTENSITIES, which MUST stay
|
||||
// in lockstep with the discriminated union below.
|
||||
describe("Engine Combos UI ↔ stackedPipelineStepSchema parity (#4955)", () => {
|
||||
const unionEngines = stackedPipelineStepSchema.options
|
||||
.map((option: { shape: { engine: { value: string } } }) => option.shape.engine.value)
|
||||
.sort();
|
||||
|
||||
it("offers exactly the engines the API update schema accepts (no drift)", () => {
|
||||
const uiEngines = Object.keys(STACKED_PIPELINE_ENGINE_INTENSITIES).sort();
|
||||
assert.deepEqual(uiEngines, unionEngines);
|
||||
});
|
||||
|
||||
it("every (engine, intensity) the UI can emit is accepted by the schema", () => {
|
||||
for (const [engine, intensities] of Object.entries(STACKED_PIPELINE_ENGINE_INTENSITIES)) {
|
||||
for (const intensity of intensities) {
|
||||
const result = stackedPipelineStepSchema.safeParse({ engine, intensity });
|
||||
assert.equal(
|
||||
result.success,
|
||||
true,
|
||||
`expected { engine: "${engine}", intensity: "${intensity}" } to be accepted`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("the engines removed from the UI in #4955 are indeed rejected by the schema", () => {
|
||||
for (const engine of ["headroom", "session-dedup", "ccr", "llmlingua"]) {
|
||||
assert.equal(
|
||||
stackedPipelineStepSchema.safeParse({ engine, intensity: "standard" }).success,
|
||||
false,
|
||||
`engine "${engine}" must not be a valid stacked-pipeline step`
|
||||
);
|
||||
assert.equal(
|
||||
STACKED_PIPELINE_ENGINE_INTENSITIES[engine],
|
||||
undefined,
|
||||
`engine "${engine}" must not be offered by the combos UI`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user