-
-
- {t("compressionCavemanConfig")}
-
-
- {t("compressionCavemanConfigDesc")}
-
-
-
- save({
- cavemanConfig: {
- ...config.cavemanConfig!,
- enabled: !config.cavemanConfig!.enabled,
- },
- })
- }
- className={`relative w-10 h-5 rounded-full transition-colors ${
- config.cavemanConfig.enabled ? "bg-green-500" : "bg-border"
- }`}
- >
-
-
+ {/* Engine on/off is owned by the single-source panel (/dashboard/context/settings):
+ the panel's `engines.caveman.enabled` is authoritative (planResolution.ts). This tab
+ keeps only the advanced caveman tuning the panel does not expose. */}
+
+
+ {t("compressionCavemanConfig")}
+
+
+ {t("compressionCavemanConfigDesc")} {t("compressionCavemanPanelHint")}{" "}
+ /dashboard/context/settings
+
- {config.cavemanConfig.enabled && (
- <>
+ <>
{t("compressionRoles")}
@@ -534,8 +516,7 @@ export default function CompressionSettingsTab() {
className="w-full min-h-[80px] px-3 py-2 text-sm rounded-lg border border-border bg-surface text-text-main font-mono resize-y"
/>
- >
- )}
+ >
)}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 12a34a0ab4..8029041ba5 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -5637,6 +5637,7 @@
"compressionPreserveSystem": "Preserve System Prompt",
"compressionCavemanConfig": "Caveman Engine Configuration",
"compressionCavemanConfigDesc": "Fine-tune the rule-based compression engine",
+ "compressionCavemanPanelHint": "Its on/off and level are set in the panel:",
"compressionRoles": "Compress Message Roles",
"compressionRoleUser": "User",
"compressionRoleAssistant": "Assistant",
diff --git a/tests/unit/compression-settings-tab-consolidation.test.tsx b/tests/unit/compression-settings-tab-consolidation.test.tsx
new file mode 100644
index 0000000000..3f3f730081
--- /dev/null
+++ b/tests/unit/compression-settings-tab-consolidation.test.tsx
@@ -0,0 +1,115 @@
+// @vitest-environment jsdom
+import React from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// next-intl → echo the key so we can assert on stable identifiers.
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock("next/link", () => ({
+ default: ({ children, href }: { children: React.ReactNode; href: string }) => (
+
{children}
+ ),
+}));
+
+vi.mock("@/shared/components", () => ({
+ Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
+
+ {children}
+
+ ),
+ Button: ({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }) => (
+
{children}
+ ),
+}));
+
+const CONFIG = {
+ enabled: true,
+ defaultMode: "standard",
+ autoTriggerTokens: 0,
+ cacheMinutes: 5,
+ preserveSystemPrompt: true,
+ comboOverrides: {},
+ cavemanConfig: {
+ enabled: true,
+ compressRoles: ["user"],
+ skipRules: [],
+ minMessageLength: 50,
+ preservePatterns: [],
+ intensity: "full",
+ },
+ cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
+ rtkConfig: { enabled: true, intensity: "standard" },
+};
+
+let container: HTMLDivElement;
+let root: ReturnType
| undefined;
+
+beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ vi.stubGlobal(
+ "fetch",
+ vi.fn((url: string) => {
+ const u = String(url);
+ if (u.includes("/api/settings/compression")) {
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CONFIG) });
+ }
+ if (u.includes("/api/compression/rules")) {
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ rules: [] }) });
+ }
+ return Promise.resolve({ ok: false, json: () => Promise.resolve(null) });
+ })
+ );
+});
+
+afterEach(() => {
+ act(() => root?.unmount());
+ root = undefined;
+ container.remove();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+async function renderTab() {
+ const { default: CompressionSettingsTab } = await import(
+ "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab"
+ );
+ await act(async () => {
+ root = createRoot(container);
+ root.render( );
+ });
+ // Drain the on-mount fetch → json → setState microtask chain.
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+}
+
+describe("CompressionSettingsTab — compression controls consolidation (T11)", () => {
+ it("renders the read-only TokenSaver summary that links to the unified panel", async () => {
+ await renderTab();
+ const hrefs = Array.from(container.querySelectorAll("a")).map((a) => a.getAttribute("href"));
+ expect(hrefs).toContain("/dashboard/context/settings");
+ expect(container.textContent).toContain("tokenSaverTitle");
+ });
+
+ it("does not render a duplicate caveman engine on/off toggle (panel owns on/off)", async () => {
+ await renderTab();
+ const note = container.querySelector('[data-testid="caveman-panel-note"]');
+ expect(note).not.toBeNull();
+ // The note points users to the single-source panel...
+ expect(note?.textContent).toContain("/dashboard/context/settings");
+ // ...and the caveman header no longer carries its own enable toggle button.
+ expect(note?.querySelector("button")).toBeNull();
+ });
+
+ it("keeps the advanced caveman tuning the panel does not expose", async () => {
+ await renderTab();
+ expect(container.textContent).toContain("compressionRoleUser");
+ expect(container.textContent).toContain("compressionSkipRules");
+ expect(container.textContent).toContain("compressionPreservePatterns");
+ });
+});