From 91ae52320b71867fc00c2224ea7931cdff2ddb57 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Sat, 6 Jun 2026 22:37:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E6=89=A9=E5=B1=95=20Settings?= =?UTF-8?q?Data=20=E5=8A=A0=20compact=20=E5=AD=97=E6=AE=B5,=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E8=A7=A3=E6=9E=90=E4=B8=8E=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/db/settings.ts | 7 +-- src/server/routes/settings.ts | 4 ++ src/shared/api.ts | 1 + tests/server/db/settings.test.ts | 54 ++++++++++++++++++---- tests/server/routes/settings.test.ts | 68 ++++++++++++++++++++++++---- 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/src/server/db/settings.ts b/src/server/db/settings.ts index 5483e6e..79c7ae5 100644 --- a/src/server/db/settings.ts +++ b/src/server/db/settings.ts @@ -19,16 +19,17 @@ export function getSettings(raw: Database): SettingsData { .get(); if (!row) { - return { theme: "system" }; + return { compact: false, theme: "system" }; } try { const parsed = JSON.parse(row.data) as Partial; return { + compact: typeof parsed.compact === "boolean" ? parsed.compact : false, theme: parsed.theme === "dark" || parsed.theme === "light" || parsed.theme === "system" ? parsed.theme : "system", }; } catch { - return { theme: "system" }; + return { compact: false, theme: "system" }; } } @@ -40,7 +41,7 @@ export function updateSettings(raw: Database, data: Partial, _logg .where(and(eq(settings.id, SETTINGS_ID), notDeleted(settings))) .get(); - let currentData: SettingsData = { theme: "system" }; + let currentData: SettingsData = { compact: false, theme: "system" }; if (existing) { try { currentData = JSON.parse(existing.data) as SettingsData; diff --git a/src/server/routes/settings.ts b/src/server/routes/settings.ts index 0d282b0..d123001 100644 --- a/src/server/routes/settings.ts +++ b/src/server/routes/settings.ts @@ -32,6 +32,10 @@ export async function handleUpdateSettings( return jsonResponse(createApiError("theme 仅支持 dark、light、system", 400), { mode, status: 400 }); } + if (body.compact !== undefined && typeof body.compact !== "boolean") { + return jsonResponse(createApiError("compact 必须为布尔值", 400), { mode, status: 400 }); + } + const result = updateSettings(db, body, logger); logger.info({ data: result }, "设置已更新"); return jsonResponse(result, { mode }); diff --git a/src/shared/api.ts b/src/shared/api.ts index 5a0ed3f..8a56ccd 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -238,6 +238,7 @@ export type ProviderType = "anthropic" | "openai" | "openai-compatible"; export type RuntimeMode = "development" | "production" | "test"; export interface SettingsData { + compact?: boolean; theme: ThemePreference; } diff --git a/tests/server/db/settings.test.ts b/tests/server/db/settings.test.ts index 43c1cee..d750d9d 100644 --- a/tests/server/db/settings.test.ts +++ b/tests/server/db/settings.test.ts @@ -20,17 +20,17 @@ describe("设置数据访问层", () => { test("getSettings 无数据时返回默认值", () => { withSettingsDb((db) => { const result = getSettings(db); - expect(result).toEqual({ theme: "system" }); + expect(result).toEqual({ compact: false, theme: "system" }); }); }); test("updateSettings 写入并读取", () => { withSettingsDb((db) => { const updated = updateSettings(db, { theme: "dark" }, createNoopLogger()); - expect(updated).toEqual({ theme: "dark" }); + expect(updated).toEqual({ compact: false, theme: "dark" }); const read = getSettings(db); - expect(read).toEqual({ theme: "dark" }); + expect(read).toEqual({ compact: false, theme: "dark" }); }); }); @@ -38,7 +38,7 @@ describe("设置数据访问层", () => { withSettingsDb((db) => { updateSettings(db, { theme: "dark" }, createNoopLogger()); const result = updateSettings(db, { theme: "light" }, createNoopLogger()); - expect(result).toEqual({ theme: "light" }); + expect(result).toEqual({ compact: false, theme: "light" }); }); }); @@ -48,7 +48,7 @@ describe("设置数据访问层", () => { "INSERT INTO settings (id, created_at, updated_at, data) VALUES ('default', '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z', 'not-json')", ); const result = getSettings(db); - expect(result).toEqual({ theme: "system" }); + expect(result).toEqual({ compact: false, theme: "system" }); }); }); @@ -58,7 +58,7 @@ describe("设置数据访问层", () => { "INSERT INTO settings (id, created_at, updated_at, data) VALUES ('default', '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z', '{\"theme\":\"unknown\"}')", ); const result = getSettings(db); - expect(result).toEqual({ theme: "system" }); + expect(result).toEqual({ compact: false, theme: "system" }); }); }); @@ -66,8 +66,8 @@ describe("设置数据访问层", () => { withSettingsDb((db) => { const a = updateSettings(db, { theme: "dark" }, createNoopLogger()); const b = updateSettings(db, { theme: "dark" }, createNoopLogger()); - expect(a).toEqual({ theme: "dark" }); - expect(b).toEqual({ theme: "dark" }); + expect(a).toEqual({ compact: false, theme: "dark" }); + expect(b).toEqual({ compact: false, theme: "dark" }); const row = db .query("SELECT COUNT(*) as cnt FROM settings WHERE id = 'default' AND deleted_at IS NULL") @@ -75,4 +75,42 @@ describe("设置数据访问层", () => { expect(row.cnt).toBe(1); }); }); + + test("getSettings 无 compact 字段时默认 false", () => { + withSettingsDb((db) => { + db.run( + "INSERT INTO settings (id, created_at, updated_at, data) VALUES ('default', '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z', '{\"theme\":\"dark\"}')", + ); + const result = getSettings(db); + expect(result).toEqual({ compact: false, theme: "dark" }); + }); + }); + + test("updateSettings 写入 compact 并读取", () => { + withSettingsDb((db) => { + const updated = updateSettings(db, { compact: true }, createNoopLogger()); + expect(updated).toEqual({ compact: true, theme: "system" }); + + const read = getSettings(db); + expect(read).toEqual({ compact: true, theme: "system" }); + }); + }); + + test("updateSettings compact 与 theme 合并", () => { + withSettingsDb((db) => { + updateSettings(db, { theme: "dark" }, createNoopLogger()); + const result = updateSettings(db, { compact: true }, createNoopLogger()); + expect(result).toEqual({ compact: true, theme: "dark" }); + }); + }); + + test("getSettings compact 为非布尔值时回退 false", () => { + withSettingsDb((db) => { + db.run( + "INSERT INTO settings (id, created_at, updated_at, data) VALUES ('default', '2024-01-01T00:00:00.000Z', '2024-01-01T00:00:00.000Z', '{\"theme\":\"dark\",\"compact\":\"yes\"}')", + ); + const result = getSettings(db); + expect(result).toEqual({ compact: false, theme: "dark" }); + }); + }); }); diff --git a/tests/server/routes/settings.test.ts b/tests/server/routes/settings.test.ts index 325eaca..0fafc25 100644 --- a/tests/server/routes/settings.test.ts +++ b/tests/server/routes/settings.test.ts @@ -36,8 +36,8 @@ describe("设置 API 路由", () => { const req = new Request("http://localhost/api/settings"); const res = await getSettingsViaHandler(req, db); expect(res.status).toBe(200); - const body = (await res.json()) as { theme: string }; - expect(body).toEqual({ theme: "system" }); + const body = (await res.json()) as { compact: boolean; theme: string }; + expect(body).toEqual({ compact: false, theme: "system" }); }); }); @@ -50,14 +50,14 @@ describe("设置 API 路由", () => { }); const putRes = await updateSettingsViaHandler(putReq, db); expect(putRes.status).toBe(200); - const putBody = (await putRes.json()) as { theme: string }; - expect(putBody).toEqual({ theme: "dark" }); + const putBody = (await putRes.json()) as { compact: boolean; theme: string }; + expect(putBody).toEqual({ compact: false, theme: "dark" }); const getReq = new Request("http://localhost/api/settings"); const getRes = await getSettingsViaHandler(getReq, db); expect(getRes.status).toBe(200); - const getBody = (await getRes.json()) as { theme: string }; - expect(getBody).toEqual({ theme: "dark" }); + const getBody = (await getRes.json()) as { compact: boolean; theme: string }; + expect(getBody).toEqual({ compact: false, theme: "dark" }); }); }); @@ -77,8 +77,8 @@ describe("设置 API 路由", () => { }); const res2 = await updateSettingsViaHandler(req2, db); expect(res2.status).toBe(200); - const body = (await res2.json()) as { theme: string }; - expect(body).toEqual({ theme: "light" }); + const body = (await res2.json()) as { compact: boolean; theme: string }; + expect(body).toEqual({ compact: false, theme: "light" }); }); }); @@ -139,8 +139,56 @@ describe("设置 API 路由", () => { }); const res = await updateSettingsViaHandler(req, db); expect(res.status).toBe(200); - const body = (await res.json()) as { theme: string }; - expect(body).toEqual({ theme: "system" }); + const body = (await res.json()) as { compact: boolean; theme: string }; + expect(body).toEqual({ compact: false, theme: "system" }); + }); + }); + + test("PUT /api/settings compact 为非布尔值返回 400", async () => { + await withRouteDb(async (db) => { + const req = new Request("http://localhost/api/settings", { + body: JSON.stringify({ compact: "true" }), + headers: { "Content-Type": "application/json" }, + method: "PUT", + }); + const res = await updateSettingsViaHandler(req, db); + expect(res.status).toBe(400); + }); + }); + + test("PUT /api/settings compact=true 合法", async () => { + await withRouteDb(async (db) => { + const req = new Request("http://localhost/api/settings", { + body: JSON.stringify({ compact: true }), + headers: { "Content-Type": "application/json" }, + method: "PUT", + }); + const res = await updateSettingsViaHandler(req, db); + expect(res.status).toBe(200); + const body = (await res.json()) as { compact: boolean; theme: string }; + expect(body.compact).toBe(true); + expect(body.theme).toBe("system"); + }); + }); + + test("PUT /api/settings compact 与 theme 合并持久化", async () => { + await withRouteDb(async (db) => { + const req1 = new Request("http://localhost/api/settings", { + body: JSON.stringify({ theme: "dark" }), + headers: { "Content-Type": "application/json" }, + method: "PUT", + }); + await updateSettingsViaHandler(req1, db); + + const req2 = new Request("http://localhost/api/settings", { + body: JSON.stringify({ compact: true }), + headers: { "Content-Type": "application/json" }, + method: "PUT", + }); + const res2 = await updateSettingsViaHandler(req2, db); + expect(res2.status).toBe(200); + const body = (await res2.json()) as { compact: boolean; theme: string }; + expect(body).toEqual({ compact: true, theme: "dark" }); }); }); });