From 981c1c12631361c2de6371b420d426df1f178f81 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 05:54:48 +0700 Subject: [PATCH 1/5] test(settings): add unit tests for debugMode and hiddenSidebarItems Tests cover: - PATCH debugMode=true/false - PATCH hiddenSidebarItems with array values - Combined updates with both fields --- tests/unit/settings-api.test.mjs | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/unit/settings-api.test.mjs diff --git a/tests/unit/settings-api.test.mjs b/tests/unit/settings-api.test.mjs new file mode 100644 index 0000000000..ff21acccdf --- /dev/null +++ b/tests/unit/settings-api.test.mjs @@ -0,0 +1,67 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { getSettings, updateSettings } from "../../src/lib/localDb.ts"; + +describe("Settings API - debugMode and hiddenSidebarItems", () => { + describe("debugMode", () => { + test("PATCH with debugMode=true succeeds", async () => { + const result = await updateSettings({ debugMode: true }); + assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + + const settings = getSettings(); + assert.strictEqual(settings.debugMode, true, "debugMode should be true"); + }); + + test("PATCH with debugMode=false succeeds", async () => { + const result = await updateSettings({ debugMode: false }); + assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + + const settings = getSettings(); + assert.strictEqual(settings.debugMode, false, "debugMode should be false"); + }); + }); + + describe("hiddenSidebarItems", () => { + test("PATCH with hiddenSidebarItems=['translator'] succeeds", async () => { + const result = await updateSettings({ hiddenSidebarItems: ["translator"] }); + assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + + const settings = getSettings(); + assert.deepStrictEqual( + settings.hiddenSidebarItems, + ["translator"], + "hiddenSidebarItems should contain translator" + ); + }); + + test("PATCH with empty hiddenSidebarItems succeeds", async () => { + const result = await updateSettings({ hiddenSidebarItems: [] }); + assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + + const settings = getSettings(); + assert.deepStrictEqual( + settings.hiddenSidebarItems, + [], + "hiddenSidebarItems should be empty array" + ); + }); + }); + + describe("combined updates", () => { + test("PATCH with both debugMode and hiddenSidebarItems succeeds", async () => { + const result = await updateSettings({ + debugMode: true, + hiddenSidebarItems: ["translator"], + }); + assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + + const settings = getSettings(); + assert.strictEqual(settings.debugMode, true, "debugMode should be true"); + assert.deepStrictEqual( + settings.hiddenSidebarItems, + ["translator"], + "hiddenSidebarItems should be updated" + ); + }); + }); +}); From 8f5c9a3c722255137ab021c48cd4fa681a745b95 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 10:54:07 +0700 Subject: [PATCH 2/5] test(e2e): add Playwright tests for settings toggles Tests cover: - Debug mode toggle on/off - Sidebar visibility toggle - Settings persistence after page reload --- tests/e2e/settings-toggles.spec.ts | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/e2e/settings-toggles.spec.ts diff --git a/tests/e2e/settings-toggles.spec.ts b/tests/e2e/settings-toggles.spec.ts new file mode 100644 index 0000000000..96ed7587d0 --- /dev/null +++ b/tests/e2e/settings-toggles.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Settings Toggles", () => { + test.describe("Debug Mode Toggle", () => { + test("should toggle debug mode on and off", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); + + const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); + + if (await debugToggle.isVisible({ timeout: 3000 }).catch(() => false)) { + const initialState = await debugToggle.isChecked(); + await debugToggle.click(); + await page.waitForTimeout(500); + const newState = await debugToggle.isChecked(); + expect(newState).not.toBe(initialState); + } + }); + }); + + test.describe("Sidebar Visibility Toggle", () => { + test("should toggle sidebar items visibility", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=General"); + + const sidebarToggle = page + .locator('[aria-label*="sidebar" i], [data-testid*="sidebar" i]') + .first(); + + if (await sidebarToggle.isVisible({ timeout: 3000 }).catch(() => false)) { + const initialState = await sidebarToggle.isChecked(); + await sidebarToggle.click(); + await page.waitForTimeout(500); + const newState = await sidebarToggle.isChecked(); + expect(newState).not.toBe(initialState); + } + }); + }); + + test.describe("Settings Persistence", () => { + test("should persist debug mode after page reload", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); + + const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); + + if (await debugToggle.isVisible({ timeout: 3000 }).catch(() => false)) { + const wasChecked = await debugToggle.isChecked(); + await debugToggle.click(); + await page.waitForTimeout(500); + await page.reload(); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); + const isChecked = await debugToggle.isChecked(); + expect(isChecked).not.toBe(wasChecked); + } + }); + }); +}); From b98d6984a184871e630c105163ec731abf745b11 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 11:15:54 +0700 Subject: [PATCH 3/5] fix(tests): address code review issues - Unit tests: fix async/await for getSettings, use direct db functions - E2E tests: remove conditional logic, use Playwright auto-waiting assertions --- tests/e2e/settings-toggles.spec.ts | 83 +++++++++++++----------------- tests/unit/settings-api.test.mjs | 32 ++++++------ 2 files changed, 52 insertions(+), 63 deletions(-) diff --git a/tests/e2e/settings-toggles.spec.ts b/tests/e2e/settings-toggles.spec.ts index 96ed7587d0..fbd4167488 100644 --- a/tests/e2e/settings-toggles.spec.ts +++ b/tests/e2e/settings-toggles.spec.ts @@ -1,62 +1,51 @@ import { test, expect } from "@playwright/test"; test.describe("Settings Toggles", () => { - test.describe("Debug Mode Toggle", () => { - test("should toggle debug mode on and off", async ({ page }) => { - await page.goto("/dashboard/settings"); - await page.waitForLoadState("networkidle"); - await page.click("text=Advanced"); + test("Debug mode toggle should work", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); - const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); + const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); - if (await debugToggle.isVisible({ timeout: 3000 }).catch(() => false)) { - const initialState = await debugToggle.isChecked(); - await debugToggle.click(); - await page.waitForTimeout(500); - const newState = await debugToggle.isChecked(); - expect(newState).not.toBe(initialState); - } - }); + await expect(debugToggle).toBeVisible({ timeout: 5000 }); + + const initialState = await debugToggle.isChecked(); + await debugToggle.click(); + await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); }); - test.describe("Sidebar Visibility Toggle", () => { - test("should toggle sidebar items visibility", async ({ page }) => { - await page.goto("/dashboard/settings"); - await page.waitForLoadState("networkidle"); - await page.click("text=General"); + test("Sidebar visibility toggle should work", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=General"); - const sidebarToggle = page - .locator('[aria-label*="sidebar" i], [data-testid*="sidebar" i]') - .first(); + const sidebarToggle = page + .locator('[aria-label*="sidebar" i], [data-testid*="sidebar" i]') + .first(); - if (await sidebarToggle.isVisible({ timeout: 3000 }).catch(() => false)) { - const initialState = await sidebarToggle.isChecked(); - await sidebarToggle.click(); - await page.waitForTimeout(500); - const newState = await sidebarToggle.isChecked(); - expect(newState).not.toBe(initialState); - } - }); + await expect(sidebarToggle).toBeVisible({ timeout: 5000 }); + + const initialState = await sidebarToggle.isChecked(); + await sidebarToggle.click(); + await expect(sidebarToggle).not.toBeChecked({ timeout: 5000 }); }); - test.describe("Settings Persistence", () => { - test("should persist debug mode after page reload", async ({ page }) => { - await page.goto("/dashboard/settings"); - await page.waitForLoadState("networkidle"); - await page.click("text=Advanced"); + test("Debug mode should persist after page reload", async ({ page }) => { + await page.goto("/dashboard/settings"); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); - const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); + const debugToggle = page.locator('[aria-label*="debug" i], [data-testid*="debug" i]').first(); - if (await debugToggle.isVisible({ timeout: 3000 }).catch(() => false)) { - const wasChecked = await debugToggle.isChecked(); - await debugToggle.click(); - await page.waitForTimeout(500); - await page.reload(); - await page.waitForLoadState("networkidle"); - await page.click("text=Advanced"); - const isChecked = await debugToggle.isChecked(); - expect(isChecked).not.toBe(wasChecked); - } - }); + await expect(debugToggle).toBeVisible({ timeout: 5000 }); + + const wasChecked = await debugToggle.isChecked(); + await debugToggle.click(); + await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); + await page.reload(); + await page.waitForLoadState("networkidle"); + await page.click("text=Advanced"); + await expect(debugToggle).not.toBeChecked({ timeout: 5000 }); }); }); diff --git a/tests/unit/settings-api.test.mjs b/tests/unit/settings-api.test.mjs index ff21acccdf..e3192e2c76 100644 --- a/tests/unit/settings-api.test.mjs +++ b/tests/unit/settings-api.test.mjs @@ -1,32 +1,32 @@ import { describe, test } from "node:test"; import assert from "node:assert/strict"; -import { getSettings, updateSettings } from "../../src/lib/localDb.ts"; +import { getSettings, updateSettings } from "../../src/lib/db/settings.ts"; describe("Settings API - debugMode and hiddenSidebarItems", () => { describe("debugMode", () => { - test("PATCH with debugMode=true succeeds", async () => { + test("updateSettings with debugMode=true succeeds", async () => { const result = await updateSettings({ debugMode: true }); - assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + assert.ok(result, "updateSettings should return truthy result"); - const settings = getSettings(); + const settings = await getSettings(); assert.strictEqual(settings.debugMode, true, "debugMode should be true"); }); - test("PATCH with debugMode=false succeeds", async () => { + test("updateSettings with debugMode=false succeeds", async () => { const result = await updateSettings({ debugMode: false }); - assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + assert.ok(result, "updateSettings should return truthy result"); - const settings = getSettings(); + const settings = await getSettings(); assert.strictEqual(settings.debugMode, false, "debugMode should be false"); }); }); describe("hiddenSidebarItems", () => { - test("PATCH with hiddenSidebarItems=['translator'] succeeds", async () => { + test("updateSettings with hiddenSidebarItems=['translator'] succeeds", async () => { const result = await updateSettings({ hiddenSidebarItems: ["translator"] }); - assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + assert.ok(result, "updateSettings should return truthy result"); - const settings = getSettings(); + const settings = await getSettings(); assert.deepStrictEqual( settings.hiddenSidebarItems, ["translator"], @@ -34,11 +34,11 @@ describe("Settings API - debugMode and hiddenSidebarItems", () => { ); }); - test("PATCH with empty hiddenSidebarItems succeeds", async () => { + test("updateSettings with empty hiddenSidebarItems succeeds", async () => { const result = await updateSettings({ hiddenSidebarItems: [] }); - assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + assert.ok(result, "updateSettings should return truthy result"); - const settings = getSettings(); + const settings = await getSettings(); assert.deepStrictEqual( settings.hiddenSidebarItems, [], @@ -48,14 +48,14 @@ describe("Settings API - debugMode and hiddenSidebarItems", () => { }); describe("combined updates", () => { - test("PATCH with both debugMode and hiddenSidebarItems succeeds", async () => { + test("updateSettings with both debugMode and hiddenSidebarItems succeeds", async () => { const result = await updateSettings({ debugMode: true, hiddenSidebarItems: ["translator"], }); - assert.strictEqual(result.ok, true, "updateSettings should return ok: true"); + assert.ok(result, "updateSettings should return truthy result"); - const settings = getSettings(); + const settings = await getSettings(); assert.strictEqual(settings.debugMode, true, "debugMode should be true"); assert.deepStrictEqual( settings.hiddenSidebarItems, From ae3d2bebbe6f8dd084898947890bb82cb739c197 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 11:21:30 +0700 Subject: [PATCH 4/5] docs: add dashboard settings toggles to CONTRIBUTING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add section documenting: - Debug Mode toggle (Settings → Advanced) - Sidebar Visibility toggle (Settings → General) --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f227558a07..c306f5894f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,17 @@ Key variables for development: | `INITIAL_PASSWORD` | `123456` | First login password | | `ENABLE_REQUEST_LOGS` | `false` | Enable debug request logs | +### Dashboard Settings + +The dashboard provides UI toggles for features that can also be configured via environment variables: + +| Setting Location | Toggle | Description | +| ------------------- | ------------------ | ------------------------------ | +| Settings → Advanced | Debug Mode | Enable debug request logs (UI) | +| Settings → General | Sidebar Visibility | Show/hide sidebar sections | + +These settings are stored in the database and persist across restarts, overriding env var defaults when set. + ### Running Locally ```bash From ac37a44ffae004525eb7e7124ead1928619e4bc1 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 11:30:03 +0700 Subject: [PATCH 5/5] fix(cache): only inject prompt_cache_key for supported providers Only inject prompt_cache_key for providers that support prompt caching (Claude, Anthropic, ZAI, Qwen, DeepSeek). This fixes issue #848 where NVIDIA API rejected the parameter. --- open-sse/handlers/chatCore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 5c3312ab87..c022af7513 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -47,7 +47,10 @@ import { } from "@/lib/localDb"; import { getExecutor } from "../executors/index.ts"; import { getCacheControlSettings } from "@/lib/cacheControlSettings"; -import { shouldPreserveCacheControl } from "../utils/cacheControlPolicy.ts"; +import { + shouldPreserveCacheControl, + providerSupportsCaching, +} from "../utils/cacheControlPolicy.ts"; import { getCacheMetrics } from "@/lib/db/settings.ts"; import { @@ -955,9 +958,10 @@ export async function handleChatCore({ ? translatedBody : { ...translatedBody, model: modelToCall }; - // Inject prompt_cache_key for OpenAI providers if not already set + // Inject prompt_cache_key only for providers that support it if ( targetFormat === FORMATS.OPENAI && + providerSupportsCaching(provider) && !bodyToSend.prompt_cache_key && Array.isArray(bodyToSend.messages) ) {