feat(quality): no-new-warnings por PR — ESLint bulk suppressions + lint-guard fork-condicional (Pacote 4) (#6218)

* feat(quality): no-new-warnings per PR via native ESLint bulk suppressions

Pacote 4 do plano mestre testes+CI (aprovado 2026-07-04). O ratchet de
eslintWarnings so rodava no CI pesado (release-PR) -> o drift acumulava invisivel
e explodia na release (+41/+37/+88 por ciclo, rebaselinado as cegas — historico
no proprio quality-baseline.json). Modelo novo (SonarSource Clean-as-You-Code +
ESLint bulk suppressions nativo >=9.24):

- config/quality/eslint-suppressions.json congela a divida existente por
  arquivo+regra: 476 arquivos / 4.273 violacoes.
- npm run lint + lint-staged (pre-commit) + novo job lint-guard no quality.yml
  rodam suppressions-aware: violacao NOVA fica vermelha NO PR que a introduz
  (bulk suppressions ainda eleva estouros de baseline por arquivo a error).
- 3 regras warn promovidas a error em src/** (react-hooks/exhaustive-deps,
  @next/next/no-img-element, import/no-anonymous-default-export) — divida
  existente congelada, ocorrencia nova = erro imediato.
- collect-metrics mede sob o baseline congelado -> a metrica eslintWarnings
  vira 'divida liquida nova' (~0 em regime); baseline apertado 4279->0 no mesmo
  PR (exigencia do require-tighten). Aperto do ESTOQUE congelado: npx eslint .
  --prune-suppressions na reconciliacao da release.
- Principio Zero: lint-guard usa continue-on-error para PR de FORK (report-only;
  a campanha /green-prs aplica o fix via co-autoria) — bloqueante so para
  branches internas, a origem real do drift.

Validacao: negativo (any novo em tests/) exit 1; negativo (img em src/, regra
promovida) exit 1; positivo escopado exit 0; baseline gerado por --suppress-all
no tip (tree inteiro passa por construcao); YAML js-yaml ok.

* fix(quality): clear the 6 residual warnings so lint-guard runs clean at --max-warnings 0

The committed baseline still let 6 warnings through the lint-guard gate:
5 now-unused inline eslint-disable directives (the file-level suppressions
made them redundant — removed via eslint --fix, suppressions regenerated to
absorb the re-exposed occurrences) and 1 anonymous default export in
tests/load/k6-soak.js (outside the src/** severity-override scope — named
the k6 scenario function instead).

Verified on the clean tree: lint-guard exit=0; any-canary (new 'const x: any'
in open-sse) exit=1 — the gate bites on NEW violations while the 4,273
frozen ones stay suppressed (476 files).

* fix(ci): lint-guard continue-on-error must be boolean on non-PR events

github.event.pull_request is undefined on workflow_dispatch — the bare property
expression made the job fail at plan time (run 28722888456: 4 jobs green, run red,
lint-guard never materialized). Guard with event_name check so the expression is
always boolean: PR de fork = report-only (Principio Zero), resto = blocking.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-04 21:25:17 -03:00
committed by GitHub
parent 5a4bde1879
commit 059dbe9f13
12 changed files with 2507 additions and 69 deletions

View File

@@ -146,3 +146,31 @@ jobs:
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/2
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
# config/quality/eslint-suppressions.json congela as violações EXISTENTES por
# arquivo+regra; qualquer warning NOVO aparece e o --max-warnings 0 falha o job — o
# drift de +41/+88 warnings por ciclo passa a morrer no PR que o introduz, em vez de
# ser rebaselinado às cegas na release. Aperto do baseline (na reconciliação da
# release): npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json
#
# Princípio Zero: bloqueante SÓ para branches internas (as campanhas/sessões são a
# origem do drift). PR de FORK roda em modo report (continue-on-error → o job fica
# verde com anotação; a campanha /green-prs aplica o fix via co-autoria — o
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: ESLint (baseline congelado — warning novo = vermelho)
run: npx eslint . --suppressions-location config/quality/eslint-suppressions.json --max-warnings 0

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
"metrics": {
"eslintWarnings": {
"value": 4279,
"value": 0,
"_rebaseline_2026_07_03_v3844_residual_release_green": "4270->4279 (+9). v3.8.44 residual drift on release tip 716041223 (moving target: eslint 4270->4279 as the branch advanced past the prior rebaseline). Inherited from parallel-session merges (Quality Ratchet not on PR->release fast-gates).",
"_rebaseline_2026_07_03_v3844_ipfilter_release_green": "4256->4270 (+14). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.",
"_rebaseline_2026_07_03_v3844_review_prs_fix_batch": "4199->4256 (+57). Inherited v3.8.44 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrue unmeasured across the cycle). 4256 = measured by `node scripts/quality/collect-metrics.mjs` on the release tip 72ee80649 during the /review-prs fix-batch round. The round's own merges (#5958 SSE-accept, #5988 deepseek-web, #6013/#5974 retry-after-json, #5975 embeddings-proxy, #5973 non-json-guard) plus the parallel-session merge burst into release/v3.8.44 account for the delta; all `any`-warn-allowed in open-sse/ + tests/. Cyclomatic is already green (2012 < baseline 2015) and needs no bump. Tighten via --require-tighten next cycle.",
@@ -18,7 +18,8 @@
"_rebaseline_2026_06_23_v3835_release": "v3.8.35 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR→release fast-gates, so warnings accrued across this cycle's parallel-session merges — Compression Phase 4 #4694/#4707/#4716/#4720, chatCore #3501 leaf extractions, contributor PRs #4726/#4753/#4774/#4781/#4783/#4793, etc.). 3907→3912 (+5). Verified my release-finalize working tree touches ONLY docs/*.md (THREAT_MODEL), CHANGELOG.md, baselines, and 1 string line in scripts/check/check-fabricated-docs.mjs — 0 production-code change, so all +5 is inherited contributor drift. No coverage/openapi/i18n regressions.",
"_rebaseline_2026_06_22_v3834_release": "v3.8.34 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR→release fast-gates, so warnings accrued across this cycle's parallel-session merges — #4583-4586/#4588-4593/#4606-4621/#4644/#4647/#4696/etc.). 3900→3907 (+7). Verified my release-finalize working tree touches ONLY CHANGELOG.md (git status: 0 code changes), so all +7 is inherited contributor drift. No coverage/openapi/i18n regressions.",
"_rebaseline_2026_06_22_v3833_release": "Cumulative cycle drift surfaced by the release PR full CI. 3867→3900 (+33).",
"_rebaseline_2026_06_26_v3837_release": "3970->3987. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle."
"_rebaseline_2026_06_26_v3837_release": "3970->3987. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
"_rebaseline_2026_07_04_pacote4_no_new_warnings": "4279->0. Pacote 4 do plano mestre testes+CI: a divida pre-existente (4279 warnings + violacoes das 3 regras promovidas a error em src/**) foi CONGELADA em config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo) e passa a ser bloqueada NO PR que a introduziria (job lint-guard no quality.yml + npm run lint + lint-staged, todos suppressions-aware; fork = report-only, Principio Zero). collect-metrics agora mede sob o baseline congelado -> a metrica vira 'divida liquida NOVA' (~0 em regime). O aperto do ESTOQUE congelado acontece via `npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json` na reconciliacao da release. Fim das rebaselines-surpresa de +41/+88 por ciclo."
},
"eslintErrors": {
"value": 0,

View File

@@ -4,6 +4,23 @@ import tseslint from "typescript-eslint";
/** @type {import("eslint").Linter.Config[]} */
const eslintConfig = [
...nextVitals,
// Pacote 4 (plano mestre testes+CI, 2026-07-04) — zero-warning policy: TODA regra roda
// como "error" e a dívida pré-existente vive congelada por arquivo+regra em
// config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo). Violação
// NOVA = vermelho no ato (lint-staged no pre-commit + job lint-guard no fast path);
// o drift de +41/+88 warnings/ciclo que era rebaselinado às cegas na release morre no
// PR que o introduz. Aperto do baseline: npx eslint . --prune-suppressions
// --suppressions-location config/quality/eslint-suppressions.json (na release).
{
// Escopo = onde os presets do next registram estes plugins (bloco global sem `files`
// atingiria scripts/*.mjs sem o plugin react-hooks e explodiria o flat config).
files: ["src/**/*.{ts,tsx,js,jsx}"],
rules: {
"react-hooks/exhaustive-deps": "error",
"@next/next/no-img-element": "error",
"import/no-anonymous-default-export": "error",
},
},
// FASE-02: Security rules (strict everywhere)
{
rules: {
@@ -32,7 +49,7 @@ const eslintConfig = [
files: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
rules: {
"no-restricted-syntax": [
"warn",
"error",
{
selector:
"CallExpression[callee.property.name='includes'][callee.object.callee.property.name='toLowerCase']",
@@ -49,7 +66,7 @@ const eslintConfig = [
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-explicit-any": "error",
"@next/next/no-assign-module-variable": "off",
"react-hooks/rules-of-hooks": "off",
},

View File

@@ -65,7 +65,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
* The pattern is strictly bounded (no unbounded quantifiers over overlapping
* alternatives) so it runs in linear time on untrusted input — ReDoS-safe.
*/
// eslint-disable-next-line no-control-regex
const ANSI_ESCAPE_RE =
/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[A-Z\[\]\\^_`])|[\x00-\x08\x0b\x0c\x0e-\x1f]/g;

View File

@@ -87,7 +87,7 @@
"build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs",
"build:native:tproxy": "cd src/mitm/tproxy/native && npx --yes node-gyp rebuild",
"start": "node scripts/dev/run-next.mjs start",
"lint": "eslint .",
"lint": "eslint . --suppressions-location config/quality/eslint-suppressions.json",
"lint:md": "npx --yes markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"!docs/i18n\" \"!docs/research\"",
"lint:prose": "vale docs",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"",
@@ -346,7 +346,7 @@
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs}": [
"prettier --write",
"eslint --fix --no-error-on-unmatched-pattern"
"eslint --fix --no-error-on-unmatched-pattern --suppressions-location config/quality/eslint-suppressions.json"
],
"*.{json,md,yml,yaml,css}": [
"prettier --write"

View File

@@ -15,11 +15,21 @@ import * as yaml from "js-yaml";
const cwd = process.cwd();
const out = {};
// 1) ESLint: contagem de warnings (errors devem ser 0; o lint já gata isso)
// 1) ESLint: contagem de warnings/errors NOVOS além do baseline congelado.
// Pacote 4 (plano mestre testes+CI, 2026-07-04): a dívida pré-existente vive em
// config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo) e é
// bloqueada no PR que a introduziria (job lint-guard + lint-staged). A métrica do
// ratchet passa a medir a dívida LÍQUIDA nova — em regime, ~0 — em vez do estoque
// bruto (que driftava +41/+88 por ciclo e era rebaselinado às cegas na release).
// O aperto do estoque acontece via --prune-suppressions na release.
function eslintCounts() {
let stdout;
const args = ["eslint", ".", "--format", "json"];
if (fs.existsSync(path.join(cwd, "config/quality/eslint-suppressions.json"))) {
args.push("--suppressions-location", "config/quality/eslint-suppressions.json");
}
try {
stdout = execFileSync("npx", ["eslint", ".", "--format", "json"], {
stdout = execFileSync("npx", args, {
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
});

View File

@@ -150,7 +150,6 @@ export default function StreamTransformerAccordion({
const prev = prevForceOpen.current;
prevForceOpen.current = Boolean(forceOpen);
if (!prev && forceOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing deep-link prop into local state
setOpen(true);
setHasOpened(true);
onOpenChange?.(true);
@@ -344,10 +343,7 @@ export default function StreamTransformerAccordion({
onClick={() => handleCopy(rawSse, "input")}
aria-label={translateOrFallback("copyInput", "Copy input")}
>
<span
className="material-symbols-outlined text-[14px]"
aria-hidden="true"
>
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
{copiedField === "input" ? "check" : "content_copy"}
</span>
</Button>
@@ -358,7 +354,10 @@ export default function StreamTransformerAccordion({
data-testid="raw-sse-input"
className="min-h-[360px] w-full rounded-lg border border-border bg-bg-secondary px-3 py-3 text-xs font-mono text-text-main focus:outline-none focus:ring-1 focus:ring-primary/50"
spellCheck={false}
aria-label={translateOrFallback("rawChatSseInput", "Raw chat completions SSE")}
aria-label={translateOrFallback(
"rawChatSseInput",
"Raw chat completions SSE"
)}
/>
</div>
@@ -378,10 +377,7 @@ export default function StreamTransformerAccordion({
disabled={!transformedSse}
aria-label={translateOrFallback("copyOutput", "Copy output")}
>
<span
className="material-symbols-outlined text-[14px]"
aria-hidden="true"
>
<span className="material-symbols-outlined text-[14px]" aria-hidden="true">
{copiedField === "output" ? "check" : "content_copy"}
</span>
</Button>
@@ -440,9 +436,7 @@ export default function StreamTransformerAccordion({
<th className="pb-2 pr-4">
{translateOrFallback("eventType", "Event type")}
</th>
<th className="pb-2">
{translateOrFallback("eventPreview", "Preview")}
</th>
<th className="pb-2">{translateOrFallback("eventPreview", "Preview")}</th>
</tr>
</thead>
<tbody>

View File

@@ -107,14 +107,17 @@ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"
function resolveArea(urlPath: string): SkillArea | null {
for (const [prefix, area] of PATH_AREA_MAP) {
if (urlPath === prefix || urlPath.startsWith(prefix + "/") || urlPath.startsWith(prefix + "{")) {
if (
urlPath === prefix ||
urlPath.startsWith(prefix + "/") ||
urlPath.startsWith(prefix + "{")
) {
return area;
}
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractOperations(pathsObj: Record<string, any>): OpenapiPath[] {
const ops: OpenapiPath[] = [];
@@ -155,7 +158,7 @@ export function parseOpenapi(): ParsedOpenapi {
} catch (err) {
throw new Error(
`openapiParser: could not read ${yamlPath}. ` +
`Run from project root. Underlying error: ${err instanceof Error ? err.message : String(err)}`,
`Run from project root. Underlying error: ${err instanceof Error ? err.message : String(err)}`
);
}

View File

@@ -13,7 +13,7 @@ export const options = {
const BASE_URL = __ENV.BASE_URL || "http://localhost:20128";
export default function () {
export default function soakScenario() {
const res = http.get(`${BASE_URL}/api/monitoring/health`);
check(res, { "status is 200": (r) => r.status === 200 });
}

View File

@@ -138,21 +138,13 @@ test("#3685 empty Claude stream (content_filter, no content blocks) is marked in
false,
`expected invalid for empty content-filtered stream, got valid=true (reason: ${out.reason})`
);
assert.match(
out.reason ?? "",
/empty/i,
`reason should mention 'empty', got: "${out.reason}"`
);
assert.match(out.reason ?? "", /empty/i, `reason should mention 'empty', got: "${out.reason}"`);
});
test("#3685 non-empty Claude stream (has content blocks) remains valid", async () => {
const res = makeNonEmptyClaudeStream();
const out = await validateResponseQuality(res, true, silentLog);
assert.equal(
out.valid,
true,
`expected valid for non-empty stream, got invalid: ${out.reason}`
);
assert.equal(out.valid, true, `expected valid for non-empty stream, got invalid: ${out.reason}`);
// The clonedResponse must be present so the combo loop can pipe the stream body
assert.ok(out.clonedResponse, "clonedResponse must be returned for valid streaming response");
assert.ok(out.clonedResponse!.body, "clonedResponse must have a body stream");
@@ -172,10 +164,7 @@ test("#3685 empty Claude stream without message_start lifecycle → valid (incom
// A stream that only has partial events (e.g. disconnected before message_start)
// should not trigger the failover since the lifecycle isn't complete — this is
// handled by other mechanisms (stream readiness timeout).
const partialEvents = [
`event: ping\ndata: ${JSON.stringify({ type: "ping" })}`,
"",
];
const partialEvents = [`event: ping\ndata: ${JSON.stringify({ type: "ping" })}`, ""];
const res = new Response(claudeSseStream(partialEvents), {
status: 200,
headers: { "content-type": "text/event-stream" },
@@ -252,7 +241,7 @@ test("#3685 streaming is preserved for non-empty response: clonedResponse body y
// Drain the clonedResponse body and reconstruct the full byte sequence.
const reader = out.clonedResponse!.body!.getReader();
const receivedChunks: Uint8Array[] = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
@@ -277,7 +266,10 @@ test("#3685 streaming is preserved for non-empty response: clonedResponse body y
// Verify the response carries SSE content blocks in the decoded text,
// confirming real content was streamed through.
const decoded = new TextDecoder().decode(reconstructed);
assert.ok(decoded.includes("content_block_start"), "decoded body must contain content_block_start");
assert.ok(
decoded.includes("content_block_start"),
"decoded body must contain content_block_start"
);
assert.ok(decoded.includes("Hello"), "decoded body must contain the actual text content");
assert.ok(decoded.includes(", world!"), "decoded body must contain the full text delta");
});

View File

@@ -93,7 +93,6 @@ function createMockRedisClient() {
return keys.map((k) => store.get(k) ?? null);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async eval(...args: unknown[]): Promise<unknown> {
record("eval", ...args);
return null;
@@ -121,7 +120,8 @@ function createMockRedisClient() {
// ─── Tests ──────────────────────────────────────────────────────────────────
test("redisQuotaStore: consume calls INCRBYFLOAT + EXPIRE and returns sliding window value", async () => {
const { RedisQuotaStore, resetRedisClient } = await import("../../src/lib/quota/redisQuotaStore.ts");
const { RedisQuotaStore, resetRedisClient } =
await import("../../src/lib/quota/redisQuotaStore.ts");
resetRedisClient();
const mock = createMockRedisClient();
@@ -183,7 +183,8 @@ test("redisQuotaStore: getRedisClient throws clear error if ioredis not installe
}
if (!ioredisAvailable) {
const { getRedisClient, resetRedisClient } = await import("../../src/lib/quota/redisQuotaStore.ts");
const { getRedisClient, resetRedisClient } =
await import("../../src/lib/quota/redisQuotaStore.ts");
resetRedisClient();
await assert.rejects(
@@ -195,7 +196,8 @@ test("redisQuotaStore: getRedisClient throws clear error if ioredis not installe
);
} else {
// ioredis is installed — just verify getRedisClient returns a client object
const { getRedisClient, resetRedisClient } = await import("../../src/lib/quota/redisQuotaStore.ts");
const { getRedisClient, resetRedisClient } =
await import("../../src/lib/quota/redisQuotaStore.ts");
resetRedisClient();
const client = await getRedisClient("redis://localhost:6399");
@@ -212,34 +214,39 @@ test("redisQuotaStore: getRedisClient throws clear error if ioredis not installe
// ─── Real Redis integration (gated) ─────────────────────────────────────────
test("redisQuotaStore: real Redis integration (skipped unless RUN_QUOTA_REDIS_INT=1)", {
skip: process.env.RUN_QUOTA_REDIS_INT !== "1",
}, async () => {
const { RedisQuotaStore, resetRedisClient } = await import("../../src/lib/quota/redisQuotaStore.ts");
resetRedisClient();
test(
"redisQuotaStore: real Redis integration (skipped unless RUN_QUOTA_REDIS_INT=1)",
{
skip: process.env.RUN_QUOTA_REDIS_INT !== "1",
},
async () => {
const { RedisQuotaStore, resetRedisClient } =
await import("../../src/lib/quota/redisQuotaStore.ts");
resetRedisClient();
const REDIS_URL = process.env.QUOTA_STORE_REDIS_URL ?? "redis://localhost:6379";
const store = new RedisQuotaStore(REDIS_URL);
const dim = { poolId: "it-pool", unit: "tokens" as const, window: "hourly" as const };
const REDIS_URL = process.env.QUOTA_STORE_REDIS_URL ?? "redis://localhost:6379";
const store = new RedisQuotaStore(REDIS_URL);
const dim = { poolId: "it-pool", unit: "tokens" as const, window: "hourly" as const };
// Clear before test
await store.clear("it-key", dim);
// Clear before test
await store.clear("it-key", dim);
await store.consume("it-key", dim, 100);
await store.consume("it-key", dim, 200);
const effective = await store.peek("it-key", dim);
await store.consume("it-key", dim, 100);
await store.consume("it-key", dim, 200);
const effective = await store.peek("it-key", dim);
// In same bucket, prev=0 → effective≈300
assert.ok(effective > 290, `Expected >290, got ${effective}`);
assert.ok(effective <= 300, `Expected <=300, got ${effective}`);
// In same bucket, prev=0 → effective≈300
assert.ok(effective > 290, `Expected >290, got ${effective}`);
assert.ok(effective <= 300, `Expected <=300, got ${effective}`);
// Cleanup
await store.clear("it-key", dim);
const afterClear = await store.peek("it-key", dim);
assert.equal(afterClear, 0);
// Cleanup
await store.clear("it-key", dim);
const afterClear = await store.peek("it-key", dim);
assert.equal(afterClear, 0);
resetRedisClient();
});
resetRedisClient();
}
);
test("redisQuotaStore: sliding window decay formula is correct", async () => {
// Unit test for the math without real Redis.
@@ -265,7 +272,8 @@ test("redisQuotaStore: sliding window decay formula is correct", async () => {
});
test("redisQuotaStore: resetRedisQuotaStore resets the store singleton", async () => {
const { getRedisQuotaStore, resetRedisQuotaStore } = await import("../../src/lib/quota/redisQuotaStore.ts");
const { getRedisQuotaStore, resetRedisQuotaStore } =
await import("../../src/lib/quota/redisQuotaStore.ts");
const store1 = getRedisQuotaStore("redis://localhost:6399");
resetRedisQuotaStore();