Compare commits

...

1 Commits

Author SHA1 Message Date
backryun
058b2573c6 fix(build): repair broken production build, red lint gate and SWR crash
The Build CI job is advisory, so eight module-level defects from eight
different PRs accumulated on release/v3.8.50 until `npm run build` failed
with 7 Turbopack errors and `npm run lint` with 14.

Build (link-time):
- modelSelectModalHelpers.ts: a lost `}` swallowed PROVIDER_TEST_CHUNK_SIZE
  into isProviderModelHidden's body (#9011).
- videoGeneration.ts: handleFalVideoGeneration imported twice; the standalone
  falHandler.ts is superseded by the provider-neutral mediaGeneration/fal.ts
  and is removed here (#9982 over #9969).
- catalog.ts: re-exported and called the injectable SWR policy that #9199
  deliberately replaced with a fixed 30s bound. Fixed on the consumer side —
  restoring the accessor would resurrect the unbounded window #9199 removed
  after measuring a 41s catalog build in production.
- tinycmsSigner.ts: generated wasm-bindgen glue kept a sidecar
  `new URL('wasm_signer_bg.wasm', import.meta.url)` that no file backs;
  Turbopack resolves it statically. The module ships inlined as WASM_BASE64
  and the only caller always passes it explicitly (#8736/#10087).
- conolDiscovery.ts: imported getProviderOutboundGuard from outboundUrlGuard,
  which does not export it. Fixed on the consumer side: outboundUrlGuard.ts is
  loaded by the packaged CLI without a tsconfig, so it must stay free of
  `@/`-aliased imports (#7682).

Runtime (the build never caught this one):
- catalogCache.ts::scheduleBackgroundRefresh had two dangling statements
  referencing undeclared `inFlight`/`promise`, so EVERY stale-while-revalidate
  read threw a ReferenceError. Surfaced by realigning the #8728 suite, which
  #9199 left asserting a removed contract.

Lint:
- driverFactory.test.ts: a case inserted between the preceding test's `finally`
  and its `});` left the file unparseable, so the SQLite driver-cascade suite
  (26 tests) had not run since 2026-08-11 (#9173).
- providerModelsConfig.ts: imported an executor directly, crossing the G14
  boundary; routed through a new open-sse/services/zaiWebCredentials.ts (#8451).
- image-combo.test.ts: 11 `any` violations, now typed (#9499).

Validation: npm run build exit 0, npm run lint clean, typecheck:core clean,
41/41 tests green across the affected suites.

Refs #9011 #9982 #9199 #8728 #8736 #10087 #8974 #9173 #8451 #9499
2026-08-12 13:31:40 -03:00
15 changed files with 278 additions and 434 deletions

View File

@@ -0,0 +1 @@
- **fix(build):** repair the broken Turbopack production build, the red lint gate and a runtime crash on `release/v3.8.50`. Six independent module-level defects, each from a different PR, had accumulated because the `Build` CI job is advisory rather than blocking: a lost closing brace in `modelSelectModalHelpers.ts` that swallowed `PROVIDER_TEST_CHUNK_SIZE` into a function body (#9011); `handleFalVideoGeneration` imported twice in `videoGeneration.ts` after the provider-neutral Fal module superseded the standalone handler (#9982 over #9969); `catalog.ts` still re-exporting and calling the injectable stale-while-revalidate policy that #9199 deliberately replaced with a fixed 30 s bound when it landed on top of #8728 — the consumer and the #8728 test suite were never realigned; two dangling statements left in `catalogCache.ts::scheduleBackgroundRefresh` referencing undeclared `inFlight`/`promise`, which made **every** stale-while-revalidate read throw a `ReferenceError` at runtime (a defect the build never caught, surfaced here by the realigned test); a generated wasm-bindgen sidecar URL in `tinycmsSigner.ts` that Turbopack resolves at build time even though the WASM module ships inlined as base64 (#8736/#10087); `conolDiscovery.ts` importing `getProviderOutboundGuard` from `outboundUrlGuard` instead of the sibling `outboundUrlGuardPolicy` module that actually exports it (#8974) — fixed on the consumer side, since re-exporting it would put a `@/`-aliased import into the module the packaged CLI loads without a tsconfig (#7682); and an unbalanced brace in `tests/unit/db-adapters/driverFactory.test.ts` where a new case was inserted between the preceding test's `finally` block and its `});`, so the whole file stopped parsing and the SQLite driver-cascade coverage silently stopped running since 2026-08-11 (#9173).

View File

@@ -438,7 +438,14 @@ async function __wbg_init(module_or_path) {
}
if (module_or_path === undefined) {
module_or_path = new URL('wasm_signer_bg.wasm', import.meta.url);
// Upstream wasm-bindgen glue defaults to a sidecar binary resolved via
// `new URL(<sidecar>, import.meta.url)`. OmniRoute ships the module inlined as
// WASM_BASE64 instead — no sidecar exists in the repo — and the only caller,
// initTinyCmsWasm(), always passes that decoded Buffer explicitly, so this
// branch is unreachable. The literal URL still had to go: Turbopack resolves
// `new URL(<literal>, import.meta.url)` statically, so keeping it failed
// `next build` with a "Module not found" for the missing sidecar.
throw new Error('TinyCMS WASM module must be supplied explicitly (see initTinyCmsWasm)');
}
const imports = __wbg_get_imports();

View File

@@ -18,7 +18,6 @@ import { handleNovitaVideoGeneration } from "./videoGeneration/novitaHandler.ts"
import { handleXaiVideoGeneration } from "./videoGeneration/xaiGrokImagineHandler.ts";
import { handleSegmindVideoGeneration } from "./videoGeneration/providers/segmind.ts";
import { handleAdobeFireflyVideoGeneration } from "./videoGeneration/adobeFireflyHandler.ts";
import { handleFalVideoGeneration } from "./videoGeneration/falHandler.ts";
import { handleOpenAIVideoGeneration } from "./videoGeneration/openai.ts";
import { getVideoJobPreset, handleVideoJobGeneration } from "./videoGeneration/job.ts";
import {

View File

@@ -1,254 +0,0 @@
import { saveCallLog } from "@/lib/usageDb";
import {
FetchTimeoutError,
fetchWithTimeout,
getConfiguredTimeout,
} from "@/shared/utils/fetchTimeout";
import { sanitizeErrorMessage } from "../../utils/error.ts";
interface FalVideoBody {
prompt?: unknown;
aspect_ratio?: unknown;
duration?: unknown;
resolution?: unknown;
quality?: unknown;
generate_audio?: unknown;
poll_interval_ms?: unknown;
[key: string]: unknown;
}
interface FalCredentials {
apiKey?: unknown;
accessToken?: unknown;
}
interface FalProviderConfig {
baseUrl: string;
}
interface FalLog {
info?: (scope: string, message: string, meta?: unknown) => void;
error?: (scope: string, message: string) => void;
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function numberValue(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function grokDuration(value: unknown, fallback = 6): number {
const numeric = numberValue(value);
if (numeric !== undefined) return Math.round(numeric);
if (typeof value === "string") {
const match = value.trim().match(/^(\d+)s$/);
if (match) return Number(match[1]);
}
return fallback;
}
function falDuration(value: unknown, fallback = "8s"): string {
if (typeof value === "string" && /^(4|6|8)s$/.test(value)) return value;
const numeric = numberValue(value);
return numeric && [4, 6, 8].includes(numeric) ? `${numeric}s` : fallback;
}
export function buildFalVideoPayload(model: string, body: FalVideoBody): Record<string, unknown> {
const prompt = stringValue(body.prompt) || "";
const aspectRatio = stringValue(body.aspect_ratio) || "16:9";
const resolution = stringValue(body.resolution) || (body.quality === "hd" ? "1080p" : "720p");
if (model.startsWith("xai/grok-imagine-video/")) {
return {
prompt,
aspect_ratio: aspectRatio,
duration: grokDuration(body.duration),
resolution,
};
}
return {
prompt,
aspect_ratio: aspectRatio,
duration: falDuration(body.duration),
resolution,
generate_audio: typeof body.generate_audio === "boolean" ? body.generate_audio : true,
};
}
function absoluteFalUrl(value: unknown, baseUrl: string): string | undefined {
const url = stringValue(value);
if (!url) return undefined;
if (url.startsWith("http://") || url.startsWith("https://")) return url;
return `${baseUrl.replace(/\/$/, "")}/${url.replace(/^\//, "")}`;
}
function normalizeFalVideoResponse(payload: unknown) {
const record = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
const video =
record.video && typeof record.video === "object"
? (record.video as Record<string, unknown>)
: null;
const url = video && typeof video.url === "string" ? video.url.trim() : "";
if (!url) {
return {
success: false as const,
status: 502,
error: "Fal video generation returned no video URL",
};
}
return {
success: true as const,
data: {
created: typeof record.created === "number" ? record.created : Math.floor(Date.now() / 1000),
data: [{ url, format: "mp4" }],
},
};
}
function falModelPath(model: string): string {
return model.startsWith("xai/") ? model : `fal-ai/${model}`;
}
function getToken(credentials: FalCredentials | null | undefined): string {
return String(credentials?.apiKey || credentials?.accessToken || "");
}
function responseError(payload: unknown): string {
return sanitizeErrorMessage(JSON.stringify(payload).slice(0, 500));
}
export async function handleFalVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: FalProviderConfig;
body: FalVideoBody;
credentials: FalCredentials | null | undefined;
log?: FalLog | null;
}) {
const token = getToken(credentials);
if (!token) return { success: false as const, status: 401, error: "Fal API key is required" };
const startTime = Date.now();
const timeoutMs = getConfiguredTimeout();
const pollIntervalMs = Math.max(100, numberValue(body.poll_interval_ms) || 1000);
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
const queueUrl = `${baseUrl}/${falModelPath(model)}`;
const headers = {
Authorization: `Key ${token}`,
"Content-Type": "application/json",
};
log?.info?.("VIDEO", `${provider}/${model} (fal-ai-video)`, {
prompt: stringValue(body.prompt)?.slice(0, 200) || "",
});
try {
const createResponse = await fetchWithTimeout(queueUrl, {
method: "POST",
headers,
body: JSON.stringify(buildFalVideoPayload(model, body)),
timeoutMs,
});
const createPayload = await createResponse.json().catch(() => ({}));
if (!createResponse.ok) {
const error = responseError(createPayload);
log?.error?.("VIDEO", `Fal create failed (${createResponse.status}): ${error}`);
return { success: false as const, status: createResponse.status, error };
}
const requestId = stringValue(createPayload.request_id);
if (!requestId) return normalizeFalVideoResponse(createPayload);
const statusUrl =
absoluteFalUrl(createPayload.status_url, baseUrl) ||
`${queueUrl}/requests/${requestId}/status`;
const responseUrl =
absoluteFalUrl(createPayload.response_url, baseUrl) || `${queueUrl}/requests/${requestId}`;
const deadline = startTime + timeoutMs;
while (Date.now() < deadline) {
const remainingMs = Math.max(1000, deadline - Date.now());
const statusResponse = await fetchWithTimeout(statusUrl, {
headers: { Authorization: `Key ${token}` },
timeoutMs: Math.min(timeoutMs, remainingMs),
});
const statusPayload = await statusResponse.json().catch(() => ({}));
if (!statusResponse.ok) {
return {
success: false as const,
status: statusResponse.status,
error: responseError(statusPayload),
};
}
const status = stringValue(statusPayload.status);
if (status === "COMPLETED") {
const resultResponse = await fetchWithTimeout(responseUrl, {
headers: { Authorization: `Key ${token}` },
timeoutMs: Math.min(timeoutMs, Math.max(1000, deadline - Date.now())),
});
const resultPayload = await resultResponse.json().catch(() => ({}));
if (!resultResponse.ok) {
return {
success: false as const,
status: resultResponse.status,
error: responseError(resultPayload),
};
}
const result = normalizeFalVideoResponse(resultPayload);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: result.success ? 200 : result.status,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
...(result.success ? {} : { error: result.error }),
}).catch(() => {});
return result;
}
if (status && !["IN_QUEUE", "IN_PROGRESS"].includes(status)) {
return {
success: false as const,
status: 502,
error: `Fal video generation ended with status ${status}`,
};
}
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remainingMs)));
}
return {
success: false as const,
status: 504,
error: `Fal video generation timed out after ${timeoutMs}ms`,
};
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const isTimeout =
error instanceof FetchTimeoutError || (error as { name?: string }).name === "AbortError";
const status = isTimeout ? 504 : 502;
const safeMessage = sanitizeErrorMessage(message);
log?.error?.("VIDEO", `Fal request failed: ${safeMessage}`);
return { success: false as const, status, error: `Fal video provider error: ${safeMessage}` };
}
}

View File

@@ -0,0 +1,10 @@
/**
* Service boundary for Z.ai web-cookie credential parsing.
*
* `extractZaiToken` is pure credential parsing, not request execution, but it lives in
* `executors/zai-web/protocol.ts` alongside the transport it was written for. App routes
* and `src/lib` consumers must not import from `open-sse/executors/**` (G14 import
* boundary — see EXECUTOR_IMPORT_RESTRICTION in eslint.config.mjs), so they go through
* this service instead of reaching into the executor tree.
*/
export { extractZaiToken } from "../executors/zai-web/protocol.ts";

View File

@@ -1,5 +1,5 @@
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuardPolicy";
import { resolveConolCredentials } from "@omniroute/open-sse/services/conolAuth.ts";
import {
CONOL_FALLBACK_MODELS,

View File

@@ -23,7 +23,7 @@ import { QWEN_CLOUD_TEXT_MODELS } from "@omniroute/open-sse/config/providers/reg
import { filterAlibabaFreeEligibleModels } from "@omniroute/open-sse/services/alibabaFreeTierDiscovery.ts";
import { shouldUseLiveAlibabaFreeModelDiscovery } from "@omniroute/open-sse/services/alibabaFreeTier.ts";
import { isDashscopeTextModelId } from "@omniroute/open-sse/services/dashscopeTextModels.ts";
import { extractZaiToken } from "@omniroute/open-sse/executors/zai-web.ts";
import { extractZaiToken } from "@omniroute/open-sse/services/zaiWebCredentials.ts";
import { normalizeOpenAiLikeModelsResponse } from "./normalizers";
const QWEN_CLOUD_TEXT_MODEL_IDS = new Set(QWEN_CLOUD_TEXT_MODELS.map((model) => model.id));

View File

@@ -118,25 +118,18 @@ export { getCustomVisionCapabilityFields };
// lives in ./catalogCache. Re-exported here because the existing tests import the
// hooks from this module, and CATALOG_STALE_WHILE_REVALIDATE_MS is part of the
// documented behavior of this endpoint.
import {
CATALOG_CACHE_TTL_MS_DEFAULT,
resolveCachedCatalogResponse,
type CatalogCachePolicy,
} from "./catalogCache";
import { CATALOG_CACHE_TTL_MS_DEFAULT, resolveCachedCatalogResponse } from "./catalogCache";
export {
CATALOG_STALE_WHILE_REVALIDATE_MS,
getCatalogStaleWhileRevalidateMs,
__resetCatalogBuilderRunsForTest,
__getCatalogBuilderRunsForTest,
__expireCatalogCacheForTest,
__setCatalogCacheEntryForTest,
__flushCatalogBackgroundRefreshForTest,
__forceCatalogInFlightRejectionForTest,
__setCatalogStaleWhileRevalidateAccessorForTest,
__setCatalogStaleWhileRevalidateMsForTest,
} from "./catalogCache";
export type { CachedCatalog, CatalogCachePolicy } from "./catalogCache";
export type { CachedCatalog } from "./catalogCache";
const BUILTIN_AUTO_YIELD_INTERVAL = 8;
@@ -150,8 +143,7 @@ function yieldCatalogBuildTurn(): Promise<void> {
*/
export async function getUnifiedModelsResponse(
request: Request,
corsHeaders: Record<string, string> = {},
cachePolicy: CatalogCachePolicy = {}
corsHeaders: Record<string, string> = {}
) {
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
@@ -184,7 +176,6 @@ export async function getUnifiedModelsResponse(
request,
{ corsHeaders, diagnosticHeaders },
buildCatalogPayload,
cachePolicy,
{
hideAutoCombos: settingsForAuth?.hideAutoCombos === true,
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,

View File

@@ -200,12 +200,10 @@ function scheduleBackgroundRefresh(
});
}, 0);
});
inFlight = { version: lastSeenCatalogCacheVersion, promise };
// Nobody on the stale path awaits this, so pre-handle the rejection; a cold-path
// caller that joins it via catalogInFlight attaches its own handler and still
// observes the failure.
promise.catch(() => {});
refreshPromise.catch(() => {});
catalogInFlight.set(cacheKey, { generation, promise: refreshPromise });
refreshPromise

View File

@@ -139,6 +139,8 @@ export function isProviderModelHidden(
return false;
}
return hiddenModelsByProvider.get(providerId)?.has(modelId) ?? false;
}
/** Matches the provider-page "Test All Models" concurrency (#chunks of 3). */
export const PROVIDER_TEST_CHUNK_SIZE = 3;

View File

@@ -25,13 +25,19 @@ const core = await import("@/lib/db/core.ts");
const { createCombo } = await import("@/lib/db/combos");
const { executeImageCombo } = await import("@omniroute/open-sse/services/imageCombo");
type LogEntry = { level: string; tag: unknown; msg: unknown };
function createLog() {
const entries: any[] = [];
const entries: LogEntry[] = [];
const record =
(level: string) =>
(tag: unknown, msg: unknown): number =>
entries.push({ level, tag, msg });
return {
info: (tag: any, msg: any) => entries.push({ level: "info", tag, msg }),
warn: (tag: any, msg: any) => entries.push({ level: "warn", tag, msg }),
error: (tag: any, msg: any) => entries.push({ level: "error", tag, msg }),
debug: (tag: any, msg: any) => entries.push({ level: "debug", tag, msg }),
info: record("info"),
warn: record("warn"),
error: record("error"),
debug: record("debug"),
entries,
};
}
@@ -52,13 +58,13 @@ function createMockAuth() {
}
async function cleanupTestDataDir() {
let lastError: any;
let lastError: unknown;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
return;
} catch (error: any) {
} catch (error: unknown) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 25));
}

View File

@@ -264,6 +264,8 @@ describe("driverFactory", () => {
second.close();
first.close();
}
});
// Cursor renewal plan, Task 2 Step 5: tryIdeAuth() now passes a
// busy-timeout to tryOpenSync() on every driver path, since it's invoked
// from an unattended sweep tick (not just the human-attended auto-import

View File

@@ -1,3 +1,20 @@
/**
* Stale-while-revalidate for the /v1/models catalog cache (#8728).
*
* Contract note: #8728 originally shipped an injectable `CatalogCachePolicy`
* (a per-call SWR accessor + refresh scheduler) and an unbounded
* `CATALOG_STALE_WHILE_REVALIDATE_MS = Number.POSITIVE_INFINITY`. #9199 landed
* afterwards and deliberately replaced both: the window is now a fixed 30 s
* constant and the refresh is scheduled internally via `setTimeout(…, 0)`.
* See catalogCache.ts — an unbounded window let a refresh that kept failing pin
* an ancient catalog forever.
*
* These tests were left asserting the removed API, which is what broke the
* production build (catalog.ts re-exported three symbols that no longer exist).
* They are realigned here to the shipped contract: the BEHAVIOR #8728 added —
* an expired-but-recent successful entry is served immediately while a refresh
* runs behind it — is still fully covered.
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
@@ -10,8 +27,6 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const readCache = await import("../../src/lib/db/readCache.ts");
const catalogCache = await import("../../src/app/api/v1/models/catalogCache.ts");
type RefreshTask = () => Promise<void>;
function request() {
return new Request("http://localhost/v1/models");
}
@@ -25,28 +40,11 @@ function payload(body: string, status = 200): catalogCache.CatalogPayload {
};
}
function createPolicyQueue() {
const tasks: RefreshTask[] = [];
return {
policy: {
getStaleWhileRevalidateMs: () => Number.POSITIVE_INFINITY,
scheduleBackgroundRefresh: (task: RefreshTask) => {
tasks.push(task);
},
},
tasks,
};
}
async function resolve(
build: (request: Request) => Promise<catalogCache.CatalogPayload>,
policy = createPolicyQueue().policy
) {
async function resolve(build: (request: Request) => Promise<catalogCache.CatalogPayload>) {
return catalogCache.resolveCachedCatalogResponse(
request(),
{ corsHeaders: {}, diagnosticHeaders: {} },
build,
policy
build
);
}
@@ -58,146 +56,88 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("production SWR policy is unbounded and reset restores the default accessor", () => {
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, Number.POSITIVE_INFINITY);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
catalogCache.__setCatalogStaleWhileRevalidateAccessorForTest(() => 0);
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), 0);
catalogCache.__resetCatalogBuilderRunsForTest();
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
test("the SWR window is a bounded constant, not an unbounded accessor", () => {
// #9199 replaced the injectable POSITIVE_INFINITY accessor with this bound so a
// refresh that keeps failing cannot pin an old catalog forever.
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
assert.ok(Number.isFinite(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS));
});
test("reset detaches scheduled work before it can run", async () => {
const { policy, tasks } = createPolicyQueue();
await resolve(async () => payload("old"), policy);
catalogCache.__expireCatalogCacheForTest();
await resolve(async () => payload("detached"), policy);
assert.equal(tasks.length, 1);
catalogCache.__resetCatalogBuilderRunsForTest();
await tasks[0]();
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 0);
});
test("ordinary TTL expiry serves the last success indefinitely and schedules one refresh per key", async () => {
const { policy, tasks } = createPolicyQueue();
const initial = await resolve(async () => payload("old"), policy);
assert.equal(await initial.text(), "old");
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
const staleResponses = await Promise.all(
Array.from({ length: 5 }, () => resolve(async () => payload("new"), policy))
);
assert.deepEqual(
await Promise.all(staleResponses.map((response) => response.text())),
Array(5).fill("old")
);
assert.equal(tasks.length, 1, "concurrent stale reads must schedule exactly one refresh");
test("a fresh entry is replayed without running the builder again", async () => {
const first = await resolve(async () => payload("fresh"));
assert.equal(await first.text(), "fresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
await tasks[0]();
const refreshed = await resolve(async () => payload("unexpected"), policy);
assert.equal(await refreshed.text(), "new");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
const second = await resolve(async () => payload("SHOULD NOT BUILD"));
assert.equal(await second.text(), "fresh");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
});
test("unsuccessful cold payloads are returned but never cached", async () => {
const first = await resolve(async () => payload("temporary failure", 503));
assert.equal(first.status, 503);
assert.equal(await first.text(), "temporary failure");
const second = await resolve(async () => payload("recovered"));
assert.equal(second.status, 200);
assert.equal(await second.text(), "recovered");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("failed background refresh retains the prior successful snapshot and permits retry", async (t) => {
t.mock.method(console, "error", () => {});
const { policy, tasks } = createPolicyQueue();
assert.equal(await (await resolve(async () => payload("old"), policy)).text(), "old");
test("an expired successful entry is served immediately while a refresh runs behind it", async () => {
await resolve(async () => payload("old"));
catalogCache.__expireCatalogCacheForTest();
// The stale body comes back on THIS call — the caller never waits for the rebuild.
const stale = await resolve(async () => payload("new"));
assert.equal(await stale.text(), "old", "the expired-but-recent entry must be served as-is");
await catalogCache.__flushCatalogBackgroundRefreshForTest();
const refreshed = await resolve(async () => payload("SHOULD NOT BUILD"));
assert.equal(await refreshed.text(), "new", "the background refresh must replace the snapshot");
});
test("an entry aged past the SWR window is not served stale", async () => {
await resolve(async () => payload("ancient"));
catalogCache.__expireCatalogCacheForTest(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS + 1_000);
const rebuilt = await resolve(async () => payload("rebuilt"));
assert.equal(await rebuilt.text(), "rebuilt", "past the window the caller must wait for a build");
});
test("a cached non-200 is never replayed as stale", async () => {
// Replaying a cached error as "stale" would mask an intermittent failure behind
// a fake success forever.
catalogCache.__setCatalogCacheEntryForTest(request(), {
body: "boom",
headers: {},
status: 500,
expiresAt: Date.now() - 1,
});
const res = await resolve(async () => payload("recovered"));
assert.equal(res.status, 200);
assert.equal(await res.text(), "recovered");
});
test("concurrent cold requests share a single builder run", async () => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const build = async () => {
await gate;
return payload("coalesced");
};
const inFlight = [resolve(build), resolve(build), resolve(build)];
release();
const bodies = await Promise.all((await Promise.all(inFlight)).map((r) => r.text()));
assert.deepEqual(bodies, ["coalesced", "coalesced", "coalesced"]);
assert.equal(
await (
await resolve(async () => {
throw new Error("temporary failure");
}, policy)
).text(),
"old"
catalogCache.__getCatalogBuilderRunsForTest(),
1,
"identical concurrent requests must coalesce onto one in-flight build (#6408)"
);
await tasks.shift()!();
assert.equal(
await (await resolve(async () => payload("temporary failure", 503), policy)).text(),
"old"
);
assert.equal(tasks.length, 1, "a failed refresh must release single-flight state for retry");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("new"), policy)).text(), "old");
assert.equal(tasks.length, 1, "an unsuccessful payload must also permit another refresh");
await tasks.shift()!();
assert.equal(await (await resolve(async () => payload("unused"), policy)).text(), "new");
});
test("hard invalidation drops snapshots, detaches old work, and guards old-generation writeback", async () => {
let resolveOld!: (value: catalogCache.CatalogPayload) => void;
const oldPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveOld = resolvePromise;
});
let currentBuildStarted = false;
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
test("a state change invalidates the cache so the next read rebuilds", async () => {
await resolve(async () => payload("before"));
const oldRequest = resolve(async () => oldPayload);
await Promise.resolve();
readCache.invalidateDbCache();
readCache.invalidateModelCatalogCache();
const currentRequest = resolve(async () => {
currentBuildStarted = true;
return currentPayload;
});
await Promise.resolve();
assert.equal(currentBuildStarted, true, "the first post-write read must start a current build");
resolveCurrent(payload("current"));
assert.equal(await (await currentRequest).text(), "current");
resolveOld(payload("old"));
assert.equal(await (await oldRequest).text(), "old");
const cached = await resolve(async () => payload("unexpected"));
assert.equal(await cached.text(), "current", "old completion must not overwrite current cache");
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
});
test("hard invalidation clears a completed snapshot and makes the next read block", async () => {
assert.equal(await (await resolve(async () => payload("old"))).text(), "old");
readCache.invalidateModelCatalogCache();
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
resolveCurrent = resolvePromise;
});
let settled = false;
const next = resolve(async () => currentPayload).then((response) => {
settled = true;
return response;
});
await Promise.resolve();
assert.equal(settled, false, "post-write reads may block and must not serve the old snapshot");
resolveCurrent(payload("current"));
assert.equal(await (await next).text(), "current");
const after = await resolve(async () => payload("after"));
assert.equal(await after.text(), "after", "a write must be reflected on the very next read");
});

View File

@@ -0,0 +1,142 @@
/**
* Regression guard for the broken Turbopack production build on release/v3.8.50.
*
* Six independent module-level defects, each from a different PR, made `npm run build`
* fail. Most are *link-time* errors, so those cases are expressed as "this module must be
* importable / statically resolvable" rather than as behavioral assertions:
*
* 1. src/shared/components/modelSelectModalHelpers.ts — a lost `}` (#9011) left
* `export const PROVIDER_TEST_CHUNK_SIZE` inside a function body.
* 2. open-sse/handlers/videoGeneration.ts — `handleFalVideoGeneration` imported
* twice from two different modules (#9982 landed on top of #9969).
* 3. src/app/api/v1/models/catalog.ts — re-exported three SWR symbols that #9199
* deliberately removed from ./catalogCache when it replaced #8728's injectable
* policy with a fixed 30 s bound. The consumer was never updated.
* 4. src/app/api/v1/models/catalogCache.ts — the same PR left two dangling statements
* referencing undeclared `inFlight` / `promise`, so every stale-while-revalidate
* read threw a ReferenceError at RUNTIME (this one the build never caught).
* 5. open-sse/executors/tinycmsSigner.ts — generated wasm-bindgen glue kept a
* `new URL('wasm_signer_bg.wasm', import.meta.url)` default that no file backs.
* 6. src/app/api/providers/[id]/models/conolDiscovery.ts — imported
* `getProviderOutboundGuard` from ./outboundUrlGuard, which does not export it (#8974).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("modelSelectModalHelpers exports survive isProviderModelHidden (#9011 missing brace)", async () => {
const helpers = await import("@/shared/components/modelSelectModalHelpers");
// The missing `}` swallowed everything after isProviderModelHidden into its body,
// so this constant stopped being a module-level export.
assert.equal(helpers.PROVIDER_TEST_CHUNK_SIZE, 3);
const hidden = helpers.parseHiddenModelsByProvider({ openai: ["gpt-4o"] });
assert.equal(helpers.isProviderModelHidden(hidden, "openai", "gpt-4o"), true);
assert.equal(helpers.isProviderModelHidden(hidden, "openai", "gpt-4o-mini"), false);
assert.equal(helpers.isProviderModelHidden(hidden, "anthropic", "gpt-4o"), false);
});
test("videoGeneration handler links with a single handleFalVideoGeneration binding (#9982)", async () => {
// A duplicate import binding is an ESM early SyntaxError, so the import itself is
// the assertion. The Fal video path must resolve to the provider-neutral module
// that #9982 added (it also covers the #9969 Grok Imagine routing).
const mod = await import("@omniroute/open-sse/handlers/videoGeneration.ts");
assert.equal(typeof mod.handleVideoGeneration, "function");
const source = readFileSync(path.join(repoRoot, "open-sse/handlers/videoGeneration.ts"), "utf8");
const falImports = source.match(/import \{ handleFalVideoGeneration \}/g) ?? [];
assert.equal(falImports.length, 1, "handleFalVideoGeneration must be imported exactly once");
assert.match(source, /handleFalVideoGeneration \} from "\.\/mediaGeneration\/fal\.ts"/);
});
test("catalog.ts re-exports only what catalogCache actually exports (#9199)", async () => {
// #9199 deliberately replaced #8728's injectable SWR policy with a fixed bound, but
// left catalog.ts re-exporting the three removed symbols. Re-exporting a binding the
// source module does not export is a link-time error, so this import IS the assertion.
const catalog = await import("@/app/api/v1/models/catalog");
const catalogCache = await import("@/app/api/v1/models/catalogCache");
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
assert.equal(catalog.CATALOG_STALE_WHILE_REVALIDATE_MS, 30_000);
// The accessor/policy trio must stay gone — re-adding it would resurrect the
// unbounded window #9199 removed (a failing refresh could pin an old catalog forever).
for (const removed of [
"getCatalogStaleWhileRevalidateMs",
"__setCatalogStaleWhileRevalidateAccessorForTest",
"__setCatalogStaleWhileRevalidateMsForTest",
]) {
assert.equal(
(catalogCache as Record<string, unknown>)[removed],
undefined,
`${removed} was removed by #9199 and must not come back`
);
}
});
test("the stale-while-revalidate path does not throw a ReferenceError (#9199 merge residue)", async () => {
// The mangled merge left `inFlight = { … promise }` referencing two undeclared
// identifiers inside scheduleBackgroundRefresh, so EVERY stale read crashed at runtime.
const catalogCache = await import("@/app/api/v1/models/catalogCache");
catalogCache.__resetCatalogBuilderRunsForTest();
const payload = (body: string) => ({
body,
headers: { "content-type": "application/json" },
status: 200,
cacheTTL: 60_000,
});
const call = (body: string) =>
catalogCache.resolveCachedCatalogResponse(
new Request("http://localhost/v1/models"),
{ corsHeaders: {}, diagnosticHeaders: {} },
async () => payload(body)
);
await call("first");
catalogCache.__expireCatalogCacheForTest();
const stale = await call("second");
assert.equal(await stale.text(), "first", "the stale body must be served, not a crash");
await catalogCache.__flushCatalogBackgroundRefreshForTest();
});
test("conolDiscovery resolves getProviderOutboundGuard from the policy module (#8974)", async () => {
// outboundUrlGuard.ts does NOT export getProviderOutboundGuard — importing it from
// there is a link-time error, so the import below is the assertion.
const mod = await import("@/app/api/providers/[id]/models/conolDiscovery");
assert.equal(typeof mod.maybeHandleConolModelDiscovery, "function");
// The fix must stay on the consumer side. outboundUrlGuard.ts is loaded by the packaged
// CLI, where no tsconfig resolves `@/*`, so re-exporting the policy helpers from it
// (they pull in featureFlags → the DB layer) would break `omniroute setup-opencode`
// (#7682). Guard that nobody "fixes" this by adding the re-export instead.
const guardSource = readFileSync(
path.join(repoRoot, "src/shared/network/outboundUrlGuard.ts"),
"utf8"
);
assert.doesNotMatch(
guardSource,
/^\s*(import|export)[^\n]*from\s+["']@\//m,
"outboundUrlGuard.ts must stay free of @/-aliased imports/re-exports (#7682)"
);
});
test("tinycmsSigner has no build-time-resolvable wasm sidecar URL (#8736/#10087)", () => {
const source = readFileSync(path.join(repoRoot, "open-sse/executors/tinycmsSigner.ts"), "utf8");
// The wasm binary ships inlined as WASM_BASE64; there is no wasm_signer_bg.wasm file.
// Turbopack statically resolves `new URL(<literal>, import.meta.url)`, so leaving the
// generated default in place fails `next build` with "Module not found".
assert.ok(source.includes("const WASM_BASE64 ="), "wasm binary must stay inlined");
assert.doesNotMatch(
source,
/new URL\(\s*['"]wasm_signer_bg\.wasm['"]/,
"generated wasm-bindgen sidecar URL must not survive — Turbopack resolves it at build time"
);
});

View File

@@ -134,8 +134,11 @@ await test("chave quota-exclusive não constrói o catálogo completo", async (t
process.env.EXPOSE_CC_DISCOVERY_ALIASES = "1";
// A chave do cache é `prefix|isCodex|apiKey|configuredOnly` — um query param
// qualquer NÃO a invalida, então a resposta do subteste anterior seria servida.
v1ModelsCatalog.__setCatalogStaleWhileRevalidateMsForTest(0);
v1ModelsCatalog.__expireCatalogCacheForTest(1);
// #9199 removed the injectable SWR window; age the entry past the fixed
// 30 s constant instead so the next read rebuilds rather than serving stale.
v1ModelsCatalog.__expireCatalogCacheForTest(
v1ModelsCatalog.CATALOG_STALE_WHILE_REVALIDATE_MS + 1_000
);
try {
const res = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models", {
@@ -151,9 +154,6 @@ await test("chave quota-exclusive não constrói o catálogo completo", async (t
`ids=${JSON.stringify(body.data.map((m) => m.id).slice(0, 6))}`
);
} finally {
v1ModelsCatalog.__setCatalogStaleWhileRevalidateMsForTest(
v1ModelsCatalog.CATALOG_STALE_WHILE_REVALIDATE_MS
);
if (prev === undefined) delete process.env.EXPOSE_CC_DISCOVERY_ALIASES;
else process.env.EXPOSE_CC_DISCOVERY_ALIASES = prev;
}