mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 12:52:25 +03:00
Merge remote-tracking branch 'origin/release/v3.8.51' into fix/trivy-cve-2025-68121-tls-client
This commit is contained in:
36
.github/workflows/api-route-typecheck.yml
vendored
Normal file
36
.github/workflows/api-route-typecheck.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: API Route Typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "release/**"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
api-typecheck:
|
||||
name: API Route Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- name: Reject new API-route TypeScript diagnostics
|
||||
run: node scripts/check/check-api-typecheck.mjs
|
||||
- name: API typecheck gate unit tests
|
||||
run: node --import tsx/esm --test tests/unit/build/check-api-typecheck.test.ts
|
||||
1
changelog.d/fixes/11758-usage-refresh-exclusive-lease.md
Normal file
1
changelog.d/fixes/11758-usage-refresh-exclusive-lease.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(usage):** quota and usage refresh no longer 409 when an exclusive lease reserves the connection ([#11758](https://github.com/diegosouzapw/OmniRoute/pull/11758)) — thanks @TheDemonTuan
|
||||
419
config/quality/api-typecheck-baseline.json
Normal file
419
config/quality/api-typecheck-baseline.json
Normal file
@@ -0,0 +1,419 @@
|
||||
{
|
||||
"open-sse/transformer/responsesTransformer.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"open-sse/utils/progressTracker.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"open-sse/utils/sseHeartbeat.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"open-sse/utils/stream.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"src/app/api/assess/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/cache/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/cli-tools/all-statuses/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/cli-tools/claude-settings/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/cli-tools/cline-settings/route.ts": {
|
||||
"TS2339": 5
|
||||
},
|
||||
"src/app/api/cli-tools/codex-settings/route.ts": {
|
||||
"TS2345": 2
|
||||
},
|
||||
"src/app/api/cli-tools/grok-build-settings/route.ts": {
|
||||
"TS2304": 1
|
||||
},
|
||||
"src/app/api/cli-tools/hermes-agent-settings/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/cli-tools/letta-settings/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/cli-tools/omp-settings/route.ts": {
|
||||
"TS2339": 8
|
||||
},
|
||||
"src/app/api/cli-tools/qwen-settings/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/combos/auto/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/combos/test/route.ts": {
|
||||
"TS2345": 1,
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/compression/compare/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/compression/preview/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/context/combos/[id]/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/context/combos/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/copilot/chat/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/guardrails/test/route.ts": {
|
||||
"TS2554": 1
|
||||
},
|
||||
"src/app/api/internal/codex-responses-ws/route.ts": {
|
||||
"TS2740": 1,
|
||||
"TS2339": 7
|
||||
},
|
||||
"src/app/api/keys/[id]/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/local/redis/start/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/local/redis/stop/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/logs/[id]/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/model-capability-overrides/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/model-combo-mappings/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/models/alias/route.ts": {
|
||||
"TS2339": 5
|
||||
},
|
||||
"src/app/api/models/route.ts": {
|
||||
"TS2345": 3,
|
||||
"TS2538": 1
|
||||
},
|
||||
"src/app/api/monitoring/health/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/oauth/codex/import-token/route.ts": {
|
||||
"TS2339": 3
|
||||
},
|
||||
"src/app/api/oauth/codex/import/route.ts": {
|
||||
"TS2554": 1,
|
||||
"TS2353": 1,
|
||||
"TS2339": 3
|
||||
},
|
||||
"src/app/api/oauth/cursor/login/poll/route.ts": {
|
||||
"TS2554": 1
|
||||
},
|
||||
"src/app/api/oauth/kiro/auto-import/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/omniroute/route/preview/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/playground/presets/[id]/route.ts": {
|
||||
"TS2339": 3
|
||||
},
|
||||
"src/app/api/provider-nodes/validate/route.ts": {
|
||||
"TS2339": 2
|
||||
},
|
||||
"src/app/api/providers/[id]/login/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/[id]/models/route.ts": {
|
||||
"TS2367": 1,
|
||||
"TS2339": 2,
|
||||
"TS2322": 2,
|
||||
"TS2554": 2,
|
||||
"TS2345": 3
|
||||
},
|
||||
"src/app/api/providers/[id]/refresh-cursor/route.ts": {
|
||||
"TS2352": 1
|
||||
},
|
||||
"src/app/api/providers/[id]/refresh/route.ts": {
|
||||
"TS2345": 1,
|
||||
"TS2698": 1,
|
||||
"TS2339": 6
|
||||
},
|
||||
"src/app/api/providers/[id]/sync-models/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/providers/[id]/test/route.ts": {
|
||||
"TS2362": 1,
|
||||
"TS2698": 1
|
||||
},
|
||||
"src/app/api/providers/free-onboarding/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/providers/health-autopilot/actions/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/providers/route.ts": {
|
||||
"TS2352": 1,
|
||||
"TS2322": 2,
|
||||
"TS2345": 4
|
||||
},
|
||||
"src/app/api/providers/test-batch/route.ts": {
|
||||
"TS2345": 4
|
||||
},
|
||||
"src/app/api/providers/validate/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/[sessionId]/cancel/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/[sessionId]/code/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/[sessionId]/identity/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/[sessionId]/resend/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/[sessionId]/status/route.ts": {
|
||||
"TS2739": 1
|
||||
},
|
||||
"src/app/api/providers/volcengine-plan/connect/route.ts": {
|
||||
"TS2739": 1,
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/radar/local-model-state/route.ts": {
|
||||
"TS2339": 4
|
||||
},
|
||||
"src/app/api/resilience/model-cooldowns/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/services/_shared/installRoute.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/settings/cache-config/route.ts": {
|
||||
"TS2339": 1,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/settings/database/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/settings/models-dev/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/settings/obsidian/webdav/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/settings/proxies/bulk-import/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/settings/proxy/cloudflare-deploy/route.ts": {
|
||||
"TS2769": 1,
|
||||
"TS2322": 2
|
||||
},
|
||||
"src/app/api/settings/proxy/deno-deploy/route.ts": {
|
||||
"TS2322": 4
|
||||
},
|
||||
"src/app/api/settings/proxy/vercel-deploy/route.ts": {
|
||||
"TS2322": 3
|
||||
},
|
||||
"src/app/api/settings/reasoning-routing-rules/[id]/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/settings/reasoning-routing-rules/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/settings/reasoning-routing-rules/simulate/route.ts": {
|
||||
"TS2322": 1,
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/system/env/repair/route.ts": {
|
||||
"TS2578": 1,
|
||||
"TS2353": 3
|
||||
},
|
||||
"src/app/api/system/version/route.ts": {
|
||||
"TS2769": 1
|
||||
},
|
||||
"src/app/api/tools/agent-bridge/agents/[id]/detected-models/route.ts": {
|
||||
"TS2769": 1
|
||||
},
|
||||
"src/app/api/tools/traffic-inspector/internal/ingest/route.ts": {
|
||||
"TS1117": 2,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/tools/traffic-inspector/ws/route.ts": {
|
||||
"TS2578": 1
|
||||
},
|
||||
"src/app/api/translator/send/route.ts": {
|
||||
"TS2345": 1,
|
||||
"TS2322": 1,
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/translator/translate/route.ts": {
|
||||
"TS2345": 1,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/tunnels/cloudflared/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/tunnels/ngrok/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/tunnels/tailscale/routeUtils.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/usage/analytics/route.ts": {
|
||||
"TS2352": 15
|
||||
},
|
||||
"src/app/api/usage/combo-health-autopilot/route.ts": {
|
||||
"TS2769": 2
|
||||
},
|
||||
"src/app/api/v1/batches/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/v1/chatgpt-web/image/[id]/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/classify/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/v1/files/[id]/content/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/files/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/v1/images/edits/route.ts": {
|
||||
"TS2339": 21,
|
||||
"TS2322": 5
|
||||
},
|
||||
"src/app/api/v1/messages/count_tokens/route.ts": {
|
||||
"TS2339": 2,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/v1/music/generations/route.ts": {
|
||||
"TS2322": 1,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/ocr/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/provider-plugin-manifest/route.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/providers/[provider]/embeddings/route.ts": {
|
||||
"TS2339": 3,
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/v1/providers/[provider]/images/generations/route.ts": {
|
||||
"TS2339": 5
|
||||
},
|
||||
"src/app/api/v1/rerank/route.ts": {
|
||||
"TS2339": 2
|
||||
},
|
||||
"src/app/api/v1/segment/route.ts": {
|
||||
"TS2322": 1
|
||||
},
|
||||
"src/app/api/v1/session-leases/route.ts": {
|
||||
"TS2339": 4,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/app/api/v1/speech-to-text/route.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"src/app/api/v1/text-to-speech/[voiceId]/route.ts": {
|
||||
"TS2353": 1
|
||||
},
|
||||
"src/app/api/v1/web/fetch/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/v1beta/models/route.ts": {
|
||||
"TS2345": 1,
|
||||
"TS2538": 1
|
||||
},
|
||||
"src/app/api/version-manager/restart/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/version-manager/start/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/version-manager/stop/route.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/app/api/webhooks/[id]/route.ts": {
|
||||
"TS2554": 1
|
||||
},
|
||||
"src/app/api/webhooks/[id]/test/route.ts": {
|
||||
"TS2352": 2
|
||||
},
|
||||
"src/app/api/webhooks/route.ts": {
|
||||
"TS2554": 1,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/db/tierConfig.ts": {
|
||||
"TS2345": 2
|
||||
},
|
||||
"src/lib/guardrails/videoBridgeHelpers.ts": {
|
||||
"TS2488": 1,
|
||||
"TS2365": 2,
|
||||
"TS2322": 1,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/monitoring/comboHealthAutopilot.ts": {
|
||||
"TS2305": 1,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/monitoring/providerHealthAutopilot.ts": {
|
||||
"TS2352": 4
|
||||
},
|
||||
"src/lib/omnirouteStatus.ts": {
|
||||
"TS2322": 1,
|
||||
"TS2558": 1
|
||||
},
|
||||
"src/lib/providerModels/managedModelImport.ts": {
|
||||
"TS2352": 4
|
||||
},
|
||||
"src/lib/proxySubscription/parse.ts": {
|
||||
"TS2345": 3
|
||||
},
|
||||
"src/lib/quota/quotaAnalytics.ts": {
|
||||
"TS2769": 1
|
||||
},
|
||||
"src/lib/quota/quotaResetTimers.ts": {
|
||||
"TS2769": 2
|
||||
},
|
||||
"src/lib/usage/comboForecast.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/usage/comboHealth.ts": {
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/usage/comboScoringInspector.ts": {
|
||||
"TS2352": 1,
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/lib/usage/providerWindowCosts.ts": {
|
||||
"TS2322": 2,
|
||||
"TS2558": 5,
|
||||
"TS2339": 12,
|
||||
"TS2345": 1
|
||||
},
|
||||
"src/lib/vscode/modelPresentation.ts": {
|
||||
"TS2554": 1
|
||||
},
|
||||
"src/lib/ws/handshake.ts": {
|
||||
"TS2339": 1
|
||||
},
|
||||
"src/mitm/detection/index.ts": {
|
||||
"TS2741": 1
|
||||
},
|
||||
"src/mitm/inspector/httpProxyServer.ts": {
|
||||
"TS2769": 1
|
||||
},
|
||||
"src/shared/schemas/cliCatalog.ts": {
|
||||
"TS2554": 2
|
||||
}
|
||||
}
|
||||
145
scripts/check/check-api-typecheck.mjs
Normal file
145
scripts/check/check-api-typecheck.mjs
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/check/check-api-typecheck.mjs
|
||||
// API-route-scoped typecheck gate (#11601).
|
||||
//
|
||||
// `typecheck:core` uses a curated file allowlist, the dashboard typecheck gate
|
||||
// only covers src/app/(dashboard)/**, and Next builds ignore TypeScript build
|
||||
// errors. That leaves src/app/api/** without a blocking typecheck gate.
|
||||
//
|
||||
// This gate runs `tsc` scoped to src/app/api/**/*.{ts,tsx} via
|
||||
// tsconfig.typecheck-api.json and compares live diagnostics against a frozen
|
||||
// per-file/per-TS-code count baseline. New diagnostics or count increases fail;
|
||||
// reductions are reported as improvements and can be ratcheted with --update.
|
||||
//
|
||||
// Run:
|
||||
// node scripts/check/check-api-typecheck.mjs
|
||||
// node scripts/check/check-api-typecheck.mjs --update
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const TSCONFIG = path.join(ROOT, "tsconfig.typecheck-api.json");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/api-typecheck-baseline.json");
|
||||
const UPDATE = process.argv.includes("--update");
|
||||
|
||||
const TSC_ERROR_LINE = /^(.+?)\((\d+),(\d+)\): error (TS\d+):/;
|
||||
|
||||
export function parseTscOutput(raw) {
|
||||
const counts = {};
|
||||
for (const line of String(raw).split("\n")) {
|
||||
const match = TSC_ERROR_LINE.exec(line);
|
||||
if (!match) continue;
|
||||
const [, file, , , code] = match;
|
||||
if (!counts[file]) counts[file] = {};
|
||||
counts[file][code] = (counts[file][code] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function diffAgainstBaseline(live, baseline) {
|
||||
const regressions = [];
|
||||
const improvements = [];
|
||||
|
||||
for (const [file, codes] of Object.entries(live)) {
|
||||
for (const [code, liveCount] of Object.entries(codes)) {
|
||||
const baselineCount = (baseline[file] && baseline[file][code]) || 0;
|
||||
if (liveCount > baselineCount) {
|
||||
regressions.push({ file, code, liveCount, baselineCount });
|
||||
} else if (liveCount < baselineCount) {
|
||||
improvements.push({ file, code, liveCount, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, codes] of Object.entries(baseline)) {
|
||||
for (const [code, baselineCount] of Object.entries(codes)) {
|
||||
const liveCount = (live[file] && live[file][code]) || 0;
|
||||
if (liveCount === 0 && baselineCount > 0) {
|
||||
improvements.push({ file, code, liveCount: 0, baselineCount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { regressions, improvements };
|
||||
}
|
||||
|
||||
function runTsc() {
|
||||
try {
|
||||
return execFileSync(
|
||||
process.platform === "win32" ? "npx.cmd" : "npx",
|
||||
["tsc", "--pretty", "false", "--noEmit", "-p", TSCONFIG],
|
||||
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024, cwd: ROOT }
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.stdout) return String(err.stdout);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
if (!fs.existsSync(BASELINE_PATH)) return {};
|
||||
return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8"));
|
||||
}
|
||||
|
||||
function writeBaseline(counts) {
|
||||
fs.writeFileSync(BASELINE_PATH, JSON.stringify(counts, null, 2) + "\n");
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(TSCONFIG)) {
|
||||
process.stderr.write(`[api-typecheck] FAIL — tsconfig not found at ${TSCONFIG}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
console.log("[api-typecheck] Running tsc scoped to src/app/api/**…");
|
||||
const stdout = runTsc();
|
||||
const live = parseTscOutput(stdout);
|
||||
const baseline = loadBaseline();
|
||||
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||
|
||||
const liveErrorCount = Object.values(live).reduce(
|
||||
(sum, codes) => sum + Object.values(codes).reduce((s, c) => s + c, 0),
|
||||
0
|
||||
);
|
||||
console.log(`apiTypecheckErrors=${liveErrorCount}`);
|
||||
|
||||
if (UPDATE) {
|
||||
writeBaseline(live);
|
||||
console.log(`[api-typecheck] baseline rewritten (${liveErrorCount} errors frozen).`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (improvements.length > 0) {
|
||||
console.log(
|
||||
`[api-typecheck] ${improvements.length} baselined error(s) no longer present ` +
|
||||
`— run 'node scripts/check/check-api-typecheck.mjs --update' to ratchet the baseline down:\n` +
|
||||
improvements
|
||||
.map((i) => ` - ${i.file} ${i.code} (baseline ${i.baselineCount} -> live ${i.liveCount})`)
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (regressions.length > 0) {
|
||||
process.stderr.write(
|
||||
`[api-typecheck] FAIL — ${regressions.length} new/regressed TypeScript error(s) ` +
|
||||
`under src/app/api/ not covered by the frozen baseline:\n` +
|
||||
regressions
|
||||
.map((r) => ` ✗ ${r.file} ${r.code} (baseline ${r.baselineCount}, live ${r.liveCount})`)
|
||||
.join("\n") +
|
||||
`\n\nFix new API-route TypeScript regressions rather than widening the baseline.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[api-typecheck] OK — ${liveErrorCount} pre-existing error(s), all within frozen baseline.`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
||||
main();
|
||||
}
|
||||
@@ -38,11 +38,13 @@ export async function GET(request: Request) {
|
||||
const apiKeyId = searchParams.get("apiKeyId") || undefined;
|
||||
const type = (searchParams.get("type") as any) || undefined;
|
||||
const sessionId = searchParams.get("sessionId") || undefined;
|
||||
const category = searchParams.get("category")?.trim() || undefined;
|
||||
|
||||
const result = await memoryManager.list({
|
||||
apiKeyId,
|
||||
type,
|
||||
sessionId,
|
||||
category,
|
||||
query,
|
||||
limit: paginationParams.limit,
|
||||
offset:
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { callCloudWithMachineId } from "@/shared/utils/cloud";
|
||||
import { handleChat } from "@/sse/handlers/chat";
|
||||
import { generateRequestId } from "@/shared/utils/requestId";
|
||||
import { resolveIncomingCorrelationId } from "@/shared/utils/correlationPreserve.ts";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
|
||||
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";
|
||||
@@ -243,8 +244,13 @@ export async function POST(request) {
|
||||
// paths) drop the meta the docs promise.
|
||||
const compressionRequestHeader = readCompressionRequestHeader(request);
|
||||
|
||||
// #11739: preserve caller-provided X-Correlation-Id when present; generate only when absent.
|
||||
const callerCorrelationId = resolveIncomingCorrelationId(
|
||||
request.headers.get("x-correlation-id")
|
||||
);
|
||||
|
||||
if (wantsStreaming) {
|
||||
const reqId = generateRequestId();
|
||||
const reqId = callerCorrelationId ?? generateRequestId();
|
||||
// Wrap the real handler response, not the synthetic early-keepalive response. If the
|
||||
// client cancels while handleChat is still pending, earlyStreamKeepalive will cancel the
|
||||
// eventual handler body; only that confirmed cleanup releases heavyweight capacity.
|
||||
@@ -265,7 +271,7 @@ export async function POST(request) {
|
||||
|
||||
return finishAdmission(
|
||||
withCompressionHeaderEcho(
|
||||
await handleChat(request, null, parsedBody),
|
||||
await handleChat(request, null, parsedBody, callerCorrelationId ?? undefined),
|
||||
compressionRequestHeader
|
||||
)
|
||||
);
|
||||
|
||||
@@ -24,15 +24,6 @@ export interface OneproxyProxyRecord {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OneproxyStats {
|
||||
total: number;
|
||||
active: number;
|
||||
avgQuality: number | null;
|
||||
lastValidated: string | null;
|
||||
byProtocol: Array<{ protocol: string; count: number }>;
|
||||
byCountry: Array<{ countryCode: string; count: number }>;
|
||||
}
|
||||
|
||||
interface OneproxyUpsertInput {
|
||||
ip: string;
|
||||
port: number;
|
||||
@@ -73,19 +64,6 @@ function mapProxyRow(row: unknown): OneproxyProxyRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function mapStatsRow(row: unknown) {
|
||||
const r = toRecord(row);
|
||||
return {
|
||||
total: Number(r.total) || 0,
|
||||
active: Number(r.active) || 0,
|
||||
avgQuality:
|
||||
r.avg_quality !== null && r.avg_quality !== undefined
|
||||
? Math.round(Number(r.avg_quality) * 100) / 100
|
||||
: null,
|
||||
lastValidated: typeof r.last_validated === "string" ? r.last_validated : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listOneproxyProxies(options?: {
|
||||
protocol?: string;
|
||||
countryCode?: string;
|
||||
@@ -121,47 +99,6 @@ export async function listOneproxyProxies(options?: {
|
||||
return rows.map(mapProxyRow);
|
||||
}
|
||||
|
||||
export async function getOneproxyStats(): Promise<OneproxyStats> {
|
||||
const db = getDbInstance();
|
||||
|
||||
const statsRow = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
||||
AVG(quality_score) as avg_quality,
|
||||
MAX(last_validated) as last_validated
|
||||
FROM proxy_registry WHERE source = 'oneproxy'`
|
||||
)
|
||||
.get();
|
||||
|
||||
const stats = mapStatsRow(statsRow);
|
||||
|
||||
const byProtocol = db
|
||||
.prepare(
|
||||
"SELECT type as protocol, COUNT(*) as count FROM proxy_registry WHERE source = 'oneproxy' GROUP BY type ORDER BY count DESC"
|
||||
)
|
||||
.all() as Array<JsonRecord>;
|
||||
|
||||
const byCountry = db
|
||||
.prepare(
|
||||
"SELECT country_code as countryCode, COUNT(*) as count FROM proxy_registry WHERE source = 'oneproxy' AND country_code IS NOT NULL GROUP BY country_code ORDER BY count DESC LIMIT 20"
|
||||
)
|
||||
.all() as Array<JsonRecord>;
|
||||
|
||||
return {
|
||||
...stats,
|
||||
byProtocol: byProtocol.map((r) => ({
|
||||
protocol: String(r.protocol || "unknown"),
|
||||
count: Number(r.count) || 0,
|
||||
})),
|
||||
byCountry: byCountry.map((r) => ({
|
||||
countryCode: String(r.countryCode || "unknown"),
|
||||
count: Number(r.count) || 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function upsertOneproxyProxy(
|
||||
input: OneproxyUpsertInput
|
||||
): Promise<{ proxy: OneproxyProxyRecord | null; action: "created" | "updated" }> {
|
||||
@@ -236,22 +173,6 @@ export async function getOneproxyProxyById(id: string): Promise<OneproxyProxyRec
|
||||
return mapProxyRow(row);
|
||||
}
|
||||
|
||||
export async function deleteOneproxyProxy(id: string): Promise<boolean> {
|
||||
const db = getDbInstance();
|
||||
const result = db
|
||||
.prepare("DELETE FROM proxy_registry WHERE id = ? AND source = 'oneproxy'")
|
||||
.run(id);
|
||||
backupDbFile("pre-write");
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
export async function clearAllOneproxyProxies(): Promise<number> {
|
||||
const db = getDbInstance();
|
||||
const result = db.prepare("DELETE FROM proxy_registry WHERE source = 'oneproxy'").run();
|
||||
backupDbFile("pre-write");
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
export async function getOneproxyProxyForRotation(options?: {
|
||||
strategy?: "random" | "quality" | "sequential";
|
||||
}): Promise<OneproxyProxyRecord | null> {
|
||||
|
||||
@@ -369,7 +369,7 @@ describe("GenericMemoryBackend", () => {
|
||||
|
||||
test("applies custom query param names", async () => {
|
||||
const b = createBackend({
|
||||
queryParams: { apiKeyId: "owner", limit: "count" },
|
||||
queryParams: { apiKeyId: "owner", category: "memoryCategory", limit: "count" },
|
||||
});
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url: string) => {
|
||||
if (url.toString().endsWith("/health")) {
|
||||
@@ -378,14 +378,16 @@ describe("GenericMemoryBackend", () => {
|
||||
return new Response(JSON.stringify({ data: [], total: 0, byType: {} }), { status: 200 });
|
||||
});
|
||||
|
||||
await b.list({ apiKeyId: "key-1", limit: 5 });
|
||||
await b.list({ apiKeyId: "key-1", category: "codegraph", limit: 5 });
|
||||
const listUrl = fetchMock.mock.calls.find(
|
||||
([url]) => !url.toString().endsWith("/health")
|
||||
)![0] as string;
|
||||
|
||||
expect(listUrl).toContain("owner=key-1");
|
||||
expect(listUrl).toContain("memoryCategory=codegraph");
|
||||
expect(listUrl).toContain("count=5");
|
||||
expect(listUrl).not.toContain("apiKeyId=");
|
||||
expect(listUrl).not.toContain("category=");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface MemoryFilter {
|
||||
apiKeyId?: string;
|
||||
type?: MemoryType;
|
||||
sessionId?: string;
|
||||
category?: string;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface GenericBackendConfig {
|
||||
maxTokens?: string; // default: "maxTokens"
|
||||
type?: string; // default: "type"
|
||||
sessionId?: string; // default: "sessionId"
|
||||
category?: string; // default: "category"
|
||||
orderBy?: string; // default: "orderBy"
|
||||
orderDir?: string; // default: "orderDir"
|
||||
options?: string; // default: "options"
|
||||
@@ -181,6 +182,7 @@ export class GenericMemoryBackend implements MemoryBackend {
|
||||
maxTokens: this.config.queryParams?.maxTokens ?? "maxTokens",
|
||||
type: this.config.queryParams?.type ?? "type",
|
||||
sessionId: this.config.queryParams?.sessionId ?? "sessionId",
|
||||
category: this.config.queryParams?.category ?? "category",
|
||||
orderBy: this.config.queryParams?.orderBy ?? "orderBy",
|
||||
orderDir: this.config.queryParams?.orderDir ?? "orderDir",
|
||||
options: this.config.queryParams?.options ?? "options",
|
||||
@@ -222,6 +224,7 @@ export class GenericMemoryBackend implements MemoryBackend {
|
||||
if (filter.apiKeyId) out[qp.apiKeyId] = filter.apiKeyId;
|
||||
if (filter.type) out[qp.type] = filter.type;
|
||||
if (filter.sessionId) out[qp.sessionId] = filter.sessionId;
|
||||
if (filter.category) out[qp.category] = filter.category;
|
||||
if (filter.limit !== undefined) out[qp.limit] = String(filter.limit);
|
||||
if (filter.offset !== undefined) out[qp.offset] = String(filter.offset);
|
||||
if (filter.orderBy) out[qp.orderBy] = filter.orderBy;
|
||||
|
||||
@@ -231,6 +231,7 @@ export class ObsidianBackend implements MemoryBackend {
|
||||
if (filter.apiKeyId && memory.apiKeyId !== filter.apiKeyId) continue;
|
||||
if (filter.type && memory.type !== filter.type) continue;
|
||||
if (filter.sessionId && memory.sessionId !== filter.sessionId) continue;
|
||||
if (filter.category && memory.metadata.category !== filter.category) continue;
|
||||
|
||||
memories.push(memory);
|
||||
byType[memory.type] = (byType[memory.type] || 0) + 1;
|
||||
|
||||
@@ -64,6 +64,7 @@ export class SQLiteBackend implements MemoryBackend {
|
||||
apiKeyId: filter.apiKeyId,
|
||||
type: filter.type,
|
||||
sessionId: filter.sessionId,
|
||||
category: filter.category,
|
||||
query: filter.query,
|
||||
limit: filter.limit,
|
||||
offset: filter.offset,
|
||||
|
||||
@@ -468,6 +468,7 @@ export async function listMemories(filters: {
|
||||
apiKeyId?: string;
|
||||
type?: MemoryType;
|
||||
sessionId?: string;
|
||||
category?: string;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
@@ -494,6 +495,13 @@ export async function listMemories(filters: {
|
||||
whereParams.push(filters.sessionId);
|
||||
}
|
||||
|
||||
if (typeof filters.category === "string" && filters.category.trim().length > 0) {
|
||||
whereClauses.push(
|
||||
"json_extract(CASE WHEN json_valid(metadata) THEN metadata ELSE '{}' END, '$.category') = ?"
|
||||
);
|
||||
whereParams.push(filters.category.trim());
|
||||
}
|
||||
|
||||
if (typeof filters.query === "string" && filters.query.trim().length > 0) {
|
||||
const likeQuery = `%${filters.query.trim().toLowerCase()}%`;
|
||||
whereClauses.push("(LOWER(content) LIKE ? OR LOWER(key) LIKE ?)");
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { setQuotaCache } from "@/domain/quotaCache";
|
||||
import { buildClaudeExtraUsageConnectionUpdate } from "@/lib/providers/claudeExtraUsage";
|
||||
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
|
||||
import { clearRecoveredProviderState } from "@/sse/services/auth";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
@@ -860,9 +859,6 @@ async function fetchLiveProviderLimitsWithOptions(
|
||||
connection: ProviderConnectionLike;
|
||||
usage: JsonRecord;
|
||||
}> {
|
||||
if (await isConnectionUnavailableToAuxiliaryActivity(connectionId)) {
|
||||
throw withStatus(new Error("Usage refresh deferred while an exclusive lease is active"), 409);
|
||||
}
|
||||
let connection = (await getProviderConnectionById(
|
||||
connectionId
|
||||
)) as unknown as ProviderConnectionLike | null;
|
||||
@@ -1073,16 +1069,7 @@ export async function syncAllProviderLimits(
|
||||
const connectionRows = (await getProviderConnections({
|
||||
isActive: true,
|
||||
})) as unknown as ProviderConnectionLike[];
|
||||
const connections = (
|
||||
await Promise.all(
|
||||
connectionRows.map(async (connection) => ({
|
||||
connection,
|
||||
blocked: await isConnectionUnavailableToAuxiliaryActivity(connection.id),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.filter(({ connection, blocked }) => isSupportedUsageConnection(connection) && !blocked)
|
||||
.map(({ connection }) => connection);
|
||||
const connections = connectionRows.filter(isSupportedUsageConnection);
|
||||
const cacheEntries: Array<{ connectionId: string; entry: ProviderLimitsCacheEntry }> = [];
|
||||
const caches: Record<string, ProviderLimitsCacheEntry> = {};
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Agent, fetch as undiciFetch } from "undici";
|
||||
import {
|
||||
type OutboundUrlGuardMode,
|
||||
isPrivateHost,
|
||||
parseAndValidateNonMetadataUrl,
|
||||
parseAndValidatePublicUrl,
|
||||
parseOutboundUrl,
|
||||
} from "@/shared/network/outboundUrlGuard";
|
||||
@@ -51,7 +52,9 @@ export type RemoteMediaFetchOptions = RemoteImageFetchOptions;
|
||||
export type RemoteMediaFetchResult = RemoteImageFetchResult;
|
||||
|
||||
function validateRemoteImageUrl(input: string | URL, guard: OutboundUrlGuardMode) {
|
||||
return guard === "public-only" ? parseAndValidatePublicUrl(input) : parseOutboundUrl(input);
|
||||
if (guard === "public-only") return parseAndValidatePublicUrl(input);
|
||||
if (guard === "block-metadata") return parseAndValidateNonMetadataUrl(input);
|
||||
return parseOutboundUrl(input);
|
||||
}
|
||||
|
||||
function requireHttps(url: URL, enabled: boolean): URL {
|
||||
|
||||
12
src/shared/utils/correlationPreserve.ts
Normal file
12
src/shared/utils/correlationPreserve.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Resolve caller-provided X-Correlation-Id for preservation (#11739).
|
||||
* Returns sanitized value when present and within bounds (1-256 chars), otherwise null.
|
||||
* Strips CRLF to prevent header injection, trims whitespace.
|
||||
*/
|
||||
export function resolveIncomingCorrelationId(
|
||||
headerValue: string | null | undefined
|
||||
): string | null {
|
||||
const raw = (headerValue ?? "").trim().replace(/[\r\n]/g, "");
|
||||
if (raw.length === 0 || raw.length > 256) return null;
|
||||
return raw;
|
||||
}
|
||||
130
tests/integration/memory-category-filter.test.ts
Normal file
130
tests/integration/memory-category-filter.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-memory-category-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { updateSettings } = await import("../../src/lib/db/settings.ts");
|
||||
const localDb = { updateSettings };
|
||||
const { GET } = await import("../../src/app/api/memory/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function insertMemoryRow({
|
||||
id,
|
||||
category,
|
||||
type = "factual",
|
||||
sessionId = "session-a",
|
||||
metadata,
|
||||
}: {
|
||||
id: string;
|
||||
category?: string;
|
||||
type?: string;
|
||||
sessionId?: string;
|
||||
metadata?: string;
|
||||
}) {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
const serializedMetadata =
|
||||
metadata ?? JSON.stringify(category === undefined ? {} : { category });
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO memories (
|
||||
id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
id,
|
||||
"key-a",
|
||||
sessionId,
|
||||
type,
|
||||
`memory:${id}`,
|
||||
`content ${id}`,
|
||||
serializedMetadata,
|
||||
now,
|
||||
now,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function getMemories(query = "") {
|
||||
const response = await GET(new Request(`http://localhost/api/memory${query}`));
|
||||
assert.equal(response.status, 200);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
await localDb.updateSettings({ requireLogin: false });
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("GET /api/memory filters by metadata.category and keeps totals in sync", async () => {
|
||||
insertMemoryRow({ id: "codegraph-1", category: "codegraph", type: "factual" });
|
||||
insertMemoryRow({ id: "codegraph-2", category: "codegraph", type: "semantic" });
|
||||
insertMemoryRow({ id: "decision-1", category: "decision", type: "episodic" });
|
||||
|
||||
const body = await getMemories("?category=codegraph");
|
||||
|
||||
assert.deepEqual(
|
||||
body.data.map((memory: { id: string }) => memory.id).sort(),
|
||||
["codegraph-1", "codegraph-2"]
|
||||
);
|
||||
assert.equal(body.total, 2);
|
||||
assert.equal(body.stats.total, 2);
|
||||
assert.deepEqual(body.stats.byType, { factual: 1, semantic: 1 });
|
||||
});
|
||||
|
||||
test("GET /api/memory composes category with existing filters and pagination", async () => {
|
||||
insertMemoryRow({ id: "keep-1", category: "decision", type: "episodic", sessionId: "session-a" });
|
||||
insertMemoryRow({ id: "keep-2", category: "decision", type: "episodic", sessionId: "session-a" });
|
||||
insertMemoryRow({ id: "wrong-type", category: "decision", type: "factual", sessionId: "session-a" });
|
||||
insertMemoryRow({ id: "wrong-session", category: "decision", type: "episodic", sessionId: "session-b" });
|
||||
|
||||
const body = await getMemories(
|
||||
"?category=decision&type=episodic&sessionId=session-a&limit=1&offset=1"
|
||||
);
|
||||
|
||||
assert.equal(body.data.length, 1);
|
||||
assert.ok(["keep-1", "keep-2"].includes(body.data[0].id));
|
||||
assert.equal(body.total, 2);
|
||||
assert.equal(body.stats.total, 2);
|
||||
assert.deepEqual(body.stats.byType, { episodic: 2 });
|
||||
});
|
||||
|
||||
test("GET /api/memory returns no matches for an unknown category", async () => {
|
||||
insertMemoryRow({ id: "known", category: "codegraph" });
|
||||
|
||||
const body = await getMemories("?category=missing");
|
||||
|
||||
assert.deepEqual(body.data, []);
|
||||
assert.equal(body.total, 0);
|
||||
assert.equal(body.stats.total, 0);
|
||||
assert.deepEqual(body.stats.byType, {});
|
||||
});
|
||||
|
||||
test("GET /api/memory ignores malformed metadata when applying category filter", async () => {
|
||||
insertMemoryRow({ id: "valid", category: "codegraph" });
|
||||
insertMemoryRow({ id: "malformed", metadata: "{not-json" });
|
||||
|
||||
const filtered = await getMemories("?category=codegraph");
|
||||
assert.deepEqual(
|
||||
filtered.data.map((memory: { id: string }) => memory.id),
|
||||
["valid"]
|
||||
);
|
||||
assert.equal(filtered.total, 1);
|
||||
|
||||
const unfiltered = await getMemories();
|
||||
assert.equal(unfiltered.total, 2);
|
||||
});
|
||||
87
tests/unit/build/check-api-typecheck.test.ts
Normal file
87
tests/unit/build/check-api-typecheck.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// tests/unit/build/check-api-typecheck.test.ts
|
||||
// Hermetic tests for the API typecheck gate's parsing and baseline-diff logic.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
parseTscOutput,
|
||||
diffAgainstBaseline,
|
||||
} from "../../../scripts/check/check-api-typecheck.mjs";
|
||||
|
||||
test("parseTscOutput: parses an API-route TS2554 regression", () => {
|
||||
const raw =
|
||||
`src/app/api/v1/models/route.ts(42,7): error TS2554: Expected 2 arguments, but got 3.\n` +
|
||||
`src/app/api/v1/models/route.ts(52,7): error TS2554: Expected 2 arguments, but got 3.\n`;
|
||||
|
||||
assert.deepEqual(parseTscOutput(raw), {
|
||||
"src/app/api/v1/models/route.ts": { TS2554: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
test("parseTscOutput: ignores non-error lines", () => {
|
||||
const raw =
|
||||
`src/app/api/foo/route.ts(1,1): error TS2339: Property 'bar' does not exist.\n` +
|
||||
`Found 1 error in 1 file.\n`;
|
||||
|
||||
assert.deepEqual(parseTscOutput(raw), {
|
||||
"src/app/api/foo/route.ts": { TS2339: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test("parseTscOutput: returns an empty map for clean output", () => {
|
||||
assert.deepEqual(parseTscOutput("Found 0 errors.\n"), {});
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: flags a new API diagnostic", () => {
|
||||
const live = { "src/app/api/v1/models/route.ts": { TS2554: 1 } };
|
||||
const { regressions, improvements } = diffAgainstBaseline(live, {});
|
||||
|
||||
assert.deepEqual(regressions, [
|
||||
{
|
||||
file: "src/app/api/v1/models/route.ts",
|
||||
code: "TS2554",
|
||||
liveCount: 1,
|
||||
baselineCount: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(improvements.length, 0);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: accepts an unchanged frozen diagnostic count", () => {
|
||||
const baseline = { "src/app/api/foo/route.ts": { TS2339: 2 } };
|
||||
const live = { "src/app/api/foo/route.ts": { TS2339: 2 } };
|
||||
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||
|
||||
assert.equal(regressions.length, 0);
|
||||
assert.equal(improvements.length, 0);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: fails a count increase", () => {
|
||||
const baseline = { "src/app/api/foo/route.ts": { TS2339: 1 } };
|
||||
const live = { "src/app/api/foo/route.ts": { TS2339: 2 } };
|
||||
const { regressions } = diffAgainstBaseline(live, baseline);
|
||||
|
||||
assert.equal(regressions.length, 1);
|
||||
assert.equal(regressions[0].baselineCount, 1);
|
||||
assert.equal(regressions[0].liveCount, 2);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: reports a count decrease as an improvement", () => {
|
||||
const baseline = { "src/app/api/foo/route.ts": { TS2339: 2 } };
|
||||
const live = { "src/app/api/foo/route.ts": { TS2339: 1 } };
|
||||
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
|
||||
|
||||
assert.equal(regressions.length, 0);
|
||||
assert.equal(improvements.length, 1);
|
||||
assert.equal(improvements[0].liveCount, 1);
|
||||
});
|
||||
|
||||
test("diffAgainstBaseline: reports a disappeared diagnostic as an improvement", () => {
|
||||
const baseline = { "src/app/api/foo/route.ts": { TS2339: 2 } };
|
||||
const { regressions, improvements } = diffAgainstBaseline({}, baseline);
|
||||
|
||||
assert.equal(regressions.length, 0);
|
||||
assert.equal(improvements.length, 1);
|
||||
assert.equal(improvements[0].liveCount, 0);
|
||||
assert.equal(improvements[0].baselineCount, 2);
|
||||
});
|
||||
37
tests/unit/chat-completions-correlation.test.ts
Normal file
37
tests/unit/chat-completions-correlation.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { resolveIncomingCorrelationId } = await import(
|
||||
"../../src/shared/utils/correlationPreserve.ts"
|
||||
);
|
||||
|
||||
test("resolveIncomingCorrelationId preserves valid caller ID", () => {
|
||||
assert.equal(resolveIncomingCorrelationId("caller-correlation"), "caller-correlation");
|
||||
assert.equal(resolveIncomingCorrelationId(" spaced-id "), "spaced-id");
|
||||
assert.equal(resolveIncomingCorrelationId("abc-123_XYZ"), "abc-123_XYZ");
|
||||
});
|
||||
|
||||
test("resolveIncomingCorrelationId sanitizes header injection", () => {
|
||||
assert.equal(resolveIncomingCorrelationId("evil\r\nInjected: true"), "evilInjected: true");
|
||||
assert.equal(resolveIncomingCorrelationId("with\nnewline"), "withnewline");
|
||||
assert.equal(resolveIncomingCorrelationId("with\rcarriage"), "withcarriage");
|
||||
});
|
||||
|
||||
test("resolveIncomingCorrelationId rejects empty and overlong", () => {
|
||||
assert.equal(resolveIncomingCorrelationId(null), null);
|
||||
assert.equal(resolveIncomingCorrelationId(undefined), null);
|
||||
assert.equal(resolveIncomingCorrelationId(""), null);
|
||||
assert.equal(resolveIncomingCorrelationId(" "), null);
|
||||
const long = "a".repeat(257);
|
||||
assert.equal(resolveIncomingCorrelationId(long), null);
|
||||
assert.equal(resolveIncomingCorrelationId("a".repeat(256)), "a".repeat(256));
|
||||
});
|
||||
|
||||
test("resolveIncomingCorrelationId trims before length check", () => {
|
||||
// 256 chars plus surrounding spaces should still be valid after trim
|
||||
const spacedLong = " " + "a".repeat(256) + " ";
|
||||
assert.equal(resolveIncomingCorrelationId(spacedLong), "a".repeat(256));
|
||||
// 257 after trim should be rejected
|
||||
const spacedTooLong = " " + "a".repeat(257) + " ";
|
||||
assert.equal(resolveIncomingCorrelationId(spacedTooLong), null);
|
||||
});
|
||||
@@ -24,12 +24,14 @@ const translator = await import("../../src/app/api/translator/send/route.ts");
|
||||
const translatorPreview = await import("../../src/app/api/translator/translate/route.ts");
|
||||
const modelTests = await import("../../src/lib/api/modelTestRunner.ts");
|
||||
const vnc = await import("../../src/lib/vncSession/service.ts");
|
||||
const usageRoute = await import("../../src/app/api/usage/[connectionId]/route.ts");
|
||||
const providerLimits = await import("../../src/lib/usage/providerLimits.ts");
|
||||
|
||||
const OWNER = "vlo_UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU";
|
||||
|
||||
async function seedConnection(name: string): Promise<{ id: string }> {
|
||||
async function seedConnection(name: string, provider = "openai"): Promise<{ id: string }> {
|
||||
return (await providers.createProviderConnection({
|
||||
provider: "openai",
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name,
|
||||
apiKey: `sk-${name}`,
|
||||
@@ -52,6 +54,10 @@ async function resetStorage(): Promise<void> {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
externalCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
externalCalls += 1;
|
||||
throw new Error("unexpected external provider/model call");
|
||||
};
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
@@ -190,3 +196,142 @@ test("browser-login harvest rejects ACTIVE leased connections before credential
|
||||
);
|
||||
assert.equal(externalCalls, 0);
|
||||
});
|
||||
|
||||
test("usage refresh allows FREE lease-reserved connection and queries provider quota", async () => {
|
||||
const connection = await seedConnection("deepseek-free-lease", "deepseek");
|
||||
await markLeaseOnly(connection.id);
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
externalCalls += 1;
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "https://api.deepseek.com/user/balance") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{
|
||||
currency: "USD",
|
||||
total_balance: "10.00",
|
||||
granted_balance: "0.00",
|
||||
topped_up_balance: "10.00",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw new Error(`unexpected fetch call: ${url}`);
|
||||
};
|
||||
|
||||
const response = await usageRoute.GET(
|
||||
new Request(`http://omniroute.local/api/usage/${connection.id}`),
|
||||
{ params: Promise.resolve({ connectionId: connection.id }) }
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(externalCalls, 1);
|
||||
const data = (await response.json()) as { quotas?: { credits_usd?: { remaining?: number } } };
|
||||
assert.equal(data?.quotas?.credits_usd?.remaining, 10);
|
||||
});
|
||||
|
||||
test("usage refresh allows ACTIVE leased connection and queries provider quota", async () => {
|
||||
const connection = await seedConnection("deepseek-active-lease", "deepseek");
|
||||
await markLeaseOnly(connection.id);
|
||||
const acquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER,
|
||||
apiKeyId: "managed-key",
|
||||
provider: "deepseek",
|
||||
connectionId: connection.id,
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
externalCalls += 1;
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "https://api.deepseek.com/user/balance") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{
|
||||
currency: "USD",
|
||||
total_balance: "10.00",
|
||||
granted_balance: "0.00",
|
||||
topped_up_balance: "10.00",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw new Error(`unexpected fetch call: ${url}`);
|
||||
};
|
||||
|
||||
const response = await usageRoute.GET(
|
||||
new Request(`http://omniroute.local/api/usage/${connection.id}`),
|
||||
{ params: Promise.resolve({ connectionId: connection.id }) }
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(externalCalls, 1);
|
||||
const data = (await response.json()) as { quotas?: { credits_usd?: { remaining?: number } } };
|
||||
assert.equal(data?.quotas?.credits_usd?.remaining, 10);
|
||||
});
|
||||
|
||||
test("syncAllProviderLimits refreshes all active supported connections regardless of lease state", async () => {
|
||||
const freeConn = await seedConnection("deepseek-bulk-free", "deepseek");
|
||||
const activeConn = await seedConnection("deepseek-bulk-active", "deepseek");
|
||||
await markLeaseOnly(freeConn.id);
|
||||
await markLeaseOnly(activeConn.id);
|
||||
|
||||
const acquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: OWNER,
|
||||
apiKeyId: "managed-key",
|
||||
provider: "deepseek",
|
||||
connectionId: activeConn.id,
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
externalCalls += 1;
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "https://api.deepseek.com/user/balance") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{
|
||||
currency: "USD",
|
||||
total_balance: "10.00",
|
||||
granted_balance: "0.00",
|
||||
topped_up_balance: "10.00",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw new Error(`unexpected fetch call: ${url}`);
|
||||
};
|
||||
|
||||
const result = await providerLimits.syncAllProviderLimits({
|
||||
source: "manual",
|
||||
concurrency: 2,
|
||||
});
|
||||
|
||||
assert.ok(result.caches[freeConn.id]);
|
||||
assert.ok(result.caches[activeConn.id]);
|
||||
assert.equal(externalCalls, 2);
|
||||
});
|
||||
|
||||
@@ -71,20 +71,13 @@ test("connection verification skips an ACTIVE exclusive lease before any probe o
|
||||
assert.equal(row.last_error, null);
|
||||
});
|
||||
|
||||
test("model discovery, quota refresh, and reset-credit paths reject ACTIVE leased connections", async () => {
|
||||
test("model discovery and reset-credit paths reject ACTIVE leased connections", async () => {
|
||||
const response = await providerModels.GET(
|
||||
new Request("http://omniroute.local/api/providers/leased-test-connection/models"),
|
||||
{ params: { id: "leased-test-connection" } }
|
||||
);
|
||||
assert.equal(response.status, 409);
|
||||
|
||||
await assert.rejects(
|
||||
providerLimits.fetchLiveProviderLimits("leased-test-connection"),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
(error as Error & { status?: number }).status === 409 &&
|
||||
/exclusive lease/i.test(error.message)
|
||||
);
|
||||
await assert.rejects(
|
||||
codexResetCredits.listCodexResetCredits("leased-test-connection"),
|
||||
(error: unknown) =>
|
||||
@@ -94,3 +87,59 @@ test("model discovery, quota refresh, and reset-credit paths reject ACTIVE lease
|
||||
);
|
||||
assert.equal(externalCalls, 0);
|
||||
});
|
||||
|
||||
test("quota refresh proceeds on an ACTIVE leased usage-supported connection", async () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
`INSERT INTO provider_connections
|
||||
(id, provider, auth_type, name, api_key, is_active, test_status, created_at, updated_at)
|
||||
VALUES (?, ?, 'apikey', ?, ?, 1, 'active', ?, ?)`
|
||||
).run(
|
||||
"leased-quota-connection",
|
||||
"deepseek",
|
||||
"leased quota connection",
|
||||
"synthetic-deepseek-key",
|
||||
new Date().toISOString(),
|
||||
new Date().toISOString()
|
||||
);
|
||||
const acquired = leases.acquireExclusiveConnectionLease({
|
||||
leaseOwnerId: "vlo_QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ",
|
||||
apiKeyId: "managed-quota-key",
|
||||
provider: "deepseek",
|
||||
connectionId: "leased-quota-connection",
|
||||
});
|
||||
assert.equal(acquired.kind, "ACQUIRED");
|
||||
|
||||
externalCalls = 0;
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
externalCalls += 1;
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url === "https://api.deepseek.com/user/balance") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{
|
||||
currency: "USD",
|
||||
total_balance: "10.00",
|
||||
granted_balance: "0.00",
|
||||
topped_up_balance: "10.00",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
throw new Error(`unexpected fetch call: ${url}`);
|
||||
};
|
||||
|
||||
const result = await providerLimits.fetchLiveProviderLimits("leased-quota-connection");
|
||||
assert.equal(result.connection.id, "leased-quota-connection");
|
||||
const quotas = result.usage.quotas as { credits_usd?: { remaining?: number } };
|
||||
assert.equal(quotas?.credits_usd?.remaining, 10);
|
||||
assert.equal(externalCalls, 1);
|
||||
});
|
||||
|
||||
@@ -310,11 +310,14 @@ test("managed request surfaces are fenced centrally or rejected before independe
|
||||
"src/lib/api/modelTestRunner.ts",
|
||||
"src/lib/services/quotaAutoPing.ts",
|
||||
"src/lib/usage/codexResetCredits.ts",
|
||||
"src/lib/usage/providerLimits.ts",
|
||||
"src/lib/vncSession/service.ts",
|
||||
"src/lib/warmupScheduler.ts",
|
||||
"src/shared/services/modelSyncScheduler.ts",
|
||||
].map((file) => fs.readFileSync(path.join(REPO_ROOT, file), "utf8"));
|
||||
const unfencedUsageRefreshSource = fs.readFileSync(
|
||||
path.join(REPO_ROOT, "src/lib/usage/providerLimits.ts"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert.match(chat, /parseManagedLeaseRequestContext\(request\.headers\)/);
|
||||
assert.match(chat, /isManagedComboUnsupported/);
|
||||
@@ -329,6 +332,10 @@ test("managed request surfaces are fenced centrally or rejected before independe
|
||||
for (const source of auxiliaryIsolationSources) {
|
||||
assert.match(source, /isConnectionUnavailableToAuxiliaryActivity/);
|
||||
}
|
||||
// Usage/quota refresh is read-only admin telemetry (#11758) and must not inherit
|
||||
// the exclusive-lease auxiliary fence that blocks model tests, translation, VNC,
|
||||
// reset-credits, and warmup.
|
||||
assert.doesNotMatch(unfencedUsageRefreshSource, /isConnectionUnavailableToAuxiliaryActivity/);
|
||||
});
|
||||
|
||||
test("SQLite claim-race retry removes only the lost candidate from the same policy-valid set", () => {
|
||||
|
||||
@@ -56,3 +56,52 @@ test("fetchRemoteImage blocks redirects to private image hosts", async () => {
|
||||
/Blocked private or local provider URL/
|
||||
);
|
||||
});
|
||||
|
||||
// The default guard mode (no `guard` option passed, matching production callers that rely on
|
||||
// `getProviderOutboundGuard()`'s local-first default) is "block-metadata". Every other test in
|
||||
// this file passes `guard: "public-only"` explicitly, which never exercised this branch — the
|
||||
// gap that let `validateRemoteImageUrl()`'s fall-through to the unchecked `parseOutboundUrl()`
|
||||
// for cloud-metadata hosts go undetected.
|
||||
test("fetchRemoteImage blocks cloud-metadata hosts under the default block-metadata guard", async () => {
|
||||
let called = false;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
fetchRemoteImage("http://169.254.169.254/latest/meta-data", {
|
||||
fetchImpl: async () => {
|
||||
called = true;
|
||||
return new Response("unexpected");
|
||||
},
|
||||
}),
|
||||
/Blocked cloud-metadata endpoint/
|
||||
);
|
||||
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
test("fetchRemoteImage allows private/LAN image hosts under the default block-metadata guard", async () => {
|
||||
const result = await fetchRemoteImage("http://192.168.1.50:8080/local.png", {
|
||||
fetchImpl: async () =>
|
||||
new Response(new Uint8Array([1, 2, 3]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.buffer.toString("base64"), "AQID");
|
||||
});
|
||||
|
||||
test("fetchRemoteImage blocks redirects to cloud-metadata hosts under the default block-metadata guard", async () => {
|
||||
await assert.rejects(
|
||||
() =>
|
||||
fetchRemoteImage("https://cdn.example.com/redirect.png", {
|
||||
fetchImpl: async () =>
|
||||
new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: "http://169.254.169.254/latest/meta-data" },
|
||||
}),
|
||||
lookup: publicLookup,
|
||||
}),
|
||||
/Blocked cloud-metadata endpoint/
|
||||
);
|
||||
});
|
||||
|
||||
8
tsconfig.typecheck-api.json
Normal file
8
tsconfig.typecheck-api.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"incremental": false
|
||||
},
|
||||
"include": ["src/app/api/**/*.ts", "src/app/api/**/*.tsx"]
|
||||
}
|
||||
Reference in New Issue
Block a user