mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
feat(compression): T05/C5 — preserveSystemPrompt mode enum (always|whenNoCache|never)
Replace the implicit 3-state behaviour of the boolean preserveSystemPrompt with an
explicit authoritative enum, keeping the boolean as the engine-facing effective value:
- always -> never compress the system prompt (legacy true).
- whenNoCache -> compress it only when there is no cache to protect (legacy false; the
#3890/#3955 cache guard already did exactly this).
- never -> always compress the system prompt, even when it breaks a prompt cache (new).
The mode is resolved to the effective boolean at resolveCacheAwareConfig (already upstream
in chatCore with the caching context), generalizing the previous hard-coded force-true.
Back-compat shim derives the mode from the legacy boolean, so existing configs and the
cache-guard tests are behaviour-identical. Persisted in compression settings, validated by
Zod, exposed as a 3-way select in both compression panels.
New helper open-sse/services/compression/preserveSystemPromptMode.ts is the single source
of the enum<->boolean mapping. TDD: helper, cache-aware resolution incl. never, DB
round-trip, UI select render + save.
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
72
open-sse/services/compression/preserveSystemPromptMode.ts
Normal file
72
open-sse/services/compression/preserveSystemPromptMode.ts
Normal 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 });
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type RtkConfig,
|
||||
type UltraConfig,
|
||||
} from "@omniroute/open-sse/services/compression/types.ts";
|
||||
import { isPreserveSystemPromptMode } from "@omniroute/open-sse/services/compression/preserveSystemPromptMode.ts";
|
||||
import { maybePrewarmUltraSlmOnConfig } from "@omniroute/open-sse/services/compression/ultra.ts";
|
||||
|
||||
const NAMESPACE = "compression";
|
||||
@@ -587,6 +588,12 @@ 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;
|
||||
}
|
||||
break;
|
||||
case "mcpDescriptionCompressionEnabled":
|
||||
config.mcpDescriptionCompressionEnabled = parsed !== false;
|
||||
break;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
81
tests/unit/compression/cache-aware-preserve-mode.test.ts
Normal file
81
tests/unit/compression/cache-aware-preserve-mode.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
78
tests/unit/compression/preserve-system-prompt-mode.test.ts
Normal file
78
tests/unit/compression/preserve-system-prompt-mode.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user