- pnpm monorepo: apps/api (Fastify + SQLite + SSE), apps/web (React+Vite), packages/shared, packages/pi-adapter - Local auth (admin/webhook-runner roles) + Keycloak JWT ready - Multi-session chat with reliable history (user persisted before LLM, assistant persisted after stream) - Markdown knowledge base with /api/docs/search + /api/docs/:id - YAML webhook catalog with backend-only execution, retry/backoff, audit (webhook_runs), and per-user rate limit - Skills config (sre-on-call, blameless-postmortem, security-incident) injected into LLM system prompt - LLM provider failover chain (config/models.yml fallback + LLM_FALLBACK_CHAIN override) - Context-aware webhooks panel + backend id-mention safety net - Per-message stats (time/duration/tokens/model), Markdown+GFM render, code & table copy/download buttons - Vitest suite, end-to-end smoke test (scripts/smoke.mjs), per-session system prompt override - /metrics Prometheus endpoint + /api/metrics JSON, request-id correlation - dotenv with explicit repo-root path; envString/envNumber helpers (handles empty-string env) - Runbooks + SOPs under knowledge/ in English; README, docs, and INDEX.md in English
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createRateLimiter } from "../src/rate-limit.js";
|
|
|
|
describe("rate-limit", () => {
|
|
it("accepts up to burst then rejects", () => {
|
|
const lim = createRateLimiter({ perMinute: 60, burst: 3 });
|
|
expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 2 });
|
|
expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 1 });
|
|
expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 0 });
|
|
const denied = lim.consume("u1", 0);
|
|
expect(denied.ok).toBe(false);
|
|
if (!denied.ok) {
|
|
expect(denied.retryAfterMs).toBeGreaterThan(0);
|
|
expect(denied.retryAfterMs).toBeLessThanOrEqual(1000);
|
|
}
|
|
});
|
|
|
|
it("refills tokens over time", () => {
|
|
const lim = createRateLimiter({ perMinute: 60, burst: 2 });
|
|
expect(lim.consume("u1", 0).ok).toBe(true);
|
|
expect(lim.consume("u1", 0).ok).toBe(true);
|
|
expect(lim.consume("u1", 0).ok).toBe(false);
|
|
// 1 second later, 1 token refilled
|
|
expect(lim.consume("u1", 1000).ok).toBe(true);
|
|
expect(lim.consume("u1", 1000).ok).toBe(false);
|
|
});
|
|
|
|
it("isolates buckets per id", () => {
|
|
const lim = createRateLimiter({ perMinute: 60, burst: 1 });
|
|
expect(lim.consume("u1", 0).ok).toBe(true);
|
|
expect(lim.consume("u1", 0).ok).toBe(false);
|
|
// u2 has its own bucket
|
|
expect(lim.consume("u2", 0).ok).toBe(true);
|
|
expect(lim.consume("u2", 0).ok).toBe(false);
|
|
});
|
|
|
|
it("caps refill at burst", () => {
|
|
const lim = createRateLimiter({ perMinute: 60, burst: 2 });
|
|
// Wait a long time, tokens should still be capped at 2
|
|
const result = lim.consume("u1", 60_000);
|
|
expect(result).toEqual({ ok: true, remaining: 1 });
|
|
expect(lim.consume("u1", 60_000).ok).toBe(true);
|
|
expect(lim.consume("u1", 60_000).ok).toBe(false);
|
|
});
|
|
|
|
it("reset clears a single bucket or all", () => {
|
|
const lim = createRateLimiter({ perMinute: 60, burst: 1 });
|
|
lim.consume("u1", 0);
|
|
lim.consume("u2", 0);
|
|
expect(lim.size()).toBe(2);
|
|
lim.reset("u1");
|
|
expect(lim.size()).toBe(1);
|
|
expect(lim.consume("u1", 0).ok).toBe(true);
|
|
lim.reset();
|
|
expect(lim.size()).toBe(0);
|
|
});
|
|
});
|