From 24ae0b32f89ca64471c2c63e8e49b7a1901cc538 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:54:58 -0300 Subject: [PATCH] fix: surface fusion-specific config on the Global Routing tab (#5598) (#5688) --- CHANGELOG.md | 2 + config/quality/file-size-baseline.json | 3 +- .../settings/components/ComboDefaultsTab.tsx | 5 + .../components/FusionDefaultsFields.tsx | 92 +++++++++++++++++++ .../ui/combo-defaults-fusion-5598.test.tsx | 84 +++++++++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx create mode 100644 tests/unit/ui/combo-defaults-fusion-5598.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a3f0ae3b..60bf7370c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### πŸ”§ Bug Fixes +- **dashboard (routing):** selecting the **fusion** strategy on the Global Routing defaults tab now reveals fusion-specific config instead of only the generic resilience fields. Fusion's engine knobs β€” `judgeModel` (the model that synthesizes the panel answers) and `fusionTuning` (`minPanel` / `stragglerGraceMs` / `panelHardTimeoutMs`) β€” already existed in the schema and the per-combo editor, but the Global Routing tab never surfaced them, so picking "fusion" there was effectively a no-op. The fields are now shown (extracted into a new `FusionDefaultsFields` component). Voting / aggregation-mode / per-provider-weight are intentionally not shown β€” those don't exist in the fusion engine. Regression guard: `tests/unit/ui/combo-defaults-fusion-5598.test.tsx`. ([#5598](https://github.com/diegosouzapw/OmniRoute/issues/5598)) + - **dashboard (free proxy pool):** the free proxy pool "Sync All" no longer fails silently with `Total: 0`. Three fixes: (1) the **IPLocate** source fetched `…/protocols/.json` and parsed it as JSON, but the upstream list is plain text (`.txt`, one `ip:port` per line) β€” every protocol 404'd / failed to parse; it now fetches `.txt` and parses the line list. (2) The sync route **isolates each source** in its own try/catch, so one provider throwing (e.g. a TLS handshake failure) no longer aborts the whole sync β€” the working sources still populate the pool. (3) The UI now **surfaces the per-source errors** the route already returns, instead of discarding the response, so a partial/empty sync explains itself. Regression guards: `tests/unit/free-proxy-providers.test.ts`, `tests/unit/proxy-pool-sync-4878.test.ts`, `tests/unit/free-pool-tab.test.tsx`. ([#5595](https://github.com/diegosouzapw/OmniRoute/issues/5595)) - **dashboard (memory engine):** the memory engine status page no longer mixes English and Portuguese. The embedding / vector-store / rerank **status detail strings** were hardcoded in Portuguese in the backend (`resolveEmbeddingSource`, `engineStatus`), e.g. `auto: nenhuma fonte de embedding disponΓ­vel` and `sqlite-vec ativo, dim=…`, while the surrounding UI labels render from the English i18n bundle β€” so an English user saw a half-translated page. The backend detail strings are now English (`auto: no embedding source available`, `sqlite-vec active, dim=…`, etc.), matching the rest of the page. Regression guard: `tests/unit/memory-engine-status.test.ts`. ([#5596](https://github.com/diegosouzapw/OmniRoute/issues/5596)) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index e957dfe278..31170f8a77 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_30_5598_fusion_defaults": "PR #5598 own growth: ComboDefaultsTab.tsx 846->851 (+5 = import + the `strategy === \"fusion\"` conditional that mounts the new FusionDefaultsFields component). The bulk of the fusion-defaults UI (judgeModel + fusionTuning inputs) was EXTRACTED to a new src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx (92 LOC, 997 + chat 1632->1635 + chatHelpers 811->842 (#5351 opencode visible-rotation-logs/ProxyEgress/SQL-vars), base.ts 1475->1497 + openai-to-claude 805->823 (#5352 thinking hydrate/redacted-replay + #5342 empty-messages guard), accountFallback 1777->1783 (#5346 402 auto-disable depleted key) + account-fallback-service.test 1569->1572 (#5346 test), provider-validation-specialty.test 2801->2843 (#5358 grok cf_clearance + #5337 gemini catalog). Cycle-close drift from merged campaign PRs absorbed at release close per generate-release Phase 0 (the PR->release fast-gates do NOT run check:file-size); all irreducible chokepoint/feature growth next to existing branches, covered by per-PR tests. Structural shrink tracked under decomposition roadmap #3501.", "_rebaseline_2026_06_26_v3838_ownerprs_batch": "Lote /review-prs v3.8.38 (PRs do dono + contribuidores): src/lib/db/providers.ts 1093->1107 (+14 = #5121 cookie-dedup branch extraido para o helper findExistingCookieConnection β€” a extracao reduz a complexidade ciclomatica de createProviderConnection mas adiciona ~14 linhas ao arquivo; trade-off consciente complexity-vs-file-size) e src/lib/usage/usageHistory.ts 934->983 (+49 = #4940 dedup guard SELECT-before-INSERT + endpoint backfill + scheduleStatsEvent debounce). Crescimento de feature/fix legitimo recem-TDD'd; o fast-path PR->release nao roda check:file-size, entao o ramo acumula sem rebaselinar. Reducao estrutural fica como debt (#3501).", "_rebaseline_2026_06_26_relgreen_db_test": "Release-green follow-up: tests/unit/db-core-init.test.ts 867->877 (+10 = the invalid-DATA_DIR test now captures the rejection and asserts both Error type AND message β€” restores net-neutral assert count after #5117's consolidation, satisfying check:test-masking β€” instead of a single assert.rejects).", @@ -201,7 +202,7 @@ "src/app/(dashboard)/dashboard/providers/page.tsx": 1927, "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201, "src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819, - "src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 846, + "src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 851, "src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974, "src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898, "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012, diff --git a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx index 86a633473e..bf83cbd4aa 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { Card, Button, Input, Toggle } from "@/shared/components"; import { cn } from "@/shared/utils/cn"; +import FusionDefaultsFields from "./FusionDefaultsFields"; import { ROUTING_STRATEGIES, SETTINGS_FALLBACK_STRATEGY_VALUES, @@ -647,6 +648,10 @@ export default function ComboDefaultsTab() { )} + {comboDefaults.strategy === "fusion" && ( + + )} + {/* Toggles */}
diff --git a/src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx b/src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx new file mode 100644 index 0000000000..15762763ce --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx @@ -0,0 +1,92 @@ +import { Input } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +function translateOrFallback( + t: ReturnType, + key: string, + fallback: string +): string { + return typeof t.has === "function" && t.has(key) ? t(key) : fallback; +} + +interface FusionDefaultsFieldsProps { + comboDefaults: any; + setComboDefaults: (updater: (prev: any) => any) => void; +} + +/** + * #5598 β€” Fusion-specific defaults for the Global Routing tab. Selecting the + * "fusion" strategy previously showed only the generic resilience fields even + * though fusion has real engine knobs (`open-sse/services/fusion.ts`, schema in + * `src/shared/validation/schemas/combo.ts`): `judgeModel` synthesizes the final + * answer and `fusionTuning` controls the quorum-grace panel collection. The + * per-combo editor already exposes these; this surfaces the same knobs as global + * defaults. (Voting / aggregation-mode / per-provider-weight do not exist in the + * engine, so they are intentionally not shown.) + */ +export default function FusionDefaultsFields({ + comboDefaults, + setComboDefaults, +}: FusionDefaultsFieldsProps) { + const t = useTranslations("settings"); + const setTuning = (patch: Record) => + setComboDefaults((prev: any) => ({ + ...prev, + fusionTuning: { ...prev.fusionTuning, ...patch }, + })); + const num = (value: string) => (value ? Number(value) : undefined); + + return ( +
+ + setComboDefaults((prev: any) => ({ ...prev, judgeModel: e.target.value || undefined })) + } + className="text-sm md:col-span-2" + /> + setTuning({ minPanel: num(e.target.value) })} + className="text-sm" + /> + setTuning({ stragglerGraceMs: num(e.target.value) })} + className="text-sm" + /> + setTuning({ panelHardTimeoutMs: num(e.target.value) })} + className="text-sm md:col-span-2" + /> +
+

+ {translateOrFallback( + t, + "fusionDefaultsNote", + "Fusion fans out to all of a combo's models and a judge model synthesizes the final answer (defaults to the first panel model when unset). These are global defaults for new or unconfigured combos." + )} +

+
+
+ ); +} diff --git a/tests/unit/ui/combo-defaults-fusion-5598.test.tsx b/tests/unit/ui/combo-defaults-fusion-5598.test.tsx new file mode 100644 index 0000000000..b4bd0b870d --- /dev/null +++ b/tests/unit/ui/combo-defaults-fusion-5598.test.tsx @@ -0,0 +1,84 @@ +// @vitest-environment jsdom +// +// #5598 β€” Selecting the "fusion" routing strategy on the Global Routing defaults +// tab previously revealed no fusion-specific config (only the generic resilience +// fields). Fusion's engine knobs (judgeModel + fusionTuning) exist in the schema +// and the per-combo editor, but were never surfaced as global defaults. This +// asserts they now appear when fusion is the selected strategy. +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +vi.mock("next-intl", () => ({ + // identity translator with no `has`, so translateOrFallback uses the English fallbacks + useTranslations: () => (key: string) => key, +})); + +const { default: ComboDefaultsTab } = await import( + "../../../src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab" +); + +function okJson(data: unknown) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(data) } as Response); +} + +function setupFetch(strategy: string) { + vi.stubGlobal( + "fetch", + vi.fn((url: string) => { + if (String(url).includes("/combo-defaults")) return okJson({ comboDefaults: { strategy } }); + if (String(url).includes("/api/providers")) return okJson({ connections: [] }); + return okJson({}); // /api/settings + }) + ); +} + +const containers: Array<{ root: ReturnType; el: HTMLDivElement }> = []; + +function renderTab() { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => { + root.render(); + }); + containers.push({ root, el }); + return el; +} + +async function waitFor(fn: () => boolean, timeoutMs = 2000) { + const start = Date.now(); + while (!fn()) { + if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out"); + await new Promise((r) => setTimeout(r, 20)); + } +} + +afterEach(() => { + for (const { root, el } of containers.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + vi.unstubAllGlobals(); +}); + +describe("ComboDefaultsTab fusion config (#5598)", () => { + it("shows fusion-specific fields when the fusion strategy is selected", async () => { + setupFetch("fusion"); + const el = renderTab(); + // RED before the fix: fusion had no config block β†’ these labels never render. + await waitFor(() => el.textContent?.includes("Judge Model") === true); + expect(el.textContent).toContain("Judge Model"); + expect(el.textContent).toContain("Min Panel"); + expect(el.textContent).toContain("Straggler Grace (ms)"); + expect(el.textContent).toContain("Panel Hard Timeout (ms)"); + }); + + it("does not show fusion fields for a non-fusion strategy", async () => { + setupFetch("priority"); + const el = renderTab(); + // Let the component settle (load + render). + await new Promise((r) => setTimeout(r, 200)); + expect(el.textContent).not.toContain("Judge Model"); + }); +});