From c23cf63b120cd65270748029642fec8491cae5c6 Mon Sep 17 00:00:00 2001 From: Brandon Bennett Date: Mon, 10 Aug 2026 08:04:36 -0700 Subject: [PATCH] feat(flags): activation UX - env-wins adaptive virtual-lanes flag + env docs (#9654 Wave 2) U7: make adaptive virtual admission lanes discoverable + activatable. - New OMNIROUTE_CHAT_VIRTUAL_LANES feature flag (boolean/runtime/requiresRestart) in featureFlagDefinitions + en.json i18n key. - lib/admissionVirtualLanes.ts: env-wins resolver (env > DB > default) + boot warm folding a DB-sourced override into the process-global runtime env via reloadAdaptiveAdmissionRuntime(options.env) - no process.env mutation, no open-sse changes. Env still wins; DB toggle gates at next boot. - GET /api/settings/feature-flags special-cases the flag to report the gate true source (ccDiscoveryAliases precedent); flagPayload helper dedupes the payload shape. - Wire the warm into instrumentation-node registerNodejs (non-fatal, DB-ready). - Document the master switch in .env.example + ENVIRONMENT.md with the system-1/system-2 distinction; zero new env-doc-sync drift. - 11 new tests (resolver precedence + warm); 60/60 across feature-flag suites; typecheck core clean; ESLint + doc gates green. --- .env.example | 6 + docs/reference/ENVIRONMENT.md | 2 + src/app/api/settings/feature-flags/route.ts | 95 ++++++++----- src/i18n/messages/en.json | 1 + src/instrumentation-node.ts | 28 ++++ src/lib/admissionVirtualLanes.ts | 91 ++++++++++++ .../constants/featureFlagDefinitions.ts | 12 ++ .../unit/admission-virtual-lanes-flag.test.ts | 132 ++++++++++++++++++ .../feature-flags-route-virtual-lanes.test.ts | 127 +++++++++++++++++ tests/unit/feature-flags-settings.test.ts | 2 +- 10 files changed, 460 insertions(+), 36 deletions(-) create mode 100644 src/lib/admissionVirtualLanes.ts create mode 100644 tests/unit/admission-virtual-lanes-flag.test.ts create mode 100644 tests/unit/feature-flags-route-virtual-lanes.test.ts diff --git a/.env.example b/.env.example index d415d6b3c7..cf03c1420c 100644 --- a/.env.example +++ b/.env.example @@ -393,6 +393,12 @@ ALLOW_API_KEY_REVEAL=false # OMNIROUTE_CHAT_VIRTUAL_TTL_MS=60000 # Per-connection virtual admission lanes (#9654): max concurrent sessions (lanes). Default 64. # OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS=64 +# Adaptive runtime virtual admission lanes (#9654): master switch for the per-tenant +# adaptive gate (system 2, open-sse/services/admission). NOTE: the TTL/MAX_SESSIONS +# vars above tune the byte-level per-connection lanes (system 1); this switch enables +# the adaptive runtime lanes. Dashboard feature flag of the same name; env wins over +# the dashboard override; restart required. Default: off. +# OMNIROUTE_CHAT_VIRTUAL_LANES=1 # Hard cap (bytes) for a non-streaming upstream response buffered fully into memory # (#5152). Past this the upstream reader is cancelled and the request fails fast diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 3cfb6e4700..2a4c88b06e 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1501,9 +1501,11 @@ These settings were introduced after the previous environment-contract snapshot. | Variable | Default | Source File | Description | | --- | --- | --- | --- | | `OMNIROUTE_CHAT_ADMISSION_QUEUE_MS` | `2000` | `src/shared/middleware/chatBodyAdmission.ts` | Maximum wait for a heavyweight chat admission slot before a retryable `503`; a short bounded wait serializes agent bursts instead of an instant `503`. `0` restores immediate rejection. | +<<<<<<< HEAD | `OMNIROUTE_CHAT_ADMISSION_MAX_QUEUED_BYTES` | `4194304` (4 MB) | `src/shared/middleware/chatBodyAdmission.ts` | Queued-bytes budget for the admission wait: bounds total buffered body bytes parked process-wide so the wait cannot amplify the heap (#4380). Over-budget waits receive a retryable `503` immediately. | | `OMNIROUTE_CHAT_VIRTUAL_TTL_MS` | `60000` (60 s) | `src/shared/middleware/chatBodyAdmission.ts` | Deprecated no-op since #10110: per-session admission lanes were removed in favor of one process-wide budget. Accepted for configuration compatibility; ignored. | | `OMNIROUTE_CHAT_VIRTUAL_MAX_SESSIONS` | `64` | `src/shared/middleware/chatBodyAdmission.ts` | Deprecated no-op since #10110: per-session admission lanes were removed in favor of one process-wide budget. Accepted for configuration compatibility; ignored. | +| `OMNIROUTE_CHAT_VIRTUAL_LANES` | `0` (off) | `open-sse/services/admission/runtime.ts` | Adaptive runtime virtual admission lanes (#9654): master switch for the per-tenant adaptive gate (system 2). Distinct from the deprecated per-connection lane vars above (TTL_MS / MAX_SESSIONS, no-ops since #10110). Dashboard feature flag of the same name; the env var wins over the dashboard override; requires restart. | | `OMNIROUTE_RUNNOW_TIMEOUT_MS` | `30000` | `src/app/api/jobs/[id]/run-now/route.ts` | Bounds how long a run-now call waits for an in-flight job before starting the queued run. | | `ADOBE_FIREFLY_BROWSER_REFRESH` | enabled | `open-sse/services/adobeFireflySession.ts` | Keeps IMS and browser-risk state fresh through account-scoped Chrome CDP sessions; set `0` to disable. | | `ADOBE_FIREFLY_SESSION_DISK` | enabled | `open-sse/services/adobeFireflySession.ts` | Persists repaired Adobe sessions under `DATA_DIR`; set `0` for memory-only state. | diff --git a/src/app/api/settings/feature-flags/route.ts b/src/app/api/settings/feature-flags/route.ts index 2f974ea8f0..a08fec2ec7 100644 --- a/src/app/api/settings/feature-flags/route.ts +++ b/src/app/api/settings/feature-flags/route.ts @@ -2,7 +2,14 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { isAuthenticated } from "@/shared/utils/apiAuth"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { FEATURE_FLAG_DEFINITIONS } from "@/shared/constants/featureFlagDefinitions"; +import { + FEATURE_FLAG_DEFINITIONS, + type FeatureFlagDefinition, +} from "@/shared/constants/featureFlagDefinitions"; +import { + ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, + resolveAdaptiveVirtualLanesFlag, +} from "@/lib/admissionVirtualLanes"; import { getFeatureFlagOverrides, setFeatureFlagOverride, @@ -20,6 +27,31 @@ function isActive(value: string): boolean { return ACTIVE_VALUES.has(value); } +/** + * Standard feature-flag payload shape for GET /api/settings/feature-flags. + * Flags whose gate resolves differently from the generic db-wins-over-env + * order pass their own effectiveValue/source (see the special cases below). + */ +function flagPayload( + definition: FeatureFlagDefinition, + effectiveValue: string, + source: "db" | "env" | "default" +) { + return { + key: definition.key, + label: definition.label, + description: definition.description, + category: definition.category, + type: definition.type, + enumValues: definition.enumValues ?? null, + defaultValue: definition.defaultValue, + effectiveValue, + source, + requiresRestart: definition.requiresRestart, + warningLevel: definition.warningLevel, + }; +} + /** * GET /api/settings/feature-flags * Returns all feature flags with their effective values and a summary. @@ -33,41 +65,22 @@ export async function GET(request: NextRequest) { const resolved = resolveAllFeatureFlags(); const flags = resolved.map(({ key, effectiveValue, source, definition }) => { - // EXPOSE_CC_DISCOVERY_ALIASES resolves with env-wins-over-db precedence - // (see db/ccDiscoveryAliases.ts::getCcAliasGlobalState) — the opposite of - // resolveAllFeatureFlags' generic db-wins-over-env order. Override the - // reported effectiveValue/source with the gate's own resolution so the - // dashboard never shows a source that doesn't match actual gate behavior. + // Flags whose gate resolves with env-wins-over-db precedence (the + // opposite of resolveAllFeatureFlags' generic db-wins-over-env order) + // report the gate's own resolution so the dashboard never shows a source + // that doesn't match actual gate behavior. if (key === CC_DISCOVERY_ALIASES_FLAG_KEY) { const gateState = getCcAliasGlobalState(); - return { - key, - label: definition.label, - description: definition.description, - category: definition.category, - type: definition.type, - enumValues: definition.enumValues ?? null, - defaultValue: definition.defaultValue, - effectiveValue: gateState.enabled ? "true" : "false", - source: gateState.source, - requiresRestart: definition.requiresRestart, - warningLevel: definition.warningLevel, - }; + return flagPayload(definition, gateState.enabled ? "true" : "false", gateState.source); } - - return { - key, - label: definition.label, - description: definition.description, - category: definition.category, - type: definition.type, - enumValues: definition.enumValues ?? null, - defaultValue: definition.defaultValue, - effectiveValue, - source, - requiresRestart: definition.requiresRestart, - warningLevel: definition.warningLevel, - }; + // #9654 U7: the adaptive virtual-lanes gate reads env at runtime + // construction — env wins over any DB override, and a DB override gates + // at next boot (see lib/admissionVirtualLanes.ts). + if (key === ADAPTIVE_VIRTUAL_LANES_FLAG_KEY) { + const state = resolveAdaptiveVirtualLanesFlag(); + return flagPayload(definition, state.enabled ? "true" : "false", state.source); + } + return flagPayload(definition, effectiveValue, source); }); const total = flags.length; @@ -152,10 +165,22 @@ export async function PUT(request: NextRequest) { const newEffectiveValue = updatedFlag?.effectiveValue ?? definition.defaultValue; const newSource = updatedFlag?.source ?? "default"; + // Env-wins gates resolve with env > DB > default (the opposite of the + // generic db-wins helper above), so report their true resolution here too — + // the response must never tell an operator they enabled a gate that the env + // var still overrides (#9654 U7). + let reportedEffectiveValue = newEffectiveValue; + let reportedSource = newSource; + if (key === ADAPTIVE_VIRTUAL_LANES_FLAG_KEY) { + const state = resolveAdaptiveVirtualLanesFlag(); + reportedEffectiveValue = state.enabled ? "true" : "false"; + reportedSource = state.source; + } + return NextResponse.json({ key, - effectiveValue: newEffectiveValue, - source: newSource, + effectiveValue: reportedEffectiveValue, + source: reportedSource, previousValue, previousSource, requiresRestart: definition.requiresRestart, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 90291a11d9..98ffdc9291 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -980,6 +980,7 @@ "featureFlagOmnirouteEmergencyFallbackDescription": "Route budget-exhausted requests to the emergency free fallback provider/model.", "featureFlagArenaEloSyncEnabledDescription": "Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings.", "featureFlagExposeCcDiscoveryAliasesDescription": "Advertise claude/<provider>/<model> mirror ids on /v1/models so Claude Code gateway model discovery lists non-Claude models. Warning: doubles catalog entries for all clients when enabled globally.", + "featureFlagChatVirtualLanesEnabledDescription": "Enable per-tenant adaptive virtual admission lanes for provider dispatch (#9654): one tenant's burst no longer 503s another. The OMNIROUTE_CHAT_VIRTUAL_LANES env var wins over this dashboard override; changes take effect at server restart.", "sidebar": { "home": "Home", "dashboard": "Dashboard", diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index b3f885c93f..ad8988c9ed 100755 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -242,6 +242,33 @@ export async function scanComboModelNameCollisionsAtBoot(): Promise { } } +/** + * #9654 U7: fold a dashboard DB toggle for the adaptive virtual-lanes flag into + * the process-global admission runtime's env at boot. Env-wins: no-op when the + * operator's OMNIROUTE_CHAT_VIRTUAL_LANES env var is set (the lazy runtime + * already reads process.env correctly). The runtime reads env only at + * construction, so this must run before the first request touches it — hence + * awaited here, after ensureDbReadyForBoot(). Non-fatal. + * + * Exported (rather than inline in registerNodejs()) so it can be unit tested + * directly without exercising the rest of the startup sequence. + */ +export async function warmAdaptiveVirtualLanesIntoRuntime(): Promise { + try { + const { warmAdaptiveVirtualLanesIntoRuntime: warm } = + await import("@/lib/admissionVirtualLanes"); + const materialized = await warm(); + if (materialized) { + console.log( + "[STARTUP] Adaptive virtual lanes flag materialized from dashboard override (#9654)" + ); + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.warn("[STARTUP] Could not warm adaptive virtual lanes flag (non-fatal):", msg); + } +} + export async function registerNodejs(): Promise { markServerStarting(); @@ -295,6 +322,7 @@ export async function registerNodejs(): Promise { } await scanComboModelNameCollisionsAtBoot(); + await warmAdaptiveVirtualLanesIntoRuntime(); const [ { initGracefulShutdown }, diff --git a/src/lib/admissionVirtualLanes.ts b/src/lib/admissionVirtualLanes.ts new file mode 100644 index 0000000000..07bf146232 --- /dev/null +++ b/src/lib/admissionVirtualLanes.ts @@ -0,0 +1,91 @@ +/** + * Adaptive virtual admission lanes (#9654) — src-side activation surface. + * + * The adaptive gate itself (`open-sse/services/admission/runtime.ts`) reads + * `OMNIROUTE_CHAT_VIRTUAL_LANES` from env at process-global construction. This + * module is the activation layer that lives where the DB is reachable: + * + * - `resolveAdaptiveVirtualLanesFlag` resolves the flag with **env-wins** + * precedence (env > DB override > default) — the OPPOSITE of the generic + * `resolveFeatureFlag` (db-wins), for the same reason as + * `getCcAliasGlobalState`: the runtime gate reads env directly, so the + * dashboard must report the source that matches actual gate behavior. + * - `warmAdaptiveVirtualLanesIntoRuntime` (called from the Node boot path) + * folds a DB-sourced override into the process-global runtime's env at + * construction time, so a dashboard toggle actually gates after restart. + * No-op when env/default — the lazy runtime already reads process.env. + * + * Env truthiness mirrors the runtime read (`runtime.ts`): only `"1"` or + * `"true"` enable; any other set value is explicitly off (and still wins). + */ +import { getFeatureFlagOverride } from "@/lib/db/featureFlags"; + +export const ADAPTIVE_VIRTUAL_LANES_FLAG_KEY = "OMNIROUTE_CHAT_VIRTUAL_LANES"; + +export type AdaptiveVirtualLanesFlagState = { + enabled: boolean; + source: "env" | "db" | "default"; +}; + +export type AdaptiveVirtualLanesFlagDeps = { + env?: NodeJS.ProcessEnv; + getOverride?: (key: string) => string | undefined; +}; + +const ENV_ON = new Set(["1", "true"]); + +/** + * Resolve the effective state of the adaptive virtual-lanes flag and where it + * came from. Env wins over the DB override (env-wins), matching the runtime + * gate's own env-only read. + */ +export function resolveAdaptiveVirtualLanesFlag( + deps: AdaptiveVirtualLanesFlagDeps = {} +): AdaptiveVirtualLanesFlagState { + const env = deps.env ?? process.env; + const envValue = env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]; + if (envValue !== undefined && envValue !== "") { + return { enabled: ENV_ON.has(envValue), source: "env" }; + } + + const getOverride = deps.getOverride ?? getFeatureFlagOverride; + const dbOverride = getOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY); + if (dbOverride !== undefined) { + const enabled = dbOverride === "true" || dbOverride === "1" || dbOverride === "yes"; + return { enabled, source: "db" }; + } + + return { enabled: false, source: "default" }; +} + +export type AdaptiveVirtualLanesWarmDeps = { + resolve?: (deps?: AdaptiveVirtualLanesFlagDeps) => AdaptiveVirtualLanesFlagState; + reload?: (options: { env?: NodeJS.ProcessEnv }) => unknown; +}; + +/** + * Boot warm (#9654 U7): when the DB override is the effective source, fold it + * into the process-global adaptive admission runtime's env so the dashboard + * toggle actually gates — the runtime reads env only at construction, hence + * `requiresRestart: true`. Returns true when it materialized, false when + * env/default already cover the state. Never throws. + */ +export async function warmAdaptiveVirtualLanesIntoRuntime( + deps: AdaptiveVirtualLanesWarmDeps = {} +): Promise { + const resolve = deps.resolve ?? resolveAdaptiveVirtualLanesFlag; + const state = resolve(); + if (state.source !== "db") return false; + + const reload = + deps.reload ?? + (await import("@omniroute/open-sse/services/admission/runtime.ts")) + .reloadAdaptiveAdmissionRuntime; + reload({ + env: { + ...(process.env ?? {}), + [ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]: state.enabled ? "1" : "0", + }, + }); + return true; +} diff --git a/src/shared/constants/featureFlagDefinitions.ts b/src/shared/constants/featureFlagDefinitions.ts index ed7e74c652..0854498516 100644 --- a/src/shared/constants/featureFlagDefinitions.ts +++ b/src/shared/constants/featureFlagDefinitions.ts @@ -457,6 +457,18 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [ requiresRestart: false, warningLevel: "info", }, + { + key: "OMNIROUTE_CHAT_VIRTUAL_LANES", + label: "Adaptive Virtual Admission Lanes", + description: + "Enable per-tenant adaptive virtual admission lanes for provider dispatch (#9654): one tenant's burst no longer 503s another. The OMNIROUTE_CHAT_VIRTUAL_LANES env var wins over this dashboard override; changes take effect at server restart.", + descriptionI18nKey: "featureFlagChatVirtualLanesEnabledDescription", + category: "runtime", + defaultValue: "false", + type: "boolean", + requiresRestart: true, + warningLevel: "info", + }, { key: "EXPOSE_FUNCTIONAL_GATEWAY_MIRRORS", label: "Functional Gateway Mirrors", diff --git a/tests/unit/admission-virtual-lanes-flag.test.ts b/tests/unit/admission-virtual-lanes-flag.test.ts new file mode 100644 index 0000000000..3b62dab254 --- /dev/null +++ b/tests/unit/admission-virtual-lanes-flag.test.ts @@ -0,0 +1,132 @@ +/** + * U7 (#9654 Wave 2): adaptive virtual-lanes feature flag — env-wins resolution + * and boot warm. + * + * Contract under test: + * - resolveAdaptiveVirtualLanesFlag: env (`"1"`|`"true"`) > DB override > + * default(false); env wins even when set to an explicit "off" value. + * - warmAdaptiveVirtualLanesIntoRuntime: only a DB-sourced state folds into + * the runtime env (`"1"`/`"0"`); env/default sources are no-ops. + * + * Run: bun test tests/unit/admission-virtual-lanes-flag.test.ts + */ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, + resolveAdaptiveVirtualLanesFlag, + warmAdaptiveVirtualLanesIntoRuntime, +} from "../../src/lib/admissionVirtualLanes.ts"; + +const emptyEnv = {}; +const noOverride = (): string | undefined => undefined; + +describe("resolveAdaptiveVirtualLanesFlag", () => { + it('env "1" enables and reports env, winning over a DB override', () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: { [ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]: "1" }, + getOverride: () => "false", + }); + assert.deepEqual(state, { enabled: true, source: "env" }); + }); + + it('env "true" enables (runtime-compatible truthiness)', () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: { [ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]: "true" }, + getOverride: noOverride, + }); + assert.deepEqual(state, { enabled: true, source: "env" }); + }); + + it('env "0" is an explicit off that still wins over the DB', () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: { [ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]: "0" }, + getOverride: () => "true", + }); + assert.deepEqual(state, { enabled: false, source: "env" }); + }); + + it("DB override enables when env is absent", () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: emptyEnv, + getOverride: () => "true", + }); + assert.deepEqual(state, { enabled: true, source: "db" }); + }); + + it('DB override "1" enables when env is absent', () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: emptyEnv, + getOverride: () => "1", + }); + assert.deepEqual(state, { enabled: true, source: "db" }); + }); + + it("DB override disables when env is absent", () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: emptyEnv, + getOverride: () => "false", + }); + assert.deepEqual(state, { enabled: false, source: "db" }); + }); + + it("defaults to disabled when neither env nor DB is set", () => { + const state = resolveAdaptiveVirtualLanesFlag({ + env: emptyEnv, + getOverride: noOverride, + }); + assert.deepEqual(state, { enabled: false, source: "default" }); + }); +}); + +describe("warmAdaptiveVirtualLanesIntoRuntime", () => { + it('folds a DB-sourced enable into the runtime env as "1"', async () => { + let reloaded = false; + let foldedEnv: NodeJS.ProcessEnv | undefined; + const materialized = await warmAdaptiveVirtualLanesIntoRuntime({ + resolve: () => ({ enabled: true, source: "db" }), + reload: (options) => { + reloaded = true; + foldedEnv = options.env; + }, + }); + + assert.equal(materialized, true); + assert.equal(reloaded, true, "must reload the runtime when the DB is the source"); + assert.equal(foldedEnv?.[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY], "1"); + }); + + it('folds a DB-sourced disable into the runtime env as "0"', async () => { + let foldedEnv: NodeJS.ProcessEnv | undefined; + const materialized = await warmAdaptiveVirtualLanesIntoRuntime({ + resolve: () => ({ enabled: false, source: "db" }), + reload: (options) => { + foldedEnv = options.env; + }, + }); + + assert.equal(materialized, true); + assert.equal(foldedEnv?.[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY], "0"); + }); + + it("no-op when the source is env (operator env wins, nothing to fold)", async () => { + const materialized = await warmAdaptiveVirtualLanesIntoRuntime({ + resolve: () => ({ enabled: true, source: "env" }), + reload: () => { + throw new Error("must not reload when env is the source"); + }, + }); + assert.equal(materialized, false); + }); + + it("no-op when the source is default", async () => { + const materialized = await warmAdaptiveVirtualLanesIntoRuntime({ + resolve: () => ({ enabled: false, source: "default" }), + reload: () => { + throw new Error("must not reload when the default applies"); + }, + }); + assert.equal(materialized, false); + }); +}); diff --git a/tests/unit/feature-flags-route-virtual-lanes.test.ts b/tests/unit/feature-flags-route-virtual-lanes.test.ts new file mode 100644 index 0000000000..1332a9cc28 --- /dev/null +++ b/tests/unit/feature-flags-route-virtual-lanes.test.ts @@ -0,0 +1,127 @@ +/** + * U7 (#9654 Wave 2) — route-level acceptance for the adaptive virtual-lanes flag. + * + * Ticket acceptance: "flag appears in GET /api/settings/feature-flags; env + * still wins." Exercises the GET + PUT handlers directly (JWT cookie auth), + * asserting the env-wins source reporting and the requiresRestart surface. + * + * Run: bun test tests/unit/feature-flags-route-virtual-lanes.test.ts + */ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test, { after, before } from "node:test"; +import { SignJWT } from "jose"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ff-vl-route-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { GET, PUT } = await import("../../src/app/api/settings/feature-flags/route.ts"); +const { removeFeatureFlagOverride, setFeatureFlagOverride } = + await import("../../src/lib/db/featureFlags"); +const { ADAPTIVE_VIRTUAL_LANES_FLAG_KEY } = await import("../../src/lib/admissionVirtualLanes.ts"); + +const ORIGINAL_ENV_VALUE = process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]; + +type FlagPayload = { + key: string; + label: string; + type: string; + defaultValue: string; + effectiveValue: string; + source: string; + requiresRestart: boolean; +}; + +async function authCookie(): Promise { + process.env.JWT_SECRET = "test-feature-flags-route-secret"; + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + const token = await new SignJWT({ sub: "test-user" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .sign(secret); + return `auth_token=${token}`; +} + +async function buildGetRequest(): Promise { + const cookie = await authCookie(); + return new Request("http://localhost/api/settings/feature-flags", { + headers: { cookie }, + }); +} + +async function buildPutRequest(value: string): Promise { + const cookie = await authCookie(); + return new Request("http://localhost/api/settings/feature-flags", { + method: "PUT", + headers: { cookie, "Content-Type": "application/json" }, + body: JSON.stringify({ key: ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, value }), + }); +} + +async function getFlag(): Promise { + const res = await GET(await buildGetRequest()); + assert.equal(res.status, 200); + const json = (await res.json()) as { flags: FlagPayload[] }; + const flag = json.flags.find((f) => f.key === ADAPTIVE_VIRTUAL_LANES_FLAG_KEY); + assert.ok(flag, `flag ${ADAPTIVE_VIRTUAL_LANES_FLAG_KEY} must appear in GET`); + return flag; +} + +before(() => { + removeFeatureFlagOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY); +}); + +after(() => { + if (ORIGINAL_ENV_VALUE === undefined) { + delete process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]; + } else { + process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY] = ORIGINAL_ENV_VALUE; + } + removeFeatureFlagOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY); +}); + +test("flag appears in GET with the requiresRestart boolean surface (default off)", async () => { + delete process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]; + const flag = await getFlag(); + assert.equal(flag.type, "boolean"); + assert.equal(flag.defaultValue, "false"); + assert.equal(flag.requiresRestart, true, "runtime reads env at construction — restart required"); + assert.equal(flag.effectiveValue, "false"); + assert.equal(flag.source, "default"); +}); + +test('env wins over a DB override in GET (env "1" + DB false -> env)', async () => { + process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY] = "1"; + setFeatureFlagOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, "false"); + const flag = await getFlag(); + assert.equal(flag.effectiveValue, "true"); + assert.equal(flag.source, "env"); +}); + +test('env explicit off still wins in GET (env "0" + DB true -> env off)', async () => { + process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY] = "0"; + setFeatureFlagOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, "true"); + const flag = await getFlag(); + assert.equal(flag.effectiveValue, "false"); + assert.equal(flag.source, "env"); +}); + +test("DB override enables when env is absent (source db)", async () => { + delete process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY]; + setFeatureFlagOverride(ADAPTIVE_VIRTUAL_LANES_FLAG_KEY, "true"); + const flag = await getFlag(); + assert.equal(flag.effectiveValue, "true"); + assert.equal(flag.source, "db"); +}); + +test("PUT response reports env-wins truth when env is set (operator toggle cannot lie)", async () => { + process.env[ADAPTIVE_VIRTUAL_LANES_FLAG_KEY] = "0"; + const res = await PUT(await buildPutRequest("true")); + assert.equal(res.status, 200); + const json = (await res.json()) as { effectiveValue: string; source: string }; + assert.equal(json.effectiveValue, "false", 'env "0" must still win over a dashboard PUT "true"'); + assert.equal(json.source, "env"); +}); diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index 8458edf857..044b82deda 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -36,7 +36,7 @@ const EXPECTED_FEATURE_FLAG_COUNT = 48; // Test group 1 — Flag definitions registry // ────────────────────────────────────────────────────── describe("featureFlagDefinitions", () => { - it("has exactly 47 flag definitions", () => { + it("has exactly 48 flag definitions", () => { assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT); });