mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 05:12:16 +03:00
feat(chat-admission): expose admission tunables via dashboard settings (#12038)
* feat(chat-admission): add settings store for admission tunables * fix(chat-admission): extract parseEnvNumber to reduce cyclomatic complexity * fix(chat-admission): repair settings store write path and add coverage The settings store could not persist anything: `updateChatAdmissionSettings` targeted an `updated_at` column that `key_value` does not have (the schema is namespace/key/value — src/lib/db/core.ts), so every write threw `table key_value has no column named updated_at`. Also fixes, found while adding the tests: - `getChatAdmissionSettingsSource` returned a partial map (only the keys whose layer differed from the default) and dropped the unset keys entirely, so a dashboard reading it could not render a complete row. - env parsing used `parseFloat` for the shed ratio, so `"0.5x"` was silently accepted as 0.5 while `chatBodyAdmission.ts` rejects that same input — both paths now share one per-field predicate table. - DB reads validated `typeof === "number"` but not integrality/range, so a hand-edited row could serve `2.5` or `-1` to the admission controller. - writes persisted unvalidated input. - malformed, non-object, and partial rows are now tolerated per field. Adds tests/unit/db-chat-admission-settings.test.ts (17 cases) covering CRUD round-trips, namespace isolation, reset, env parsing/validation boundaries, env-over-DB precedence, provenance, normalization on write, and malformed-row tolerance, per Hard Rule #8. Verification: eslint clean; `npm run typecheck:core` clean; the new suite plus the two sibling settings suites pass 63/63; check-complexity-ratchets reports complexityNewCode=0; check-db-rules OK; check-env-doc-sync OK (all three vars are already documented in .env.example). --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
This commit is contained in:
211
src/lib/db/chatAdmissionSettings.ts
Normal file
211
src/lib/db/chatAdmissionSettings.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* chatAdmissionSettings.ts — Persisted settings for the chat admission controller.
|
||||
*
|
||||
* These values live in `key_value` under the `settings` namespace so they can be
|
||||
* edited from the dashboard without touching env files. Env vars still win as
|
||||
* operator overrides; DB values are the portable deployment default.
|
||||
*/
|
||||
|
||||
import { getDbInstance } from "./core";
|
||||
import { invalidateDbCache } from "./readCache";
|
||||
|
||||
const NAMESPACE = "settings";
|
||||
const SETTINGS_KEY = "chatAdmissionSettings";
|
||||
|
||||
export interface ChatAdmissionSettings {
|
||||
chatMaxHeavyInFlight: number;
|
||||
chatAdmissionHeapShedRatio: number;
|
||||
chatAdmissionHealthyHeadroom: number;
|
||||
}
|
||||
|
||||
export type ChatAdmissionSettingKey = keyof ChatAdmissionSettings;
|
||||
export type ChatAdmissionSettingSource = "env" | "db" | "default";
|
||||
|
||||
export const DEFAULT_CHAT_ADMISSION_SETTINGS: ChatAdmissionSettings = {
|
||||
chatMaxHeavyInFlight: 1,
|
||||
chatAdmissionHeapShedRatio: 0.75,
|
||||
chatAdmissionHealthyHeadroom: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Env var backing each tunable. Presence of a non-blank var is what makes the
|
||||
* operator override bind — an explicit `=1` must win over a DB value of `1` too,
|
||||
* matching `resolveLegacyCountCap()` in `chatBodyAdmission.ts`.
|
||||
*/
|
||||
const ENV_KEYS: Record<ChatAdmissionSettingKey, string> = {
|
||||
chatMaxHeavyInFlight: "OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT",
|
||||
chatAdmissionHeapShedRatio: "OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO",
|
||||
chatAdmissionHealthyHeadroom: "OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM",
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-field validity predicates, shared by the env and DB paths so the two can
|
||||
* never drift: a value a hand-edited DB row would reject must also be rejected
|
||||
* when it arrives through the environment, and vice versa.
|
||||
*/
|
||||
function isValid(key: ChatAdmissionSettingKey, value: number): boolean {
|
||||
switch (key) {
|
||||
case "chatMaxHeavyInFlight":
|
||||
return Number.isSafeInteger(value) && value >= 1;
|
||||
case "chatAdmissionHeapShedRatio":
|
||||
return Number.isFinite(value) && value > 0 && value <= 1;
|
||||
case "chatAdmissionHealthyHeadroom":
|
||||
return Number.isSafeInteger(value) && value >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Env parsing mirrors `chatBodyAdmission.ts`: `parseInt` for the integer
|
||||
* tunables (so `"5x"` reads as 5), `Number` for the ratio (so `"0.5x"` is
|
||||
* rejected rather than silently truncated to 0.5).
|
||||
*/
|
||||
function parseSetting(key: ChatAdmissionSettingKey, raw: string): number {
|
||||
return key === "chatAdmissionHeapShedRatio" ? Number(raw) : Number.parseInt(raw, 10);
|
||||
}
|
||||
|
||||
function isEnvProvided(key: ChatAdmissionSettingKey): boolean {
|
||||
const raw = process.env[ENV_KEYS[key]];
|
||||
return raw !== undefined && raw.trim() !== "";
|
||||
}
|
||||
|
||||
/** Env value for one key; the default when unset or malformed. */
|
||||
function readEnvValue(key: ChatAdmissionSettingKey): number {
|
||||
const fallback = DEFAULT_CHAT_ADMISSION_SETTINGS[key];
|
||||
if (!isEnvProvided(key)) return fallback;
|
||||
const parsed = parseSetting(key, process.env[ENV_KEYS[key]] as string);
|
||||
return isValid(key, parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
/** A field from an untrusted record, or the default when absent/out-of-range. */
|
||||
function pickSetting(source: Record<string, unknown>, key: ChatAdmissionSettingKey): number {
|
||||
const value = source[key];
|
||||
return typeof value === "number" && isValid(key, value)
|
||||
? value
|
||||
: DEFAULT_CHAT_ADMISSION_SETTINGS[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored row as a raw record, or `null` when absent, unparsable, or not a
|
||||
* JSON object. Keeps the malformed-row case distinguishable from "no row" so
|
||||
* provenance reporting does not have to guess.
|
||||
*/
|
||||
function readStoredSettings(): Record<string, unknown> | null {
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
|
||||
.get(NAMESPACE, SETTINGS_KEY) as { value?: string } | undefined;
|
||||
|
||||
if (!row?.value) return null;
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(row.value);
|
||||
return parsed !== null && typeof parsed === "object"
|
||||
? (parsed as Record<string, unknown>)
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce an untrusted settings payload (parsed DB row or caller input) into a
|
||||
* fully-validated object. Absent or out-of-range fields fall back to defaults,
|
||||
* so a bad write can never persist a value the readers would reject anyway.
|
||||
*/
|
||||
export function normalizeChatAdmissionSettings(input: unknown): ChatAdmissionSettings {
|
||||
const source =
|
||||
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
||||
return {
|
||||
chatMaxHeavyInFlight: pickSetting(source, "chatMaxHeavyInFlight"),
|
||||
chatAdmissionHeapShedRatio: pickSetting(source, "chatAdmissionHeapShedRatio"),
|
||||
chatAdmissionHealthyHeadroom: pickSetting(source, "chatAdmissionHealthyHeadroom"),
|
||||
};
|
||||
}
|
||||
|
||||
export function readChatAdmissionSettingsFromEnv(): ChatAdmissionSettings {
|
||||
return {
|
||||
chatMaxHeavyInFlight: readEnvValue("chatMaxHeavyInFlight"),
|
||||
chatAdmissionHeapShedRatio: readEnvValue("chatAdmissionHeapShedRatio"),
|
||||
chatAdmissionHealthyHeadroom: readEnvValue("chatAdmissionHealthyHeadroom"),
|
||||
};
|
||||
}
|
||||
|
||||
export function readChatAdmissionSettingsFromDb(): ChatAdmissionSettings {
|
||||
const stored = readStoredSettings();
|
||||
return stored === null ? DEFAULT_CHAT_ADMISSION_SETTINGS : normalizeChatAdmissionSettings(stored);
|
||||
}
|
||||
|
||||
/** DB value wins only where no env override is present — resolved per key. */
|
||||
export function getEffectiveChatAdmissionSettings(): ChatAdmissionSettings {
|
||||
const db = readChatAdmissionSettingsFromDb();
|
||||
return {
|
||||
chatMaxHeavyInFlight: isEnvProvided("chatMaxHeavyInFlight")
|
||||
? readEnvValue("chatMaxHeavyInFlight")
|
||||
: db.chatMaxHeavyInFlight,
|
||||
chatAdmissionHeapShedRatio: isEnvProvided("chatAdmissionHeapShedRatio")
|
||||
? readEnvValue("chatAdmissionHeapShedRatio")
|
||||
: db.chatAdmissionHeapShedRatio,
|
||||
chatAdmissionHealthyHeadroom: isEnvProvided("chatAdmissionHealthyHeadroom")
|
||||
? readEnvValue("chatAdmissionHealthyHeadroom")
|
||||
: db.chatAdmissionHealthyHeadroom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-key provenance for the dashboard: which layer supplies the effective
|
||||
* value. A key reports `"db"` only when a row exists AND that field survived
|
||||
* validation — a present-but-malformed field reports `"default"` rather than
|
||||
* claiming the DB supplies a value it does not.
|
||||
*/
|
||||
export function getChatAdmissionSettingsSource(): Record<
|
||||
ChatAdmissionSettingKey,
|
||||
ChatAdmissionSettingSource
|
||||
> {
|
||||
const stored = readStoredSettings();
|
||||
const storedSource = (key: ChatAdmissionSettingKey): ChatAdmissionSettingSource => {
|
||||
const value = stored?.[key];
|
||||
return typeof value === "number" && isValid(key, value) ? "db" : "default";
|
||||
};
|
||||
|
||||
return {
|
||||
chatMaxHeavyInFlight: isEnvProvided("chatMaxHeavyInFlight")
|
||||
? "env"
|
||||
: storedSource("chatMaxHeavyInFlight"),
|
||||
chatAdmissionHeapShedRatio: isEnvProvided("chatAdmissionHeapShedRatio")
|
||||
? "env"
|
||||
: storedSource("chatAdmissionHeapShedRatio"),
|
||||
chatAdmissionHealthyHeadroom: isEnvProvided("chatAdmissionHealthyHeadroom")
|
||||
? "env"
|
||||
: storedSource("chatAdmissionHealthyHeadroom"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the tunables and return the newly EFFECTIVE settings — when an env
|
||||
* override is present the returned value reflects the override, not `next`, so
|
||||
* the caller's response never advertises a value the runtime will not use.
|
||||
*/
|
||||
export async function updateChatAdmissionSettings(
|
||||
next: ChatAdmissionSettings
|
||||
): Promise<ChatAdmissionSettings> {
|
||||
const db = getDbInstance();
|
||||
|
||||
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
||||
NAMESPACE,
|
||||
SETTINGS_KEY,
|
||||
JSON.stringify(normalizeChatAdmissionSettings(next))
|
||||
);
|
||||
|
||||
invalidateDbCache("settings");
|
||||
return getEffectiveChatAdmissionSettings();
|
||||
}
|
||||
|
||||
/** Drop the stored override; effective settings fall back to env/defaults. */
|
||||
export async function resetChatAdmissionSettings(): Promise<ChatAdmissionSettings> {
|
||||
const db = getDbInstance();
|
||||
|
||||
db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(NAMESPACE, SETTINGS_KEY);
|
||||
|
||||
invalidateDbCache("settings");
|
||||
return getEffectiveChatAdmissionSettings();
|
||||
}
|
||||
292
tests/unit/db-chat-admission-settings.test.ts
Normal file
292
tests/unit/db-chat-admission-settings.test.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
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";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-chat-admission-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const settings = await import("../../src/lib/db/chatAdmissionSettings.ts");
|
||||
|
||||
const ENV_NAMES = [
|
||||
"OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT",
|
||||
"OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO",
|
||||
"OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM",
|
||||
] as const;
|
||||
|
||||
const DEFAULTS = settings.DEFAULT_CHAT_ADMISSION_SETTINGS;
|
||||
const SETTINGS_KEY = "chatAdmissionSettings";
|
||||
|
||||
function clearEnvOverrides() {
|
||||
for (const name of ENV_NAMES) delete process.env[name];
|
||||
}
|
||||
|
||||
function cleanupGlobalDb() {
|
||||
const holder = globalThis as { __omnirouteDb?: { open?: boolean; close?: () => void } };
|
||||
try {
|
||||
if (holder.__omnirouteDb?.open) holder.__omnirouteDb.close?.();
|
||||
} catch {
|
||||
// ignore — the instance is being discarded either way.
|
||||
}
|
||||
delete holder.__omnirouteDb;
|
||||
}
|
||||
|
||||
// `SQLITE_FILE` in src/lib/db/core.ts is a module-level constant derived from DATA_DIR at
|
||||
// import time, so every test in this file must reuse the SAME directory path — a fresh
|
||||
// mkdtemp per test would leave the DB pointed at the first one. Wipe and recreate it
|
||||
// instead; rmSync's own retry loop absorbs a transient EBUSY from the closing handle.
|
||||
function resetStorage() {
|
||||
clearEnvOverrides();
|
||||
cleanupGlobalDb();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
core.getDbInstance();
|
||||
}
|
||||
|
||||
function readRowValue(key: string): string | undefined {
|
||||
const row = core
|
||||
.getDbInstance()
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
|
||||
.get("settings", key) as { value?: string } | undefined;
|
||||
return row?.value;
|
||||
}
|
||||
|
||||
/** Writes a raw row exactly as an operator hand-editing the DB would. */
|
||||
function writeRawSettingsRow(value: string) {
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)")
|
||||
.run("settings", SETTINGS_KEY, value);
|
||||
}
|
||||
|
||||
/** The stored settings blob re-parsed, or `undefined` when no row exists. */
|
||||
function readStoredSettingsBlob(): unknown {
|
||||
const raw = readRowValue(SETTINGS_KEY);
|
||||
return raw === undefined ? undefined : JSON.parse(raw);
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
clearEnvOverrides();
|
||||
cleanupGlobalDb();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
|
||||
});
|
||||
|
||||
test("readers fall back to defaults when neither env nor DB supply a value", () => {
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromEnv(), DEFAULTS);
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS);
|
||||
assert.deepEqual(settings.getEffectiveChatAdmissionSettings(), DEFAULTS);
|
||||
});
|
||||
|
||||
test("updateChatAdmissionSettings persists to the key_value schema and round-trips", async () => {
|
||||
const next = {
|
||||
chatMaxHeavyInFlight: 7,
|
||||
chatAdmissionHeapShedRatio: 0.4,
|
||||
chatAdmissionHealthyHeadroom: 3,
|
||||
};
|
||||
|
||||
const returned = await settings.updateChatAdmissionSettings(next);
|
||||
|
||||
assert.deepEqual(returned, next);
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), next);
|
||||
assert.deepEqual(readStoredSettingsBlob(), next);
|
||||
});
|
||||
|
||||
test("updateChatAdmissionSettings leaves other settings-namespace rows untouched", async () => {
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)")
|
||||
.run("settings", "someOtherSetting", JSON.stringify("keep-me"));
|
||||
|
||||
await settings.updateChatAdmissionSettings({
|
||||
chatMaxHeavyInFlight: 2,
|
||||
chatAdmissionHeapShedRatio: 0.5,
|
||||
chatAdmissionHealthyHeadroom: 1,
|
||||
});
|
||||
|
||||
assert.equal(readRowValue("someOtherSetting"), JSON.stringify("keep-me"));
|
||||
});
|
||||
|
||||
test("updateChatAdmissionSettings normalizes out-of-range input before persisting", async () => {
|
||||
await settings.updateChatAdmissionSettings({
|
||||
chatMaxHeavyInFlight: 0,
|
||||
chatAdmissionHeapShedRatio: 2,
|
||||
chatAdmissionHealthyHeadroom: Number.NaN,
|
||||
});
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), {
|
||||
chatMaxHeavyInFlight: DEFAULTS.chatMaxHeavyInFlight,
|
||||
chatAdmissionHeapShedRatio: DEFAULTS.chatAdmissionHeapShedRatio,
|
||||
chatAdmissionHealthyHeadroom: DEFAULTS.chatAdmissionHealthyHeadroom,
|
||||
});
|
||||
});
|
||||
|
||||
test("resetChatAdmissionSettings drops the stored row and returns effective defaults", async () => {
|
||||
await settings.updateChatAdmissionSettings({
|
||||
chatMaxHeavyInFlight: 4,
|
||||
chatAdmissionHeapShedRatio: 0.25,
|
||||
chatAdmissionHealthyHeadroom: 2,
|
||||
});
|
||||
assert.notDeepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS);
|
||||
|
||||
const returned = await settings.resetChatAdmissionSettings();
|
||||
|
||||
assert.equal(readRowValue(SETTINGS_KEY), undefined);
|
||||
assert.deepEqual(returned, DEFAULTS);
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS);
|
||||
});
|
||||
|
||||
test("env parsing accepts valid values per field", () => {
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = "6";
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO = "0.5";
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM = "0";
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromEnv(), {
|
||||
chatMaxHeavyInFlight: 6,
|
||||
chatAdmissionHeapShedRatio: 0.5,
|
||||
chatAdmissionHealthyHeadroom: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("env parsing rejects malformed values per field without discarding valid siblings", () => {
|
||||
// chatMaxHeavyInFlight requires a safe integer >= 1: "Infinity" is invalid.
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = "Infinity";
|
||||
// The ratio keeps the runtime constant's strict rule: "1.2" is outside (0, 1].
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO = "1.2";
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM = "5";
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromEnv(), {
|
||||
chatMaxHeavyInFlight: DEFAULTS.chatMaxHeavyInFlight,
|
||||
chatAdmissionHeapShedRatio: DEFAULTS.chatAdmissionHeapShedRatio,
|
||||
chatAdmissionHealthyHeadroom: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test("env parsing treats blank values as unset", () => {
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = " ";
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO = "";
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEALTHY_HEADROOM = " ";
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromEnv(), DEFAULTS);
|
||||
assert.deepEqual(settings.getChatAdmissionSettingsSource(), {
|
||||
chatMaxHeavyInFlight: "default",
|
||||
chatAdmissionHeapShedRatio: "default",
|
||||
chatAdmissionHealthyHeadroom: "default",
|
||||
});
|
||||
});
|
||||
|
||||
test("env ratio boundary: 1 is accepted, 0 is rejected", () => {
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO = "1";
|
||||
assert.equal(settings.readChatAdmissionSettingsFromEnv().chatAdmissionHeapShedRatio, 1);
|
||||
|
||||
process.env.OMNIROUTE_CHAT_ADMISSION_HEAP_SHED_RATIO = "0";
|
||||
assert.equal(
|
||||
settings.readChatAdmissionSettingsFromEnv().chatAdmissionHeapShedRatio,
|
||||
DEFAULTS.chatAdmissionHeapShedRatio
|
||||
);
|
||||
});
|
||||
|
||||
test("env parsing mirrors the runtime parser for the integer tunables", () => {
|
||||
// chatBodyAdmission.ts resolves these with parseInt, so "2.5" reads as 2 there too.
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = "2.5";
|
||||
assert.equal(settings.readChatAdmissionSettingsFromEnv().chatMaxHeavyInFlight, 2);
|
||||
});
|
||||
|
||||
test("a single env override does not collapse the other keys to their defaults", async () => {
|
||||
await settings.updateChatAdmissionSettings({
|
||||
chatMaxHeavyInFlight: 3,
|
||||
chatAdmissionHeapShedRatio: 0.2,
|
||||
chatAdmissionHealthyHeadroom: 4,
|
||||
});
|
||||
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = "9";
|
||||
|
||||
assert.deepEqual(settings.getEffectiveChatAdmissionSettings(), {
|
||||
chatMaxHeavyInFlight: 9,
|
||||
chatAdmissionHeapShedRatio: 0.2,
|
||||
chatAdmissionHealthyHeadroom: 4,
|
||||
});
|
||||
assert.deepEqual(settings.getChatAdmissionSettingsSource(), {
|
||||
chatMaxHeavyInFlight: "env",
|
||||
chatAdmissionHeapShedRatio: "db",
|
||||
chatAdmissionHealthyHeadroom: "db",
|
||||
});
|
||||
});
|
||||
|
||||
test("updateChatAdmissionSettings reports the effective value when an env override is set", async () => {
|
||||
process.env.OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT = "11";
|
||||
|
||||
const returned = await settings.updateChatAdmissionSettings({
|
||||
chatMaxHeavyInFlight: 2,
|
||||
chatAdmissionHeapShedRatio: 0.6,
|
||||
chatAdmissionHealthyHeadroom: 2,
|
||||
});
|
||||
|
||||
// The response must not advertise a value the runtime will not honour.
|
||||
assert.equal(returned.chatMaxHeavyInFlight, 11);
|
||||
// The written row keeps what the operator actually asked for.
|
||||
assert.deepEqual(readStoredSettingsBlob(), {
|
||||
chatMaxHeavyInFlight: 2,
|
||||
chatAdmissionHeapShedRatio: 0.6,
|
||||
chatAdmissionHealthyHeadroom: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("readChatAdmissionSettingsFromDb tolerates unparsable and non-object rows", () => {
|
||||
for (const raw of ["{not json", "null", "42", '"a string"', "[]"]) {
|
||||
writeRawSettingsRow(raw);
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS, `row: ${raw}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("readChatAdmissionSettingsFromDb merges a partial row with defaults per field", () => {
|
||||
writeRawSettingsRow(JSON.stringify({ chatMaxHeavyInFlight: 5 }));
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), {
|
||||
chatMaxHeavyInFlight: 5,
|
||||
chatAdmissionHeapShedRatio: DEFAULTS.chatAdmissionHeapShedRatio,
|
||||
chatAdmissionHealthyHeadroom: DEFAULTS.chatAdmissionHealthyHeadroom,
|
||||
});
|
||||
});
|
||||
|
||||
test("readChatAdmissionSettingsFromDb rejects out-of-range fields in a hand-edited row", () => {
|
||||
writeRawSettingsRow(
|
||||
JSON.stringify({
|
||||
chatMaxHeavyInFlight: 2.5,
|
||||
chatAdmissionHeapShedRatio: 1.5,
|
||||
chatAdmissionHealthyHeadroom: -1,
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS);
|
||||
});
|
||||
|
||||
test("readChatAdmissionSettingsFromDb ignores non-numeric field types", () => {
|
||||
writeRawSettingsRow(
|
||||
JSON.stringify({
|
||||
chatMaxHeavyInFlight: "7",
|
||||
chatAdmissionHeapShedRatio: null,
|
||||
chatAdmissionHealthyHeadroom: true,
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(settings.readChatAdmissionSettingsFromDb(), DEFAULTS);
|
||||
});
|
||||
|
||||
test("getChatAdmissionSettingsSource reports db only for fields that survived validation", () => {
|
||||
writeRawSettingsRow(JSON.stringify({ chatMaxHeavyInFlight: 4, chatAdmissionHeapShedRatio: 9 }));
|
||||
|
||||
assert.deepEqual(settings.getChatAdmissionSettingsSource(), {
|
||||
chatMaxHeavyInFlight: "db",
|
||||
// Present in the row but out of range, so the effective value is the default.
|
||||
chatAdmissionHeapShedRatio: "default",
|
||||
chatAdmissionHealthyHeadroom: "default",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user