File size: 8,576 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

describe("Compression Settings API Schema Validation", () => {
  const compressionModeValues = [
    "off",
    "lite",
    "standard",
    "aggressive",
    "ultra",
    "rtk",
    "stacked",
  ];

  it("should validate all compression mode values", () => {
    assert.deepStrictEqual(compressionModeValues, [
      "off",
      "lite",
      "standard",
      "aggressive",
      "ultra",
      "rtk",
      "stacked",
    ]);
  });

  it("should validate caveman config structure", () => {
    const defaultCavemanConfig = {
      enabled: true,
      compressRoles: ["user"],
      skipRules: [],
      minMessageLength: 50,
      preservePatterns: [],
    };

    assert.equal(defaultCavemanConfig.enabled, true);
    assert.deepStrictEqual(defaultCavemanConfig.compressRoles, ["user"]);
    assert.equal(Array.isArray(defaultCavemanConfig.skipRules), true);
    assert.equal(defaultCavemanConfig.minMessageLength, 50);
    assert.equal(Array.isArray(defaultCavemanConfig.preservePatterns), true);
  });

  it("should validate full compression config structure", () => {
    const defaultConfig = {
      enabled: false,
      defaultMode: "off",
      autoTriggerTokens: 0,
      cacheMinutes: 5,
      preserveSystemPrompt: true,
      comboOverrides: {},
      cavemanConfig: {
        enabled: true,
        compressRoles: ["user"],
        skipRules: [],
        minMessageLength: 50,
        preservePatterns: [],
      },
      ultra: {
        enabled: false,
        compressionRate: 0.5,
        minScoreThreshold: 0.3,
        slmFallbackToAggressive: true,
        maxTokensPerMessage: 0,
      },
    };

    assert.equal(defaultConfig.enabled, false);
    assert.ok(compressionModeValues.includes(defaultConfig.defaultMode));
    assert.equal(typeof defaultConfig.autoTriggerTokens, "number");
    assert.equal(typeof defaultConfig.cacheMinutes, "number");
    assert.equal(typeof defaultConfig.preserveSystemPrompt, "boolean");
    assert.equal(typeof defaultConfig.comboOverrides, "object");
    assert.equal(typeof defaultConfig.cavemanConfig, "object");
    assert.equal(typeof defaultConfig.ultra, "object");
    assert.equal(defaultConfig.ultra.compressionRate, 0.5);
  });

  it("should validate all caveman compression rules are defined", async () => {
    const { CAVEMAN_RULES } =
      await import("../../../../open-sse/services/compression/cavemanRules.ts");
    assert.ok(Array.isArray(CAVEMAN_RULES));
    assert.ok(CAVEMAN_RULES.length >= 29, `Expected >= 29 rules, got ${CAVEMAN_RULES.length}`);
    for (const rule of CAVEMAN_RULES) {
      assert.ok(rule.name && typeof rule.name === "string", `Rule must have a name`);
      assert.ok(rule.pattern instanceof RegExp, `Rule ${rule.name} must have a RegExp pattern`);
      assert.ok(
        typeof rule.replacement === "string" || typeof rule.replacement === "function",
        `Rule ${rule.name} must have string or function replacement`
      );
      assert.ok(
        rule.pattern.source !== "^$" || rule.replacement !== "",
        `Rule ${rule.name} must not be a no-op (empty pattern + empty replacement)`
      );
    }
  });

  it("should validate compression modes cover all CavemanConfig roles", () => {
    const validRoles = ["user", "assistant", "system"];
    for (const role of validRoles) {
      assert.ok(validRoles.includes(role), `Role ${role} should be valid`);
    }
    assert.equal(validRoles.length, 3);
  });
});

// ─── Route round-trip: engines map + activeComboId ─────────────────────────
// Mirrors the mcp-accessibility-config test harness: allocate a temp DATA_DIR,
// import route + DB modules, tear down in after().

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-route-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../../../src/lib/db/core.ts");
const route = await import("../../../../src/app/api/settings/compression/route.ts");

function makeRequest(method: string, body?: unknown): Request {
  return new Request("http://localhost/api/settings/compression", {
    method,
    headers: body !== undefined ? { "content-type": "application/json" } : {},
    body: body !== undefined ? JSON.stringify(body) : undefined,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
  }) as any;
}

describe("settings/compression route β€” engines + activeComboId", () => {
  beforeEach(() => {
    core.resetDbInstance();
    fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
    fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
  });

  after(() => {
    core.resetDbInstance();
    fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
    if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
    else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
  });

  it("PUT engines map persists and GET returns engines + activeComboId", async () => {
    const putRes = await route.PUT(
      makeRequest("PUT", { engines: { rtk: { enabled: true, level: "standard" } } })
    );
    assert.equal(putRes.status, 200);

    // Fresh DB handle so we read from storage, not from the write-path return value.
    core.resetDbInstance();

    const getRes = await route.GET(makeRequest("GET"));
    assert.equal(getRes.status, 200);
    const body = await getRes.json();

    assert.equal(body.engines?.rtk?.enabled, true, "engines.rtk.enabled should be true after PUT");
    assert.equal(
      body.engines?.rtk?.level,
      "standard",
      "engines.rtk.level should be 'standard' after PUT"
    );
    // activeComboId is always present (null by default)
    assert.ok("activeComboId" in body, "GET response must include activeComboId");
  });

  it("PUT activeComboId persists and is returned by GET", async () => {
    const putRes = await route.PUT(makeRequest("PUT", { activeComboId: "combo-abc" }));
    assert.equal(putRes.status, 200);

    core.resetDbInstance();

    const getRes = await route.GET(makeRequest("GET"));
    assert.equal(getRes.status, 200);
    const body = await getRes.json();
    assert.equal(body.activeComboId, "combo-abc");
  });

  it("PUT activeComboId:null clears the active combo", async () => {
    // First set it, then clear.
    await route.PUT(makeRequest("PUT", { activeComboId: "combo-to-clear" }));
    core.resetDbInstance();
    await route.PUT(makeRequest("PUT", { activeComboId: null }));
    core.resetDbInstance();

    const getRes = await route.GET(makeRequest("GET"));
    assert.equal(getRes.status, 200);
    const body = await getRes.json();
    assert.equal(body.activeComboId, null);
  });

  it("PUT with invalid engines shape is rejected by schema validation (400)", async () => {
    // engines values must have an `enabled` boolean β€” passing a string should fail the schema.
    const putRes = await route.PUT(makeRequest("PUT", { engines: { rtk: { enabled: "yes" } } }));
    assert.equal(putRes.status, 400);
    const body = await putRes.json();
    // Validation failures use { error: { message, details } } via validateBody helper.
    assert.ok(body.error !== null && typeof body.error === "object", "error should be an object");
    const errorMessage: string =
      typeof body.error === "string"
        ? body.error
        : (body.error?.message ?? JSON.stringify(body.error));
    assert.ok(!errorMessage.includes("at /"), "error must not contain a stack trace");
  });

  it("PUT accepts enginesExplicit (round-tripped from GET response)", async () => {
    // Regression: the GET handler injects `enginesExplicit` (compression.ts:632) so the
    // hub/panel can round-trip the full settings object. The previous .strict() PUT schema
    // rejected it with 400 ("Unrecognized key: enginesExplicit"), causing every toggle on
    // the Compression Hub / Panel to revert. Allow it through.
    const putRes = await route.PUT(makeRequest("PUT", { enabled: true, enginesExplicit: true }));
    assert.equal(putRes.status, 200);

    core.resetDbInstance();

    const putRes2 = await route.PUT(makeRequest("PUT", { enabled: false, enginesExplicit: false }));
    assert.equal(putRes2.status, 200);
  });
});