mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
fix(radar): close cumulative quality regressions
This commit is contained in:
@@ -43,15 +43,14 @@ export const KNOWN_DUPLICATE_VERSIONS = new Set([
|
||||
// ---------------------------------------------------------------------------
|
||||
// ALLOWLIST 2 — gaps de sequência CONHECIDOS.
|
||||
// Fonte: auditoria do disco (src/lib/db/migrations/). Além dos slots legados,
|
||||
// 144–145 seguem reservados pelas migrations Radar que já existem na série
|
||||
// empilhada; a migration 143 já aterrissou. O job registry foi promovido de 139
|
||||
// para 146 pela tabela RENAMED_MIGRATION_COMPATIBILITY. A
|
||||
// As migrations Radar 144–145 e a migration 143 já aterrissaram. O job registry
|
||||
// foi promovido de 139 para 146 pela tabela RENAMED_MIGRATION_COMPATIBILITY. A
|
||||
// 147–149 estão reservadas por migrations atualmente em trânsito nos PRs #8228,
|
||||
// #9313, #10047 e #10066; esta branch usa 150 para evitar essas colisões conhecidas.
|
||||
// O stale-enforcement exige que cada reserva seja removida quando os arquivos
|
||||
// correspondentes aterrissarem na release.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const KNOWN_GAPS = new Set(["026", "055", "121", "144", "145", "148", "149"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12)
|
||||
export const KNOWN_GAPS = new Set(["026", "055", "121", "148", "149"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12)
|
||||
|
||||
function pad3(n) {
|
||||
return String(n).padStart(3, "0");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useLocale, useTranslations } from "next-intl";
|
||||
|
||||
import {
|
||||
NEWS_DISMISS_EVENT,
|
||||
NEWS_DISMISS_STORAGE_KEY,
|
||||
NEWS_DISMISS_STORAGE_NAME,
|
||||
fetchNewsPayload,
|
||||
parseDismissedNewsIds,
|
||||
selectActiveNews,
|
||||
@@ -23,7 +23,7 @@ function subscribeToDismissals(callback: () => void) {
|
||||
|
||||
function readDismissedIds(): string {
|
||||
try {
|
||||
return localStorage.getItem(NEWS_DISMISS_STORAGE_KEY) ?? "";
|
||||
return localStorage.getItem(NEWS_DISMISS_STORAGE_NAME) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export default function NewsBanner() {
|
||||
const dismiss = () => {
|
||||
dismissedIds.add(announcement.id);
|
||||
try {
|
||||
localStorage.setItem(NEWS_DISMISS_STORAGE_KEY, serializeDismissedNewsIds(dismissedIds));
|
||||
localStorage.setItem(NEWS_DISMISS_STORAGE_NAME, serializeDismissedNewsIds(dismissedIds));
|
||||
} catch {
|
||||
// Storage is optional; the next announcement fetch remains functional.
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { isPaidModelTarget } from "@/shared/utils/freeModels";
|
||||
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance";
|
||||
import { isAuthRequired } from "@/shared/utils/apiAuth";
|
||||
import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
|
||||
import { extractApiKey } from "@/sse/services/auth";
|
||||
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
|
||||
|
||||
@@ -1736,6 +1736,8 @@
|
||||
"quotaShare": "Chia sẻ hạn mức",
|
||||
"discovery": "Khám phá",
|
||||
"freeProviderRankings": "Xếp hạng nhà cung cấp miễn phí",
|
||||
"radar": "Radar",
|
||||
"setup": "Thiết lập",
|
||||
"freeTiers": "Gói miễn phí",
|
||||
"gamification": "Trò chơi hóa",
|
||||
"leaderboard": "Bảng xếp hạng",
|
||||
|
||||
@@ -362,7 +362,8 @@ function normalizeRadarIdentity(
|
||||
}
|
||||
|
||||
function normalizeDisplayName(value: unknown): string | null | undefined {
|
||||
if (value === undefined || value === null) return value;
|
||||
if (value === undefined) return undefined;
|
||||
if (value === null) return null;
|
||||
if (typeof value !== "string") return undefined;
|
||||
const normalized = value.trim();
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,7 @@ export const CHANGELOG_RAW_URL =
|
||||
"https://raw.githubusercontent.com/diegosouzapw/OmniRoute/main/CHANGELOG.md";
|
||||
export const CHANGELOG_GITHUB_URL =
|
||||
"https://github.com/diegosouzapw/OmniRoute/blob/main/CHANGELOG.md";
|
||||
export const NEWS_DISMISS_STORAGE_KEY = "omniroute-news-dismissed-v2";
|
||||
export const NEWS_DISMISS_STORAGE_NAME = "omniroute-news-dismissed-v2";
|
||||
export const NEWS_DISMISS_EVENT = "omniroute:news-dismissed";
|
||||
|
||||
const NEWS_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
|
||||
@@ -31,19 +31,21 @@ const httpsUrlSchema = z
|
||||
return url.protocol === "https:" && !url.username && !url.password;
|
||||
}, "Announcement links must use HTTPS without embedded credentials");
|
||||
|
||||
const localizedTextMapSchema = z.record(localizedTextSchema).superRefine((value, context) => {
|
||||
if (!value.en) {
|
||||
context.addIssue({ code: z.ZodIssueCode.custom, message: "English copy is required" });
|
||||
}
|
||||
for (const locale of Object.keys(value)) {
|
||||
if (!LOCALE_PATTERN.test(locale)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid locale: ${locale}`,
|
||||
});
|
||||
const localizedTextMapSchema = z
|
||||
.record(z.string(), localizedTextSchema)
|
||||
.superRefine((value, context) => {
|
||||
if (!value.en) {
|
||||
context.addIssue({ code: z.ZodIssueCode.custom, message: "English copy is required" });
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const locale of Object.keys(value)) {
|
||||
if (!LOCALE_PATTERN.test(locale)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid locale: ${locale}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newsFeedItemSchema = z
|
||||
.object({
|
||||
|
||||
@@ -104,8 +104,8 @@ test("frozen allowlists match the documented legacy and stacked-series gaps", ()
|
||||
assert.ok((KNOWN_GAPS as Set<string>).has("055"));
|
||||
assert.ok((KNOWN_GAPS as Set<string>).has("121"));
|
||||
assert.equal((KNOWN_GAPS as Set<string>).has("143"), false);
|
||||
assert.ok((KNOWN_GAPS as Set<string>).has("144"));
|
||||
assert.ok((KNOWN_GAPS as Set<string>).has("145"));
|
||||
assert.equal((KNOWN_GAPS as Set<string>).has("144"), false);
|
||||
assert.equal((KNOWN_GAPS as Set<string>).has("145"), false);
|
||||
// 147 left the gap list when 147_api_keys_model_access_mode.sql landed (same pattern as 143).
|
||||
assert.equal((KNOWN_GAPS as Set<string>).has("147"), false);
|
||||
assert.ok((KNOWN_GAPS as Set<string>).has("148"));
|
||||
|
||||
90
tests/unit/radar-sync-response-limit.test.ts
Normal file
90
tests/unit/radar-sync-response-limit.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
||||
const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
|
||||
process.env.RADAR_FEED_PUBKEY = publicKeyDer.toString("base64");
|
||||
|
||||
const fixturePath = path.resolve(import.meta.dirname!, "../fixtures/radar-feed-canonical.json");
|
||||
const fixtureBytes = fs.readFileSync(fixturePath);
|
||||
|
||||
function signBytes(bytes: Buffer): string {
|
||||
return crypto.sign(null, bytes, privateKey).toString("base64");
|
||||
}
|
||||
|
||||
function mockResponse(body: Buffer, headers: Record<string, string> = {}): Response {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Map(Object.entries(headers)),
|
||||
arrayBuffer: () =>
|
||||
Promise.resolve(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
const syncMod = await import("../../src/lib/radar/sync.ts");
|
||||
|
||||
test("FIX6: oversized Content-Length avoids reading the body or touching cache", async () => {
|
||||
let arrayBufferCalled = false;
|
||||
const response = mockResponse(Buffer.from("irrelevant"), {
|
||||
"content-length": String(10 * 1024 * 1024 + 1),
|
||||
});
|
||||
const originalArrayBuffer = response.arrayBuffer.bind(response);
|
||||
(response as unknown as { arrayBuffer: () => Promise<ArrayBuffer> }).arrayBuffer = () => {
|
||||
arrayBufferCalled = true;
|
||||
return originalArrayBuffer();
|
||||
};
|
||||
|
||||
let setCacheCalled = false;
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(response)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false);
|
||||
assert.equal(arrayBufferCalled, false);
|
||||
});
|
||||
|
||||
test("FIX6: oversized streamed body without Content-Length leaves cache untouched", async () => {
|
||||
const oversized = Buffer.alloc(10 * 1024 * 1024 + 1, 0x41);
|
||||
let setCacheCalled = false;
|
||||
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(mockResponse(oversized))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false);
|
||||
});
|
||||
|
||||
test("FIX6: body within the 10MB cap proceeds normally", async () => {
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(fixtureBytes, {
|
||||
"x-omniroute-feed-signature": signBytes(fixtureBytes),
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, "too_large");
|
||||
});
|
||||
@@ -967,75 +967,3 @@ test("syncRadar: first sync (no cache) with valid data => updated", async () =>
|
||||
assert.equal(result.status, "updated");
|
||||
assert.equal(cacheStore.length, 1);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// FIX 6 — 10 MB response cap (unbounded `Buffer.from(await res.arrayBuffer())`)
|
||||
// ===========================================================================
|
||||
|
||||
test("FIX6: Content-Length header exceeding the 10MB cap => too_large, cache untouched, body never read", async () => {
|
||||
let arrayBufferCalled = false;
|
||||
const oversizedContentLength = String(10 * 1024 * 1024 + 1);
|
||||
const response = mockResponse(Buffer.from("irrelevant"), {
|
||||
"content-length": oversizedContentLength,
|
||||
});
|
||||
const originalArrayBuffer = response.arrayBuffer.bind(response);
|
||||
(response as unknown as { arrayBuffer: () => Promise<ArrayBuffer> }).arrayBuffer = () => {
|
||||
arrayBufferCalled = true;
|
||||
return originalArrayBuffer();
|
||||
};
|
||||
|
||||
let setCacheCalled = false;
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(response)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false, "cache must not be touched");
|
||||
assert.equal(
|
||||
arrayBufferCalled,
|
||||
false,
|
||||
"body must not be read once Content-Length already exceeds the cap"
|
||||
);
|
||||
});
|
||||
|
||||
test("FIX6: oversized body without a trustworthy Content-Length header => too_large, cache untouched", async () => {
|
||||
const oversized = Buffer.alloc(10 * 1024 * 1024 + 1, 0x41);
|
||||
let setCacheCalled = false;
|
||||
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(oversized, {}))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false, "cache must not be touched");
|
||||
});
|
||||
|
||||
test("FIX6: body within the 10MB cap proceeds normally (never returns too_large)", async () => {
|
||||
const sig = signBytes(FIXTURE_BYTES);
|
||||
|
||||
const result = await syncMod.syncRadar({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(FIXTURE_BYTES, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, "too_large");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user