feat(advanced): FASE-07 to FASE-09 advanced features

FASE-07 — UX & Microinteractions:
- Create notificationStore.js (Zustand global toast store)
- Create NotificationToast.js (glassmorphism toast UI with ARIA)

FASE-08 — LLM Proxy Advanced:
- Create policyEngine.js (declarative routing/budget/access policies)
- Create cacheLayer.js (LRU cache with content hashing and TTL)

FASE-09 — E2E Flow Hardening:
- Create streamState.js (SSE stream state machine with TTFB tracking)

Tests: 23/23 advanced tests pass (75/75 total across all suites)
This commit is contained in:
diegosouzapw
2026-02-14 18:28:55 -03:00
parent 1cbbc33f20
commit 2178e99da7
6 changed files with 1140 additions and 0 deletions

180
src/lib/cacheLayer.js Normal file
View File

@@ -0,0 +1,180 @@
/**
* LRU Cache Layer — FASE-08 LLM Proxy Advanced
*
* In-memory LRU cache for LLM prompt/response pairs.
* Uses content hashing for cache keys to handle semantic deduplication.
*
* @module lib/cacheLayer
*/
import crypto from "node:crypto";
/**
* @typedef {Object} CacheEntry
* @property {string} key - Cache key (hash)
* @property {*} value - Cached value
* @property {number} createdAt - Timestamp
* @property {number} ttl - TTL in ms
* @property {number} size - Approximate size in bytes
* @property {number} hits - Number of times this entry was accessed
*/
export class LRUCache {
/** @type {Map<string, CacheEntry>} */
#cache = new Map();
#maxSize;
#defaultTTL;
#currentSize = 0;
#stats = { hits: 0, misses: 0, evictions: 0 };
/**
* @param {Object} options
* @param {number} [options.maxSize=100] - Max number of entries
* @param {number} [options.defaultTTL=300000] - Default TTL in ms (5 min)
*/
constructor(options = {}) {
this.#maxSize = options.maxSize ?? 100;
this.#defaultTTL = options.defaultTTL ?? 300000;
}
/**
* Generate a cache key from input.
* @param {Object} params - Parameters to hash
* @returns {string} Cache key
*/
static generateKey(params) {
const normalized = JSON.stringify(params, Object.keys(params).sort());
return crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 16);
}
/**
* Get a value from the cache.
* @param {string} key
* @returns {*|undefined}
*/
get(key) {
const entry = this.#cache.get(key);
if (!entry) {
this.#stats.misses++;
return undefined;
}
// Check TTL
if (Date.now() - entry.createdAt > entry.ttl) {
this.#cache.delete(key);
this.#currentSize--;
this.#stats.misses++;
return undefined;
}
// Move to end (most recently used)
this.#cache.delete(key);
entry.hits++;
this.#cache.set(key, entry);
this.#stats.hits++;
return entry.value;
}
/**
* Set a value in the cache.
* @param {string} key
* @param {*} value
* @param {number} [ttl] - Override default TTL
*/
set(key, value, ttl) {
// If key exists, delete it first (will be re-added at end)
if (this.#cache.has(key)) {
this.#cache.delete(key);
this.#currentSize--;
}
// Evict oldest entries if at capacity
while (this.#currentSize >= this.#maxSize) {
const oldestKey = this.#cache.keys().next().value;
this.#cache.delete(oldestKey);
this.#currentSize--;
this.#stats.evictions++;
}
const entry = {
key,
value,
createdAt: Date.now(),
ttl: ttl ?? this.#defaultTTL,
size: JSON.stringify(value).length,
hits: 0,
};
this.#cache.set(key, entry);
this.#currentSize++;
}
/**
* Check if a key exists (without promoting it).
* @param {string} key
* @returns {boolean}
*/
has(key) {
const entry = this.#cache.get(key);
if (!entry) return false;
if (Date.now() - entry.createdAt > entry.ttl) {
this.#cache.delete(key);
this.#currentSize--;
return false;
}
return true;
}
/**
* Delete a specific key.
* @param {string} key
* @returns {boolean}
*/
delete(key) {
if (this.#cache.has(key)) {
this.#cache.delete(key);
this.#currentSize--;
return true;
}
return false;
}
/** Clear the entire cache. */
clear() {
this.#cache.clear();
this.#currentSize = 0;
}
/** @returns {{ size: number, maxSize: number, hits: number, misses: number, evictions: number, hitRate: number }} */
getStats() {
const total = this.#stats.hits + this.#stats.misses;
return {
size: this.#currentSize,
maxSize: this.#maxSize,
...this.#stats,
hitRate: total > 0 ? (this.#stats.hits / total) * 100 : 0,
};
}
}
// ─── Prompt Cache Singleton ─────────────────
let promptCache;
/**
* Get the global prompt cache instance.
* @param {Object} [options]
* @returns {LRUCache}
*/
export function getPromptCache(options) {
if (!promptCache) {
promptCache = new LRUCache({
maxSize: parseInt(process.env.PROMPT_CACHE_MAX_SIZE || "200", 10),
defaultTTL: parseInt(process.env.PROMPT_CACHE_TTL_MS || "600000", 10),
...options,
});
}
return promptCache;
}

View File

@@ -0,0 +1,182 @@
/**
* Policy Engine — FASE-08 LLM Proxy Advanced
*
* Declarative policy engine for routing, budget, and access control decisions
* in the LLM proxy pipeline. Policies are evaluated before provider selection.
*
* @module lib/policies/policyEngine
*/
/**
* @typedef {'routing'|'budget'|'access'} PolicyType
*/
/**
* @typedef {Object} Policy
* @property {string} id - Unique policy ID
* @property {string} name - Display name
* @property {PolicyType} type - Policy type
* @property {boolean} enabled - Whether the policy is active
* @property {number} priority - Evaluation order (lower = first)
* @property {Object} conditions - Matching conditions
* @property {string} [conditions.model_pattern] - Glob pattern for model names
* @property {string} [conditions.provider] - Provider ID
* @property {string} [conditions.api_key_id] - API key ID
* @property {Object} actions - Actions to take when matched
* @property {string[]} [actions.prefer_provider] - Preferred providers
* @property {string[]} [actions.block_provider] - Blocked providers
* @property {string[]} [actions.block_model] - Blocked models
* @property {number} [actions.max_cost_per_1k] - Max cost per 1000 tokens
* @property {number} [actions.max_tokens] - Max tokens per request
* @property {number} [actions.daily_budget] - Daily budget in USD
* @property {string} createdAt - ISO timestamp
* @property {string} updatedAt - ISO timestamp
*/
/**
* Simple glob pattern matching (supports * wildcard only).
* @param {string} pattern
* @param {string} value
* @returns {boolean}
*/
function matchGlob(pattern, value) {
if (!pattern || pattern === "*") return true;
const regex = new RegExp(
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
"i"
);
return regex.test(value);
}
export class PolicyEngine {
/** @type {Policy[]} */
#policies = [];
/**
* Load policies from an array.
* @param {Policy[]} policies
*/
loadPolicies(policies) {
this.#policies = [...policies].sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
}
/**
* Add a single policy.
* @param {Policy} policy
*/
addPolicy(policy) {
this.#policies.push(policy);
this.#policies.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
}
/**
* Remove a policy by ID.
* @param {string} id
* @returns {boolean}
*/
removePolicy(id) {
const idx = this.#policies.findIndex((p) => p.id === id);
if (idx === -1) return false;
this.#policies.splice(idx, 1);
return true;
}
/**
* Get all policies.
* @returns {Policy[]}
*/
getPolicies() {
return [...this.#policies];
}
/**
* Evaluate all policies against a request context.
*
* @param {{ model: string, provider?: string, apiKeyId?: string }} context
* @returns {{ allowed: boolean, reason?: string, preferredProviders?: string[], blockedProviders?: string[], maxTokens?: number }}
*/
evaluate(context) {
const result = {
allowed: true,
preferredProviders: [],
blockedProviders: [],
blockedModels: [],
maxTokens: undefined,
maxCostPer1k: undefined,
appliedPolicies: [],
};
for (const policy of this.#policies) {
if (!policy.enabled) continue;
// Check conditions
const conditions = policy.conditions || {};
let matches = true;
if (conditions.model_pattern && !matchGlob(conditions.model_pattern, context.model)) {
matches = false;
}
if (conditions.provider && conditions.provider !== context.provider) {
matches = false;
}
if (conditions.api_key_id && conditions.api_key_id !== context.apiKeyId) {
matches = false;
}
if (!matches) continue;
// Apply actions
const actions = policy.actions || {};
result.appliedPolicies.push(policy.name);
if (actions.prefer_provider) {
result.preferredProviders.push(...actions.prefer_provider);
}
if (actions.block_provider) {
result.blockedProviders.push(...actions.block_provider);
}
if (actions.block_model) {
const isBlocked = actions.block_model.some((pattern) =>
matchGlob(pattern, context.model)
);
if (isBlocked) {
result.allowed = false;
result.reason = `Model "${context.model}" blocked by policy "${policy.name}"`;
break;
}
}
if (actions.max_tokens !== undefined) {
result.maxTokens =
result.maxTokens !== undefined
? Math.min(result.maxTokens, actions.max_tokens)
: actions.max_tokens;
}
if (actions.max_cost_per_1k !== undefined) {
result.maxCostPer1k =
result.maxCostPer1k !== undefined
? Math.min(result.maxCostPer1k, actions.max_cost_per_1k)
: actions.max_cost_per_1k;
}
}
return result;
}
}
// Singleton instance
let engineInstance;
/**
* Get the global policy engine instance.
* @returns {PolicyEngine}
*/
export function getPolicyEngine() {
if (!engineInstance) {
engineInstance = new PolicyEngine();
}
return engineInstance;
}

View File

@@ -0,0 +1,175 @@
"use client";
/**
* NotificationToast — FASE-07 UX & Microinteractions
*
* Global toast notification component. Renders notifications from the
* notificationStore as stacked toasts in the top-right corner.
*
* Usage: Add <NotificationToast /> to your root layout.
*/
import { useNotificationStore } from "@/store/notificationStore";
import { useEffect, useState } from "react";
const ICONS = {
success: "✓",
error: "✕",
warning: "⚠",
info: "",
};
const COLORS = {
success: {
bg: "rgba(16, 185, 129, 0.15)",
border: "rgba(16, 185, 129, 0.4)",
icon: "#10b981",
},
error: {
bg: "rgba(239, 68, 68, 0.15)",
border: "rgba(239, 68, 68, 0.4)",
icon: "#ef4444",
},
warning: {
bg: "rgba(245, 158, 11, 0.15)",
border: "rgba(245, 158, 11, 0.4)",
icon: "#f59e0b",
},
info: {
bg: "rgba(59, 130, 246, 0.15)",
border: "rgba(59, 130, 246, 0.4)",
icon: "#3b82f6",
},
};
function Toast({ notification, onDismiss }) {
const [isExiting, setIsExiting] = useState(false);
const handleDismiss = () => {
setIsExiting(true);
setTimeout(() => onDismiss(notification.id), 200);
};
const color = COLORS[notification.type] || COLORS.info;
return (
<div
role="alert"
aria-live="polite"
style={{
display: "flex",
alignItems: "flex-start",
gap: "12px",
padding: "14px 16px",
borderRadius: "10px",
backgroundColor: color.bg,
border: `1px solid ${color.border}`,
backdropFilter: "blur(12px)",
boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
minWidth: "320px",
maxWidth: "420px",
animation: isExiting
? "toastOut 0.2s ease-in forwards"
: "toastIn 0.3s ease-out forwards",
transition: "all 0.2s ease",
}}
>
<span
style={{
fontSize: "18px",
color: color.icon,
fontWeight: "bold",
lineHeight: 1,
marginTop: "2px",
}}
>
{ICONS[notification.type]}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
{notification.title && (
<div
style={{
fontWeight: 600,
fontSize: "14px",
color: "var(--text-primary, #fff)",
marginBottom: "2px",
}}
>
{notification.title}
</div>
)}
<div
style={{
fontSize: "13px",
color: "var(--text-secondary, #ccc)",
lineHeight: 1.4,
}}
>
{notification.message}
</div>
</div>
{notification.dismissible && (
<button
onClick={handleDismiss}
aria-label="Dismiss notification"
style={{
background: "none",
border: "none",
cursor: "pointer",
color: "var(--text-secondary, #999)",
fontSize: "16px",
padding: "0 2px",
lineHeight: 1,
opacity: 0.6,
transition: "opacity 0.15s",
}}
onMouseEnter={(e) => (e.target.style.opacity = 1)}
onMouseLeave={(e) => (e.target.style.opacity = 0.6)}
>
×
</button>
)}
</div>
);
}
export default function NotificationToast() {
const { notifications, removeNotification } = useNotificationStore();
if (notifications.length === 0) return null;
return (
<>
<style>{`
@keyframes toastIn {
from { opacity: 0; transform: translateX(100%) scale(0.95); }
to { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes toastOut {
from { opacity: 1; transform: translateX(0) scale(1); }
to { opacity: 0; transform: translateX(100%) scale(0.95); }
}
`}</style>
<div
aria-live="polite"
aria-atomic="false"
style={{
position: "fixed",
top: "20px",
right: "20px",
zIndex: 9999,
display: "flex",
flexDirection: "column",
gap: "10px",
pointerEvents: "none",
}}
>
{notifications.map((n) => (
<div key={n.id} style={{ pointerEvents: "auto" }}>
<Toast notification={n} onDismiss={removeNotification} />
</div>
))}
</div>
</>
);
}

View File

@@ -0,0 +1,202 @@
/**
* Stream State Machine — FASE-09 E2E Flow Hardening
*
* Explicit state tracking for SSE streams with transition logging
* and lifecycle management.
*
* States: INITIALIZED → CONNECTING → STREAMING → COMPLETED | FAILED | CANCELLED
*
* @module sse/services/streamState
*/
export const STREAM_STATES = {
INITIALIZED: "initialized",
CONNECTING: "connecting",
STREAMING: "streaming",
COMPLETED: "completed",
FAILED: "failed",
CANCELLED: "cancelled",
};
// Valid state transitions
const VALID_TRANSITIONS = {
[STREAM_STATES.INITIALIZED]: [STREAM_STATES.CONNECTING, STREAM_STATES.CANCELLED],
[STREAM_STATES.CONNECTING]: [
STREAM_STATES.STREAMING,
STREAM_STATES.FAILED,
STREAM_STATES.CANCELLED,
],
[STREAM_STATES.STREAMING]: [
STREAM_STATES.COMPLETED,
STREAM_STATES.FAILED,
STREAM_STATES.CANCELLED,
],
[STREAM_STATES.COMPLETED]: [],
[STREAM_STATES.FAILED]: [],
[STREAM_STATES.CANCELLED]: [],
};
/**
* Tracks the lifecycle of a single SSE stream.
*/
export class StreamTracker {
/**
* @param {string} requestId - Unique request identifier
* @param {Object} [metadata] - Additional metadata (model, provider, etc.)
*/
constructor(requestId, metadata = {}) {
this.requestId = requestId;
this.state = STREAM_STATES.INITIALIZED;
this.metadata = metadata;
this.transitions = [];
this.startedAt = Date.now();
this.completedAt = null;
this.firstChunkAt = null;
this.chunkCount = 0;
this.totalBytes = 0;
this.error = null;
}
/**
* Transition to a new state.
*
* @param {string} newState - Target state
* @param {Object} [transitionMeta] - Metadata for this transition
* @returns {boolean} Whether the transition was valid
*/
transition(newState, transitionMeta = {}) {
const allowed = VALID_TRANSITIONS[this.state] || [];
if (!allowed.includes(newState)) {
console.warn(
`[StreamTracker] Invalid transition: ${this.state}${newState} (request: ${this.requestId})`
);
return false;
}
const now = Date.now();
this.transitions.push({
from: this.state,
to: newState,
at: now,
elapsed: now - this.startedAt,
...transitionMeta,
});
this.state = newState;
if (newState === STREAM_STATES.STREAMING && !this.firstChunkAt) {
this.firstChunkAt = now;
}
if (
newState === STREAM_STATES.COMPLETED ||
newState === STREAM_STATES.FAILED ||
newState === STREAM_STATES.CANCELLED
) {
this.completedAt = now;
}
return true;
}
/**
* Record a received chunk.
* @param {number} bytes - Chunk size in bytes
*/
recordChunk(bytes) {
this.chunkCount++;
this.totalBytes += bytes;
}
/**
* Mark as failed with an error.
* @param {Error|string} error
*/
fail(error) {
this.error = typeof error === "string" ? error : error.message;
this.transition(STREAM_STATES.FAILED, { error: this.error });
}
/**
* Get telemetry summary for this stream.
* @returns {Object}
*/
getSummary() {
const endTime = this.completedAt || Date.now();
return {
requestId: this.requestId,
state: this.state,
model: this.metadata.model,
provider: this.metadata.provider,
duration: endTime - this.startedAt,
ttfb: this.firstChunkAt ? this.firstChunkAt - this.startedAt : null,
chunkCount: this.chunkCount,
totalBytes: this.totalBytes,
transitions: this.transitions.length,
error: this.error,
};
}
/**
* Whether the stream is in a terminal state.
* @returns {boolean}
*/
isTerminal() {
return [STREAM_STATES.COMPLETED, STREAM_STATES.FAILED, STREAM_STATES.CANCELLED].includes(
this.state
);
}
}
// ─── Active Stream Registry ─────────────────
/** @type {Map<string, StreamTracker>} */
const activeStreams = new Map();
const MAX_COMPLETED_HISTORY = 100;
const completedStreams = [];
/**
* Create and register a new stream tracker.
* @param {string} requestId
* @param {Object} [metadata]
* @returns {StreamTracker}
*/
export function createStreamTracker(requestId, metadata) {
const tracker = new StreamTracker(requestId, metadata);
activeStreams.set(requestId, tracker);
return tracker;
}
/**
* Complete (archive) a stream — moves from active to completed history.
* @param {string} requestId
*/
export function archiveStream(requestId) {
const tracker = activeStreams.get(requestId);
if (!tracker) return;
activeStreams.delete(requestId);
completedStreams.push(tracker.getSummary());
// Keep history bounded
while (completedStreams.length > MAX_COMPLETED_HISTORY) {
completedStreams.shift();
}
}
/**
* Get all active streams (for monitoring dashboard).
* @returns {Array<Object>}
*/
export function getActiveStreams() {
return Array.from(activeStreams.values()).map((t) => t.getSummary());
}
/**
* Get recent completed streams.
* @param {number} [limit=20]
* @returns {Array<Object>}
*/
export function getRecentCompletedStreams(limit = 20) {
return completedStreams.slice(-limit);
}

View File

@@ -0,0 +1,98 @@
"use client";
/**
* Notification Store — FASE-07 UX & Microinteractions
*
* Zustand-based global notification system for the dashboard.
* Replaces ad-hoc feedback patterns with a centralized toast system.
*
* @module store/notificationStore
*/
import { create } from "zustand";
let idCounter = 0;
/**
* @typedef {'success'|'error'|'warning'|'info'} NotificationType
*/
/**
* @typedef {Object} Notification
* @property {number} id
* @property {NotificationType} type
* @property {string} message
* @property {string} [title]
* @property {number} [duration] - Auto-dismiss in ms (default 5000, 0 = no auto-dismiss)
* @property {boolean} [dismissible] - Whether user can dismiss (default true)
* @property {number} createdAt
*/
export const useNotificationStore = create((set, get) => ({
/** @type {Notification[]} */
notifications: [],
/**
* Add a notification.
* @param {Partial<Notification> & { message: string, type: NotificationType }} notification
* @returns {number} The notification ID
*/
addNotification: (notification) => {
const id = ++idCounter;
const entry = {
id,
type: notification.type || "info",
message: notification.message,
title: notification.title || null,
duration: notification.duration ?? 5000,
dismissible: notification.dismissible ?? true,
createdAt: Date.now(),
};
set((s) => ({
notifications: [...s.notifications, entry],
}));
// Auto-dismiss
if (entry.duration > 0) {
setTimeout(() => {
get().removeNotification(id);
}, entry.duration);
}
return id;
},
/**
* Remove a notification by ID.
* @param {number} id
*/
removeNotification: (id) => {
set((s) => ({
notifications: s.notifications.filter((n) => n.id !== id),
}));
},
/**
* Clear all notifications.
*/
clearAll: () => set({ notifications: [] }),
// ─── Convenience Methods ─────────────────
/** @param {string} message @param {string} [title] */
success: (message, title) =>
get().addNotification({ type: "success", message, title }),
/** @param {string} message @param {string} [title] */
error: (message, title) =>
get().addNotification({ type: "error", message, title, duration: 8000 }),
/** @param {string} message @param {string} [title] */
warning: (message, title) =>
get().addNotification({ type: "warning", message, title }),
/** @param {string} message @param {string} [title] */
info: (message, title) =>
get().addNotification({ type: "info", message, title }),
}));

View File

@@ -0,0 +1,303 @@
import test from "node:test";
import assert from "node:assert/strict";
// ═════════════════════════════════════════════════════
// FASE-07/08/09: UX, LLM Advanced, E2E Hardening Tests
// ═════════════════════════════════════════════════════
// ─── Policy Engine Tests ──────────────────────────
import { PolicyEngine } from "../../src/lib/policies/policyEngine.js";
test("PolicyEngine: evaluates empty policy list as allowed", () => {
const engine = new PolicyEngine();
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.allowed, true);
assert.equal(result.appliedPolicies.length, 0);
});
test("PolicyEngine: matches model glob patterns", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "1",
name: "prefer-openai-for-gpt",
type: "routing",
enabled: true,
priority: 1,
conditions: { model_pattern: "gpt-*" },
actions: { prefer_provider: ["openai"] },
},
]);
const result = engine.evaluate({ model: "gpt-4o" });
assert.equal(result.allowed, true);
assert.deepEqual(result.preferredProviders, ["openai"]);
assert.deepEqual(result.appliedPolicies, ["prefer-openai-for-gpt"]);
});
test("PolicyEngine: does not match non-matching models", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "1",
name: "gpt-only",
type: "routing",
enabled: true,
priority: 1,
conditions: { model_pattern: "gpt-*" },
actions: { prefer_provider: ["openai"] },
},
]);
const result = engine.evaluate({ model: "claude-3.5-sonnet" });
assert.equal(result.preferredProviders.length, 0);
});
test("PolicyEngine: blocks models via access policy", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "2",
name: "block-expensive",
type: "access",
enabled: true,
priority: 1,
conditions: {},
actions: { block_model: ["gpt-4*", "claude-3-opus*"] },
},
]);
const blocked = engine.evaluate({ model: "gpt-4o" });
assert.equal(blocked.allowed, false);
assert.ok(blocked.reason.includes("blocked"));
const allowed = engine.evaluate({ model: "gpt-3.5-turbo" });
assert.equal(allowed.allowed, true);
});
test("PolicyEngine: applies max_tokens from budget policy", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "3",
name: "limit-tokens",
type: "budget",
enabled: true,
priority: 1,
conditions: {},
actions: { max_tokens: 4096 },
},
]);
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.maxTokens, 4096);
});
test("PolicyEngine: skips disabled policies", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "4",
name: "disabled-policy",
type: "routing",
enabled: false,
priority: 1,
conditions: {},
actions: { prefer_provider: ["should-not-appear"] },
},
]);
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.preferredProviders.length, 0);
});
test("PolicyEngine: respects priority order", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "b",
name: "second",
type: "routing",
enabled: true,
priority: 10,
conditions: {},
actions: { prefer_provider: ["provider-b"] },
},
{
id: "a",
name: "first",
type: "routing",
enabled: true,
priority: 1,
conditions: {},
actions: { prefer_provider: ["provider-a"] },
},
]);
const result = engine.evaluate({ model: "any" });
assert.deepEqual(result.preferredProviders, ["provider-a", "provider-b"]);
assert.deepEqual(result.appliedPolicies, ["first", "second"]);
});
test("PolicyEngine: addPolicy and removePolicy work", () => {
const engine = new PolicyEngine();
engine.addPolicy({
id: "x",
name: "temp",
type: "routing",
enabled: true,
priority: 1,
conditions: {},
actions: { prefer_provider: ["x"] },
});
assert.equal(engine.getPolicies().length, 1);
engine.removePolicy("x");
assert.equal(engine.getPolicies().length, 0);
});
// ─── LRU Cache Tests ──────────────────────────
import { LRUCache } from "../../src/lib/cacheLayer.js";
test("LRUCache: set and get work", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k1", "v1");
assert.equal(cache.get("k1"), "v1");
});
test("LRUCache: returns undefined for missing keys", () => {
const cache = new LRUCache({ maxSize: 5 });
assert.equal(cache.get("missing"), undefined);
});
test("LRUCache: evicts oldest entry when full", () => {
const cache = new LRUCache({ maxSize: 3 });
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
cache.set("d", 4); // Should evict "a"
assert.equal(cache.get("a"), undefined);
assert.equal(cache.get("d"), 4);
});
test("LRUCache: TTL expiration works", async () => {
const cache = new LRUCache({ maxSize: 5, defaultTTL: 50 });
cache.set("temp", "value");
assert.equal(cache.get("temp"), "value");
await new Promise((r) => setTimeout(r, 60));
assert.equal(cache.get("temp"), undefined);
});
test("LRUCache: stats track hits and misses", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k", "v");
cache.get("k"); // hit
cache.get("missing"); // miss
const stats = cache.getStats();
assert.equal(stats.hits, 1);
assert.equal(stats.misses, 1);
assert.equal(stats.hitRate, 50);
});
test("LRUCache: generateKey produces consistent hashes", () => {
const key1 = LRUCache.generateKey({ model: "gpt-4", prompt: "hello" });
const key2 = LRUCache.generateKey({ prompt: "hello", model: "gpt-4" }); // different order
assert.equal(key1, key2);
});
test("LRUCache: delete removes entry", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k", "v");
assert.equal(cache.delete("k"), true);
assert.equal(cache.has("k"), false);
});
test("LRUCache: clear empties cache", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("a", 1);
cache.set("b", 2);
cache.clear();
assert.equal(cache.getStats().size, 0);
});
// ─── Stream State Machine Tests ──────────────────
import {
StreamTracker,
STREAM_STATES,
createStreamTracker,
getActiveStreams,
archiveStream,
} from "../../src/sse/services/streamState.js";
test("StreamTracker: starts in INITIALIZED state", () => {
const tracker = new StreamTracker("req-1");
assert.equal(tracker.state, STREAM_STATES.INITIALIZED);
});
test("StreamTracker: valid transitions succeed", () => {
const tracker = new StreamTracker("req-2");
assert.equal(tracker.transition(STREAM_STATES.CONNECTING), true);
assert.equal(tracker.transition(STREAM_STATES.STREAMING), true);
assert.equal(tracker.transition(STREAM_STATES.COMPLETED), true);
assert.equal(tracker.isTerminal(), true);
});
test("StreamTracker: invalid transitions are rejected", () => {
const tracker = new StreamTracker("req-3");
// Can't go directly to STREAMING from INITIALIZED
assert.equal(tracker.transition(STREAM_STATES.STREAMING), false);
assert.equal(tracker.state, STREAM_STATES.INITIALIZED);
});
test("StreamTracker: records TTFB on first STREAMING transition", () => {
const tracker = new StreamTracker("req-4");
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
assert.ok(tracker.firstChunkAt !== null);
});
test("StreamTracker: fail() transitions to FAILED", () => {
const tracker = new StreamTracker("req-5");
tracker.transition(STREAM_STATES.CONNECTING);
tracker.fail(new Error("connection timeout"));
assert.equal(tracker.state, STREAM_STATES.FAILED);
assert.equal(tracker.error, "connection timeout");
});
test("StreamTracker: getSummary returns telemetry", () => {
const tracker = new StreamTracker("req-6", { model: "gpt-4", provider: "openai" });
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
tracker.recordChunk(500);
tracker.recordChunk(300);
tracker.transition(STREAM_STATES.COMPLETED);
const summary = tracker.getSummary();
assert.equal(summary.requestId, "req-6");
assert.equal(summary.model, "gpt-4");
assert.equal(summary.chunkCount, 2);
assert.equal(summary.totalBytes, 800);
assert.ok(summary.duration >= 0);
assert.ok(summary.ttfb !== null);
});
test("StreamTracker: registry tracks active streams", () => {
const tracker = createStreamTracker("reg-1", { model: "test" });
const active = getActiveStreams();
assert.ok(active.some((s) => s.requestId === "reg-1"));
// Archive it
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
tracker.transition(STREAM_STATES.COMPLETED);
archiveStream("reg-1");
const afterArchive = getActiveStreams();
assert.ok(!afterArchive.some((s) => s.requestId === "reg-1"));
});