From ad5da814b398804cdf0b1c566209cc57a521b1c8 Mon Sep 17 00:00:00 2001 From: Lance Woodson <65194841+woodsonl@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:22:19 -0500 Subject: [PATCH 1/2] fix(db): merge partial cavemanOutputMode writes with the stored row updateCompressionSettings stored each cavemanOutputMode write as the whole JSON row, and getCompressionSettings fills missing fields with defaults, so a partial write reset every field it left out. {"autoClarity":false} also turned legacy output mode off and dropped intensity to "lite". {"enabled":true} reset a stored autoClarity false to true and intensity to "lite". The PUT /api/settings/compression schema makes all three fields optional, so the route accepts both bodies as-is. The write path now reads the stored row inside the same transaction and takes each missing or invalid field from it, the way mergeLiteSettingsForWrite handles lite. An empty {} write leaves the row unchanged. Like getCompressionSettings, the merge treats a non-text (BLOB) stored row as absent. Six regression cases in tests/unit/compression/db.test.ts cover both reported writes, fields set back to their defaults, the empty write, invalid fields, a partially populated stored row, and non-text, unparseable, or null stored rows. A route test in tests/unit/api/compression/compression-api.test.ts sends the reported partial PUT body. --- src/lib/db/compression.ts | 37 ++++-- .../api/compression/compression-api.test.ts | 26 +++- tests/unit/compression/db.test.ts | 114 ++++++++++++++++++ 3 files changed, 165 insertions(+), 12 deletions(-) diff --git a/src/lib/db/compression.ts b/src/lib/db/compression.ts index f95cb8813198..0c2575b8d5b2 100644 --- a/src/lib/db/compression.ts +++ b/src/lib/db/compression.ts @@ -114,22 +114,19 @@ function normalizeCavemanConfig(value: unknown): CavemanConfig { }; } -function normalizeCavemanOutputModeConfig(value: unknown): CavemanOutputModeConfig { +function normalizeCavemanOutputModeConfig( + value: unknown, + fallback: CavemanOutputModeConfig = DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG +): CavemanOutputModeConfig { const record = toRecord(value); return { - ...DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, - enabled: - typeof record.enabled === "boolean" - ? record.enabled - : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.enabled, + enabled: typeof record.enabled === "boolean" ? record.enabled : fallback.enabled, intensity: record.intensity === "lite" || record.intensity === "full" || record.intensity === "ultra" ? record.intensity - : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.intensity, + : fallback.intensity, autoClarity: - typeof record.autoClarity === "boolean" - ? record.autoClarity - : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.autoClarity, + typeof record.autoClarity === "boolean" ? record.autoClarity : fallback.autoClarity, }; } @@ -566,6 +563,22 @@ function mergeLiteSettingsForWrite( }; } +// Partial cavemanOutputMode writes replace the whole JSON row, and the read path fills +// missing fields with defaults. Take each missing or invalid field from the stored row instead. +function mergeCavemanOutputModeForWrite( + db: ReturnType, + value: unknown +): CavemanOutputModeConfig { + const existingRow = db + .prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?") + .get(NAMESPACE, "cavemanOutputMode") as { value: unknown } | undefined; + // getCompressionSettings skips non-text (BLOB) rows, so treat them as absent here too. + const existing = normalizeCavemanOutputModeConfig( + typeof existingRow?.value === "string" ? parseJsonSafe(existingRow.value) : undefined + ); + return normalizeCavemanOutputModeConfig(value, existing); +} + // Read the stored `engines` JSON row, keeping only well-formed `{enabled, level?}` entries for // known engine ids. Returns null when no usable row exists so the caller falls back to deriving // the map from the legacy fields (B-backfill, migration 102). @@ -928,6 +941,10 @@ export async function updateCompressionSettings( insert.run(NAMESPACE, key, JSON.stringify(mergeLiteSettingsForWrite(db, value))); continue; } + if (key === "cavemanOutputMode") { + insert.run(NAMESPACE, key, JSON.stringify(mergeCavemanOutputModeForWrite(db, value))); + continue; + } insert.run(NAMESPACE, key, JSON.stringify(value)); } }); diff --git a/tests/unit/api/compression/compression-api.test.ts b/tests/unit/api/compression/compression-api.test.ts index 90c08a7601b4..6617832d4224 100644 --- a/tests/unit/api/compression/compression-api.test.ts +++ b/tests/unit/api/compression/compression-api.test.ts @@ -123,7 +123,7 @@ describe("Compression Settings API Schema Validation", () => { }); }); -// ─── Route round-trip: engines map + activeComboId ───────────────────────── +// ─── Route round-trip: engines map, activeComboId, cavemanOutputMode ─────── // Mirrors the mcp-accessibility-config test harness: allocate a temp DATA_DIR, // import route + DB modules, tear down in after(). @@ -136,7 +136,7 @@ function makeRequest(method: string, body?: unknown): Request { }) as any; } -describe("settings/compression route — engines + activeComboId", () => { +describe("settings/compression route — engines, activeComboId, cavemanOutputMode", () => { beforeEach(() => { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); @@ -225,4 +225,26 @@ describe("settings/compression route — engines + activeComboId", () => { const putRes2 = await route.PUT(makeRequest("PUT", { enabled: false, enginesExplicit: false })); assert.equal(putRes2.status, 200); }); + + it("PUT a partial cavemanOutputMode keeps the stored fields it omits", async () => { + await route.PUT( + makeRequest("PUT", { + cavemanOutputMode: { enabled: true, intensity: "ultra", autoClarity: true }, + }) + ); + core.resetDbInstance(); + + const putRes = await route.PUT( + makeRequest("PUT", { cavemanOutputMode: { autoClarity: false } }) + ); + assert.equal(putRes.status, 200); + core.resetDbInstance(); + + const body = await (await route.GET(makeRequest("GET"))).json(); + assert.deepEqual(body.cavemanOutputMode, { + enabled: true, + intensity: "ultra", + autoClarity: false, + }); + }); }); diff --git a/tests/unit/compression/db.test.ts b/tests/unit/compression/db.test.ts index 621612998b5c..5e8ea9c196e0 100644 --- a/tests/unit/compression/db.test.ts +++ b/tests/unit/compression/db.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import type { CavemanOutputModeConfig } from "../../../open-sse/services/compression/types.ts"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-db-")); const ORIGINAL_DATA_DIR = process.env.DATA_DIR; @@ -121,6 +122,119 @@ describe("updateCompressionSettings", () => { assert.equal(settings.lite?.maxToolLength, undefined); }); + describe("a partial cavemanOutputMode write", () => { + const writeThenRead = async (cavemanOutputMode: Partial) => { + core.resetDbInstance(); + await updateCompressionSettings({ cavemanOutputMode } as Parameters< + typeof updateCompressionSettings + >[0]); + core.resetDbInstance(); + return (await getCompressionSettings()).cavemanOutputMode; + }; + const seedStoredRow = (value: unknown) => + core + .getDbInstance() + .prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)") + .run("compression", "cavemanOutputMode", value); + + it("keeps the stored enabled and intensity when it only sends autoClarity", async () => { + await updateCompressionSettings({ + cavemanOutputMode: { enabled: true, intensity: "ultra", autoClarity: true }, + }); + assert.deepEqual(await writeThenRead({ autoClarity: false }), { + enabled: true, + intensity: "ultra", + autoClarity: false, + }); + }); + + it("keeps a stored autoClarity false when it only sends enabled", async () => { + await updateCompressionSettings({ + cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: false }, + }); + assert.deepEqual(await writeThenRead({ enabled: true }), { + enabled: true, + intensity: "full", + autoClarity: false, + }); + }); + + it("applies each sent field over the stored row, and an empty write changes nothing", async () => { + await updateCompressionSettings({ + cavemanOutputMode: { enabled: true, intensity: "ultra", autoClarity: false }, + }); + assert.deepEqual(await writeThenRead({ enabled: false }), { + enabled: false, + intensity: "ultra", + autoClarity: false, + }); + assert.deepEqual(await writeThenRead({ autoClarity: true }), { + enabled: false, + intensity: "ultra", + autoClarity: true, + }); + assert.deepEqual(await writeThenRead({ enabled: true, intensity: "lite" }), { + enabled: true, + intensity: "lite", + autoClarity: true, + }); + assert.deepEqual(await writeThenRead({}), { + enabled: true, + intensity: "lite", + autoClarity: true, + }); + }); + + it("takes each present-but-invalid field from the stored row", async () => { + await updateCompressionSettings({ + cavemanOutputMode: { enabled: true, intensity: "ultra", autoClarity: false }, + }); + const invalid = { enabled: "yes", intensity: "mega", autoClarity: 1 }; + assert.deepEqual( + await writeThenRead(invalid as unknown as Partial), + { + enabled: true, + intensity: "ultra", + autoClarity: false, + } + ); + }); + + it("keeps the valid fields of a partially populated stored row", async () => { + seedStoredRow(JSON.stringify({ intensity: "ultra", autoClarity: false })); + assert.deepEqual(await writeThenRead({ enabled: true }), { + enabled: true, + intensity: "ultra", + autoClarity: false, + }); + }); + + it("treats a non-text, unparseable, or null stored row as absent", async () => { + seedStoredRow( + Buffer.from(JSON.stringify({ enabled: true, intensity: "ultra", autoClarity: true })) + ); + assert.deepEqual(await writeThenRead({ autoClarity: false }), { + enabled: false, + intensity: "lite", + autoClarity: false, + }); + + seedStoredRow("{not valid json"); + assert.deepEqual(await writeThenRead({ enabled: true }), { + enabled: true, + intensity: "lite", + autoClarity: true, + }); + + seedStoredRow("null"); + assert.deepEqual(await writeThenRead({ autoClarity: false }), { + enabled: false, + intensity: "lite", + autoClarity: false, + }); + }); + }); + it("updates defaultMode", async () => { await updateCompressionSettings({ defaultMode: "lite" } as any); const settings = await getCompressionSettings(); From af4ae6ea7f28bc29b236d5ab794782c6c3eb771c Mon Sep 17 00:00:00 2001 From: Lance Woodson <65194841+woodsonl@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:28:03 -0500 Subject: [PATCH 2/2] docs(changelog): add the #15206 fragment --- changelog.d/fixes/15206-caveman-output-mode-partial-write.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/15206-caveman-output-mode-partial-write.md diff --git a/changelog.d/fixes/15206-caveman-output-mode-partial-write.md b/changelog.d/fixes/15206-caveman-output-mode-partial-write.md new file mode 100644 index 000000000000..5a9b02408385 --- /dev/null +++ b/changelog.d/fixes/15206-caveman-output-mode-partial-write.md @@ -0,0 +1 @@ +- **fix(db):** a partial `cavemanOutputMode` settings write keeps the stored values of the fields it leaves out, so sending only `autoClarity` or only `enabled` leaves the other two as they were ([#15206](https://github.com/diegosouzapw/OmniRoute/pull/15206)) — thanks @woodsonl