feat(compression): T05/C5 — preserveSystemPrompt mode enum + legacy back-compat (#5653)

Integrated into release/v3.8.43. Includes the legacy-boolean back-compat derivation so existing preserveSystemPrompt=false installs keep whenNoCache behavior.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-30 14:53:58 -03:00
committed by GitHub
parent 6a4d0d2fae
commit bb28f9554c
13 changed files with 492 additions and 31 deletions

View File

@@ -1,23 +1,36 @@
import type { CompressionConfig } from "./types.ts";
import type { CachingDetectionContext } from "./cachingAware.ts";
import { detectCachingContext, getCacheAwareStrategy } from "./cachingAware.ts";
import {
normalizePreserveSystemPromptMode,
resolvePreserveSystemPrompt,
} from "./preserveSystemPromptMode.ts";
/**
* #3890: honor the cache-aware `skipSystemPrompt` decision that
* `getCacheAwareStrategy` already computes but `selectCompressionStrategy`
* cannot return. In a caching context the system prompt is part of the
* cacheable prefix, so compressing it breaks the upstream prompt cache.
* #3890/#3955 + T05/C5: materialize the engine-facing `preserveSystemPrompt`
* boolean from the authoritative `preserveSystemPromptMode` intent, using the
* cache-aware `skipSystemPrompt` signal that `getCacheAwareStrategy` already
* computes (a caching provider — or `cache_control` — means the system prompt is
* part of the cacheable prefix, so compressing it breaks the upstream cache).
*
* This generalizes the previous hard-coded "force `true` when a cache is present
* and the operator disabled preservation" into the three modes:
* - `always` → always `true`.
* - `whenNoCache` → `true` only when a cache is present (the legacy `false`
* behaviour — preserved exactly for back-compat).
* - `never` → always `false`, even when it breaks a prompt cache.
*/
export function resolveCacheAwareConfig(
config: CompressionConfig,
body?: Record<string, unknown>,
context?: CachingDetectionContext
): CompressionConfig {
if (!body) return config;
const ctx = detectCachingContext(body, context);
const cacheAware = getCacheAwareStrategy(config.defaultMode, ctx);
if (cacheAware.skipSystemPrompt && config.preserveSystemPrompt === false) {
return { ...config, preserveSystemPrompt: true };
}
return config;
const mode = normalizePreserveSystemPromptMode(config);
// No request body → no cacheable prefix to detect; honor the mode at its no-cache baseline.
const hasCache = body
? getCacheAwareStrategy(config.defaultMode, detectCachingContext(body, context)).skipSystemPrompt
: false;
const effective = resolvePreserveSystemPrompt(mode, { hasCache });
if (effective === config.preserveSystemPrompt) return config;
return { ...config, preserveSystemPrompt: effective };
}

View File

@@ -0,0 +1,72 @@
import type { CompressionConfig, PreserveSystemPromptMode } from "./types.ts";
/**
* T05/C5 — system-prompt preservation mode.
*
* The engine-facing field is the boolean `CompressionConfig.preserveSystemPrompt`
* (truthy = skip/preserve the system prompt). Its authoritative *intent* is the
* `preserveSystemPromptMode` enum, resolved to that boolean at the cache-aware layer
* (`resolveCacheAwareConfig`), which already runs upstream in `chatCore` with the
* caching context. This module is the single source of the enum<->boolean mapping so
* the legacy boolean and the new enum can never drift.
*
* Mode semantics:
* - `always` → preserve the system prompt unconditionally.
* - `whenNoCache` → preserve it only when there is a cache to protect (provider caches
* the prefix or `cache_control` is present); compress it otherwise.
* This is exactly what the legacy `preserveSystemPrompt: false` already
* did via the #3890/#3955 cache guard.
* - `never` → compress the system prompt even when it breaks a prompt cache.
*/
export const PRESERVE_SYSTEM_PROMPT_MODES: readonly PreserveSystemPromptMode[] = [
"always",
"whenNoCache",
"never",
];
export function isPreserveSystemPromptMode(value: unknown): value is PreserveSystemPromptMode {
return (
typeof value === "string" &&
(PRESERVE_SYSTEM_PROMPT_MODES as readonly string[]).includes(value)
);
}
/**
* Back-compat shim: derive the authoritative mode from a config. An explicit
* `preserveSystemPromptMode` always wins; otherwise the legacy boolean is mapped
* 1:1 to the behaviour it already had (`false → whenNoCache`, anything else → `always`).
*/
export function normalizePreserveSystemPromptMode(
config: Pick<CompressionConfig, "preserveSystemPrompt" | "preserveSystemPromptMode">
): PreserveSystemPromptMode {
if (isPreserveSystemPromptMode(config.preserveSystemPromptMode)) {
return config.preserveSystemPromptMode;
}
return config.preserveSystemPrompt === false ? "whenNoCache" : "always";
}
/**
* Resolve a mode to the effective engine-facing boolean given whether a cacheable
* prefix is present. `true` = preserve (skip), `false` = compress the system prompt.
*/
export function resolvePreserveSystemPrompt(
mode: PreserveSystemPromptMode,
{ hasCache }: { hasCache: boolean }
): boolean {
switch (mode) {
case "always":
return true;
case "never":
return false;
case "whenNoCache":
return hasCache;
}
}
/**
* The no-cache projection of a mode — the stored/effective boolean used as a sane
* default for any reader that runs before the cache-aware layer materializes it.
*/
export function modeToBaselineBoolean(mode: PreserveSystemPromptMode): boolean {
return resolvePreserveSystemPrompt(mode, { hasCache: false });
}

View File

@@ -145,13 +145,34 @@ export interface EngineToggle {
level?: string;
}
/** T05/C5 — system-prompt preservation intent (see `CompressionConfig.preserveSystemPromptMode`). */
export type PreserveSystemPromptMode = "always" | "whenNoCache" | "never";
export interface CompressionConfig {
enabled: boolean;
defaultMode: CompressionMode;
autoTriggerMode?: CompressionMode;
autoTriggerTokens: number;
cacheMinutes: number;
/**
* Effective, engine-facing boolean: when truthy the system prompt is skipped
* (preserved, not compressed). Kept as the materialized value all engines read.
* Its authoritative *intent* is `preserveSystemPromptMode` (T05/C5); this boolean
* is the no-cache projection of that mode, refined up to `true` by
* `resolveCacheAwareConfig` when a cacheable prefix is detected.
*/
preserveSystemPrompt: boolean;
/**
* T05/C5 — authoritative system-prompt preservation intent:
* - `always`: never compress the system prompt.
* - `whenNoCache`: compress it only when there is no cache to protect
* (preserve when the provider caches or `cache_control` is present). This is the
* behaviour the legacy `preserveSystemPrompt: false` already had via the cache guard.
* - `never`: always compress the system prompt, even when it breaks a prompt cache.
* Optional/back-compat: absent → derived from the legacy boolean
* (`false → whenNoCache`, otherwise `always`).
*/
preserveSystemPromptMode?: PreserveSystemPromptMode;
mcpDescriptionCompressionEnabled?: boolean;
comboOverrides: Record<string, CompressionMode>;
compressionComboId?: string | null;
@@ -289,6 +310,7 @@ export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = {
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
preserveSystemPromptMode: "always",
mcpDescriptionCompressionEnabled: true,
comboOverrides: {},
compressionComboId: null,

View File

@@ -51,6 +51,7 @@ interface CompressionConfig {
enabled: boolean;
autoTriggerTokens: number;
preserveSystemPrompt: boolean;
preserveSystemPromptMode?: "always" | "whenNoCache" | "never";
engines: Record<string, EngineToggle>;
activeComboId: string | null;
cavemanOutputMode?: CavemanOutputModeConfig;
@@ -458,15 +459,25 @@ export default function CompressionPanel() {
</label>
<label className="flex items-center justify-between">
<span className="text-sm text-text-muted">{t("compressionPreserveSystem")}</span>
<span data-testid="preserve-system-toggle">
<Toggle
size="sm"
checked={config.preserveSystemPrompt}
onChange={(preserveSystemPrompt) => save({ preserveSystemPrompt })}
disabled={saving}
ariaLabel={t("compressionPreserveSystem")}
/>
</span>
<select
value={
config.preserveSystemPromptMode ??
(config.preserveSystemPrompt === false ? "whenNoCache" : "always")
}
onChange={(e) =>
save({
preserveSystemPromptMode: e.target.value as "always" | "whenNoCache" | "never",
})
}
disabled={saving}
aria-label={t("compressionPreserveSystem")}
data-testid="preserve-system-mode-select"
className="w-36 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main"
>
<option value="always">{t("compressionPreserveSystemAlways")}</option>
<option value="whenNoCache">{t("compressionPreserveSystemWhenNoCache")}</option>
<option value="never">{t("compressionPreserveSystemNever")}</option>
</select>
</label>
</div>
</Card>

View File

@@ -65,6 +65,7 @@ interface CompressionConfig extends CompressionTokenSaverConfig {
autoTriggerTokens: number;
cacheMinutes: number;
preserveSystemPrompt: boolean;
preserveSystemPromptMode?: "always" | "whenNoCache" | "never";
mcpDescriptionCompressionEnabled?: boolean;
comboOverrides: Record<string, CompressionMode>;
cavemanConfig?: CavemanConfig;
@@ -372,18 +373,26 @@ export default function CompressionSettingsTab() {
<label className="flex items-center justify-between">
<span className="text-sm text-text-muted">{t("compressionPreserveSystem")}</span>
<button
onClick={() => save({ preserveSystemPrompt: !config.preserveSystemPrompt })}
className={`relative w-10 h-5 rounded-full transition-colors ${
config.preserveSystemPrompt ? "bg-green-500" : "bg-border"
}`}
<select
value={
config.preserveSystemPromptMode ??
(config.preserveSystemPrompt === false ? "whenNoCache" : "always")
}
onChange={(e) =>
save({
preserveSystemPromptMode: e.target.value as
| "always"
| "whenNoCache"
| "never",
})
}
className="w-36 px-2 py-1 text-sm rounded border border-border bg-surface text-text-main"
data-testid="preserve-system-mode-select"
>
<span
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
config.preserveSystemPrompt ? "left-5" : "left-0.5"
}`}
/>
</button>
<option value="always">{t("compressionPreserveSystemAlways")}</option>
<option value="whenNoCache">{t("compressionPreserveSystemWhenNoCache")}</option>
<option value="never">{t("compressionPreserveSystemNever")}</option>
</select>
</label>
<label className="flex items-center justify-between">

View File

@@ -5635,6 +5635,9 @@
"compressionAutoTrigger": "Auto-Trigger Threshold",
"compressionCacheTTL": "Cache TTL",
"compressionPreserveSystem": "Preserve System Prompt",
"compressionPreserveSystemAlways": "Always",
"compressionPreserveSystemWhenNoCache": "When no cache",
"compressionPreserveSystemNever": "Never",
"compressionCavemanConfig": "Caveman Engine Configuration",
"compressionCavemanConfigDesc": "Fine-tune the rule-based compression engine",
"compressionCavemanPanelHint": "Its on/off and level are set in the panel:",

View File

@@ -28,6 +28,10 @@ import {
type RtkConfig,
type UltraConfig,
} from "@omniroute/open-sse/services/compression/types.ts";
import {
isPreserveSystemPromptMode,
normalizePreserveSystemPromptMode,
} from "@omniroute/open-sse/services/compression/preserveSystemPromptMode.ts";
import { maybePrewarmUltraSlmOnConfig } from "@omniroute/open-sse/services/compression/ultra.ts";
const NAMESPACE = "compression";
@@ -550,6 +554,11 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
// we derive the engines map from the legacy fields below so behavior is preserved.
let storedEngines: Record<string, EngineToggle> | null = null;
// Tracks whether an authoritative `preserveSystemPromptMode` row was persisted. When absent
// (legacy install that only stored the `preserveSystemPrompt` boolean) the mode is derived
// from that boolean below so it keeps its old behaviour instead of inheriting the new default.
let sawPreserveSystemPromptModeRow = false;
for (const row of rows) {
const record = toRecord(row);
const key = typeof record.key === "string" ? record.key : null;
@@ -587,6 +596,13 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
case "preserveSystemPrompt":
config.preserveSystemPrompt = parsed !== false;
break;
case "preserveSystemPromptMode":
// T05/C5 — authoritative intent; ignore unknown tokens (keep the default mode).
if (isPreserveSystemPromptMode(parsed)) {
config.preserveSystemPromptMode = parsed;
sawPreserveSystemPromptModeRow = true;
}
break;
case "mcpDescriptionCompressionEnabled":
config.mcpDescriptionCompressionEnabled = parsed !== false;
break;
@@ -652,6 +668,18 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
}
}
// T05/C5 back-compat: a legacy install persisted only the `preserveSystemPrompt` boolean and no
// `preserveSystemPromptMode` row. The DEFAULT spread above seeds the new `always` mode, which would
// otherwise shadow that boolean (an explicit mode wins in normalizePreserveSystemPromptMode) and
// silently flip `preserveSystemPrompt=false` installs from "compress unless cached" to "always
// preserve". When no mode row was stored, derive the authoritative mode from the boolean instead.
if (!sawPreserveSystemPromptModeRow) {
config.preserveSystemPromptMode = normalizePreserveSystemPromptMode({
preserveSystemPrompt: config.preserveSystemPrompt,
preserveSystemPromptMode: undefined,
});
}
// Engines map: prefer the stored row; otherwise derive from the legacy fields (migration 102
// backfill on the read path). Always fill EVERY id in ENGINE_IDS so the shape matches
// DEFAULT_COMPRESSION_CONFIG.

View File

@@ -210,6 +210,7 @@ export const compressionSettingsUpdateSchema = z
autoTriggerTokens: z.number().int().min(0).optional(),
cacheMinutes: z.number().int().min(1).max(60).optional(),
preserveSystemPrompt: z.boolean().optional(),
preserveSystemPromptMode: z.enum(["always", "whenNoCache", "never"]).optional(),
mcpDescriptionCompressionEnabled: z.boolean().optional(),
comboOverrides: z.record(z.string(), compressionModeSchema).optional(),
compressionComboId: z.string().trim().min(1).nullable().optional(),

View File

@@ -0,0 +1,81 @@
/**
* T05/C5 — resolveCacheAwareConfig materializes the `preserveSystemPromptMode` enum
* into the engine-facing `preserveSystemPrompt` boolean using the cache signal.
* The legacy boolean behaviour is covered by strategySelector-cache-aware.test.ts /
* compression-cache-guard-3955.test.ts (unchanged); this file covers the new enum.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { resolveCacheAwareConfig } from "../../../open-sse/services/compression/cacheAwareConfig.ts";
import type {
CompressionConfig,
PreserveSystemPromptMode,
} from "../../../open-sse/services/compression/types.ts";
function cfg(overrides: Partial<CompressionConfig> = {}): CompressionConfig {
return {
enabled: true,
defaultMode: "standard",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
...overrides,
} as CompressionConfig;
}
// A request whose prefix the upstream caches (anthropic + explicit cache_control).
const CACHING_BODY = {
messages: [{ role: "system", content: "x", cache_control: { type: "ephemeral" } }],
};
const CACHING_CTX = { provider: "anthropic", targetFormat: "claude" } as const;
// google has no prompt caching → no cacheable prefix to protect.
const NON_CACHING_BODY = { messages: [{ role: "system", content: "x" }] };
const NON_CACHING_CTX = { provider: "google" } as const;
function resolved(mode: PreserveSystemPromptMode, caching: boolean): boolean {
const out = resolveCacheAwareConfig(
cfg({ preserveSystemPromptMode: mode, preserveSystemPrompt: true }),
caching ? CACHING_BODY : NON_CACHING_BODY,
caching ? CACHING_CTX : NON_CACHING_CTX
);
return out.preserveSystemPrompt;
}
describe("T05/C5 preserveSystemPromptMode -> effective boolean", () => {
it("always: preserves regardless of cache", () => {
assert.equal(resolved("always", true), true);
assert.equal(resolved("always", false), true);
});
it("whenNoCache: preserves only when a cache is present", () => {
assert.equal(resolved("whenNoCache", true), true);
assert.equal(resolved("whenNoCache", false), false);
});
it("never: compresses the system prompt even when it would break the prompt cache", () => {
assert.equal(resolved("never", true), false);
assert.equal(resolved("never", false), false);
});
it("the explicit mode overrides a contradicting legacy boolean", () => {
// mode=never wins even though the legacy boolean asks to preserve.
const out = resolveCacheAwareConfig(
cfg({ preserveSystemPromptMode: "never", preserveSystemPrompt: true }),
CACHING_BODY,
CACHING_CTX
);
assert.equal(out.preserveSystemPrompt, false);
});
it("no body: honors the mode at its no-cache baseline", () => {
assert.equal(
resolveCacheAwareConfig(cfg({ preserveSystemPromptMode: "whenNoCache" })).preserveSystemPrompt,
false
);
assert.equal(
resolveCacheAwareConfig(cfg({ preserveSystemPromptMode: "always" })).preserveSystemPrompt,
true
);
});
});

View File

@@ -51,6 +51,7 @@ describe("getCompressionSettings", () => {
assert.equal(settings.autoTriggerTokens, 0);
assert.equal(settings.cacheMinutes, 5);
assert.equal(settings.preserveSystemPrompt, true);
assert.equal(settings.preserveSystemPromptMode, "always");
assert.deepEqual(settings.comboOverrides, {});
assert.equal(settings.ultra?.enabled, false);
assert.equal(settings.ultra?.compressionRate, 0.5);
@@ -77,6 +78,25 @@ describe("updateCompressionSettings", () => {
await updateCompressionSettings({ defaultMode: "off" } as any);
});
it("round-trips preserveSystemPromptMode and ignores unknown tokens (T05/C5)", async () => {
await updateCompressionSettings({ preserveSystemPromptMode: "never" } as any);
let settings = await getCompressionSettings();
assert.equal(settings.preserveSystemPromptMode, "never");
await updateCompressionSettings({ preserveSystemPromptMode: "whenNoCache" } as any);
settings = await getCompressionSettings();
assert.equal(settings.preserveSystemPromptMode, "whenNoCache");
// An unknown persisted token is rejected on read and falls back to the safe
// default mode ("always"), never crashing the settings load.
await updateCompressionSettings({ preserveSystemPromptMode: "garbage" } as any);
settings = await getCompressionSettings();
assert.equal(settings.preserveSystemPromptMode, "always");
// Reset
await updateCompressionSettings({ preserveSystemPromptMode: "always" } as any);
});
it("updates autoTriggerTokens", async () => {
await updateCompressionSettings({ autoTriggerTokens: 5000 } as any);
const settings = await getCompressionSettings();

View File

@@ -0,0 +1,93 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// T05/C5 back-compat regression (#5653) at the DB read path. The new DEFAULT_COMPRESSION_CONFIG
// seeds preserveSystemPromptMode='always'. getCompressionSettings() spreads that default, so a
// legacy install that persisted ONLY the `preserveSystemPrompt` boolean (no mode row) would have
// the default 'always' shadow the boolean — silently flipping `preserveSystemPrompt=false` installs
// from "compress unless cached" (whenNoCache) to "always preserve". The read path must instead
// derive the mode from the boolean when no mode row exists.
//
// core.ts freezes DATA_DIR/SQLITE_FILE into module consts at first import, so the temp DATA_DIR is
// set BEFORE any import and the db file is shared across tests. Each test isolates by wiping the
// `compression` namespace and busting the module-level TTL cache via resetDbInstance() (which hands
// out a NEW db object, so the cache — keyed by db ref — misses).
const TEMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-preserve-mode-"));
process.env.DATA_DIR = TEMP_DIR;
async function freshCompressionDb() {
const { getDbInstance, resetDbInstance } = await import("../../../src/lib/db/core.ts");
const db = getDbInstance(); // runs migrations on first call
db.prepare("DELETE FROM key_value WHERE namespace = 'compression'").run();
return { db, resetDbInstance };
}
async function readSettings() {
const { getCompressionSettings } = await import("../../../src/lib/db/compression.ts");
return getCompressionSettings();
}
test.after(async () => {
try {
const { resetDbInstance } = await import("../../../src/lib/db/core.ts");
resetDbInstance();
} catch {
/* core never loaded */
}
try {
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
} catch {
/* best-effort */
}
});
test("legacy preserveSystemPrompt=false (no mode row) derives whenNoCache", async () => {
const { db, resetDbInstance } = await freshCompressionDb();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('compression','preserveSystemPrompt','false')"
).run();
resetDbInstance(); // new db object => TTL cache miss on the next read
const cfg = await readSettings();
assert.equal(
cfg.preserveSystemPromptMode,
"whenNoCache",
"legacy preserveSystemPrompt=false must derive whenNoCache, not inherit the 'always' default"
);
// End-to-end: without a cacheable prefix, a legacy-off install must still compress the prompt.
const { resolveCacheAwareConfig } = await import(
"../../../open-sse/services/compression/cacheAwareConfig.ts"
);
assert.equal(
resolveCacheAwareConfig(cfg).preserveSystemPrompt,
false,
"legacy-off install must compress the system prompt when there is no cache"
);
});
test("fresh install (no override rows) defaults to always", async () => {
const { resetDbInstance } = await freshCompressionDb(); // wipes all compression rows
resetDbInstance();
const cfg = await readSettings();
assert.equal(cfg.preserveSystemPromptMode, "always", "fresh default mode is always");
assert.equal(cfg.preserveSystemPrompt, true, "fresh default boolean is true");
});
test("an explicit mode row wins over the legacy boolean", async () => {
const { db, resetDbInstance } = await freshCompressionDb();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('compression','preserveSystemPrompt','false')"
).run();
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('compression','preserveSystemPromptMode','\"never\"')"
).run();
resetDbInstance();
const cfg = await readSettings();
assert.equal(cfg.preserveSystemPromptMode, "never", "an explicit stored mode row wins");
});

View File

@@ -0,0 +1,78 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
PRESERVE_SYSTEM_PROMPT_MODES,
isPreserveSystemPromptMode,
normalizePreserveSystemPromptMode,
resolvePreserveSystemPrompt,
modeToBaselineBoolean,
} from "../../../open-sse/services/compression/preserveSystemPromptMode.ts";
// T05/C5 — preserveSystemPrompt boolean -> enum (always | whenNoCache | never) + back-compat shim.
test("isPreserveSystemPromptMode only accepts the three known tokens", () => {
for (const m of PRESERVE_SYSTEM_PROMPT_MODES) assert.equal(isPreserveSystemPromptMode(m), true);
for (const bad of ["", "Always", "no-cache", true, false, 1, null, undefined, {}]) {
assert.equal(isPreserveSystemPromptMode(bad), false, `${String(bad)} must be rejected`);
}
});
test("normalize: an explicit mode wins over the legacy boolean", () => {
assert.equal(
normalizePreserveSystemPromptMode({ preserveSystemPrompt: true, preserveSystemPromptMode: "never" }),
"never"
);
assert.equal(
normalizePreserveSystemPromptMode({ preserveSystemPrompt: false, preserveSystemPromptMode: "always" }),
"always"
);
});
test("normalize: the shim maps the legacy boolean 1:1 (true->always, false->whenNoCache)", () => {
// The shim is a behaviour identity: legacy `false` already meant "compress unless
// there is a cache" via the #3890/#3955 guard, i.e. exactly `whenNoCache`.
assert.equal(normalizePreserveSystemPromptMode({ preserveSystemPrompt: true }), "always");
assert.equal(normalizePreserveSystemPromptMode({ preserveSystemPrompt: false }), "whenNoCache");
});
test("normalize: an invalid mode string falls back to the legacy boolean", () => {
assert.equal(
normalizePreserveSystemPromptMode({
preserveSystemPrompt: false,
preserveSystemPromptMode: "garbage" as unknown as "always",
}),
"whenNoCache"
);
});
test("resolve: always preserves and never compresses regardless of cache", () => {
assert.equal(resolvePreserveSystemPrompt("always", { hasCache: true }), true);
assert.equal(resolvePreserveSystemPrompt("always", { hasCache: false }), true);
assert.equal(resolvePreserveSystemPrompt("never", { hasCache: true }), false);
assert.equal(resolvePreserveSystemPrompt("never", { hasCache: false }), false);
});
test("resolve: whenNoCache preserves only when a cache is present", () => {
assert.equal(resolvePreserveSystemPrompt("whenNoCache", { hasCache: true }), true);
assert.equal(resolvePreserveSystemPrompt("whenNoCache", { hasCache: false }), false);
});
test("modeToBaselineBoolean is the no-cache projection", () => {
assert.equal(modeToBaselineBoolean("always"), true);
assert.equal(modeToBaselineBoolean("whenNoCache"), false);
assert.equal(modeToBaselineBoolean("never"), false);
});
test("back-compat identity: legacy config resolves to the old cache-guard outcome", () => {
// Old behaviour: preserveSystemPrompt=false stays false without cache, forced true with cache.
const legacyFalse = { preserveSystemPrompt: false };
const mode = normalizePreserveSystemPromptMode(legacyFalse);
assert.equal(resolvePreserveSystemPrompt(mode, { hasCache: false }), false);
assert.equal(resolvePreserveSystemPrompt(mode, { hasCache: true }), true);
// Old behaviour: preserveSystemPrompt=true is always preserved.
const legacyTrue = { preserveSystemPrompt: true };
const modeT = normalizePreserveSystemPromptMode(legacyTrue);
assert.equal(resolvePreserveSystemPrompt(modeT, { hasCache: false }), true);
assert.equal(resolvePreserveSystemPrompt(modeT, { hasCache: true }), true);
});

View File

@@ -111,4 +111,34 @@ describe("CompressionSettingsTab — compression controls consolidation (T11)",
expect(container.textContent).toContain("compressionSkipRules");
expect(container.textContent).toContain("compressionPreservePatterns");
});
it("renders the preserveSystemPrompt 3-way mode select reflecting the shim (T05/C5)", async () => {
await renderTab();
const select = container.querySelector<HTMLSelectElement>(
'[data-testid="preserve-system-mode-select"]'
);
expect(select).not.toBeNull();
const values = Array.from(select!.querySelectorAll("option")).map((o) => o.value);
expect(values).toEqual(["always", "whenNoCache", "never"]);
// CONFIG has preserveSystemPrompt: true and no explicit mode → shim renders "always".
expect(select!.value).toBe("always");
});
it("saves the chosen mode via PUT (T05/C5)", async () => {
await renderTab();
const select = container.querySelector<HTMLSelectElement>(
'[data-testid="preserve-system-mode-select"]'
);
await act(async () => {
select!.value = "never";
select!.dispatchEvent(new Event("change", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
});
const putCall = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls.find(
([, init]) => (init as RequestInit | undefined)?.method === "PUT"
);
expect(putCall).toBeTruthy();
const body = JSON.parse(String((putCall![1] as RequestInit).body));
expect(body.preserveSystemPromptMode).toBe("never");
});
});