mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
fix(api): point CLI health command at /api/monitoring/health (#6677)
bin/cli/commands/health.mjs called GET /api/health, a route that was
moved to /api/monitoring/health without updating the CLI; the top-level
/api/health handler never existed on disk (only degradation/ and ping/
sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at
/api/monitoring/health and read its real payload shape
(activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage)
instead of the old nonexistent requests/breakers/cache/memory fields.
This commit is contained in:
parent
f4fb0d310b
commit
acbb2ac63a
3 changed files with 98 additions and 20 deletions
|
|
@ -49,6 +49,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
|||
- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request").
|
||||
- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz)
|
||||
- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot)
|
||||
- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ export async function runHealthCommand(opts = {}) {
|
|||
}
|
||||
|
||||
try {
|
||||
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
|
||||
const res = await apiFetch("/api/monitoring/health", {
|
||||
retry: false,
|
||||
timeout: 5000,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
|
|
@ -66,29 +70,22 @@ export async function runHealthCommand(opts = {}) {
|
|||
if (health.uptime) console.log(t("health.uptime", { uptime: health.uptime }));
|
||||
if (health.version) console.log(` Version: ${health.version}`);
|
||||
|
||||
if (health.requests !== undefined) {
|
||||
console.log(t("health.requests", { count: health.requests }));
|
||||
if (health.activeConnections !== undefined) {
|
||||
console.log(t("health.requests", { count: health.activeConnections }));
|
||||
}
|
||||
|
||||
if (health.breakers && opts.verbose) {
|
||||
if (health.circuitBreakers && opts.verbose) {
|
||||
console.log("\n \x1b[1mCircuit Breakers\x1b[0m");
|
||||
for (const [name, status] of Object.entries(health.breakers)) {
|
||||
const state =
|
||||
status.state === "closed" ? "\x1b[32m● closed\x1b[0m" : "\x1b[33m○ open\x1b[0m";
|
||||
console.log(` ${name.padEnd(20)} ${state}`);
|
||||
}
|
||||
const { open = 0, halfOpen = 0, closed = 0 } = health.circuitBreakers;
|
||||
console.log(` \x1b[32m● closed\x1b[0m ${closed}`);
|
||||
console.log(` \x1b[33m○ half-open\x1b[0m ${halfOpen}`);
|
||||
console.log(` \x1b[31m○ open\x1b[0m ${open}`);
|
||||
}
|
||||
|
||||
if (health.cache && opts.verbose) {
|
||||
console.log("\n \x1b[1mCache\x1b[0m");
|
||||
console.log(` Semantic hits: ${health.cache.semanticHits || 0}`);
|
||||
console.log(` Signature hits: ${health.cache.signatureHits || 0}`);
|
||||
}
|
||||
|
||||
if (opts.verbose && health.memory) {
|
||||
if (opts.verbose && health.memoryUsage) {
|
||||
console.log("\n \x1b[1mMemory\x1b[0m");
|
||||
console.log(` RSS: ${health.memory.rss || "N/A"}`);
|
||||
console.log(` Heap used: ${health.memory.heapUsed || "N/A"}`);
|
||||
console.log(` RSS: ${health.memoryUsage.rss || "N/A"}`);
|
||||
console.log(` Heap used: ${health.memoryUsage.heapUsed || "N/A"}`);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -100,13 +97,17 @@ export async function runHealthCommand(opts = {}) {
|
|||
|
||||
export async function runHealthComponentsCommand(opts = {}) {
|
||||
try {
|
||||
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
|
||||
const res = await apiFetch("/api/monitoring/health", {
|
||||
retry: false,
|
||||
timeout: 5000,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`HTTP ${res.status}`);
|
||||
return 1;
|
||||
}
|
||||
const health = await res.json();
|
||||
const components = health.components || health.breakers || {};
|
||||
const components = health.components || health.circuitBreakers || {};
|
||||
for (const [name, info] of Object.entries(components)) {
|
||||
const status =
|
||||
typeof info === "object" ? info.state || info.status || "unknown" : String(info);
|
||||
|
|
|
|||
76
tests/unit/cli-health-monitoring-route.test.ts
Normal file
76
tests/unit/cli-health-monitoring-route.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import http from "node:http";
|
||||
import { runHealthCommand } from "../../bin/cli/commands/health.mjs";
|
||||
|
||||
// Regression test for GH #6677: `omniroute health` calls GET /api/health, but the
|
||||
// real server only implements GET /api/monitoring/health (plus the sub-routes
|
||||
// /api/health/degradation and /api/health/ping). This stub server mimics that
|
||||
// exact real-world shape: /api/monitoring/health responds 200 with a healthy
|
||||
// payload, everything else (including /api/health) 404s.
|
||||
//
|
||||
// Expectation once fixed: runHealthCommand() should hit /api/monitoring/health
|
||||
// and return exit code 0.
|
||||
|
||||
let server: http.Server;
|
||||
let baseUrl: string;
|
||||
|
||||
test.before(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
if (req.url === "/api/monitoring/health") {
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: "healthy",
|
||||
version: "3.8.47",
|
||||
uptime: 123,
|
||||
activeConnections: 0,
|
||||
circuitBreakers: { open: 0, halfOpen: 0, closed: 3 },
|
||||
memoryUsage: { rss: 1000, heapUsed: 500 },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Everything else, including the legacy /api/health the CLI used to call,
|
||||
// 404s — matching the real deployed route tree (only
|
||||
// app/api/health/degradation and app/api/health/ping exist on disk).
|
||||
res.writeHead(404, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "Not Found" }));
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
||||
const address = server.address();
|
||||
if (address && typeof address === "object") {
|
||||
baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
}
|
||||
process.env.OMNIROUTE_BASE_URL = baseUrl;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
delete process.env.OMNIROUTE_BASE_URL;
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
test("GH #6677: omniroute health should succeed against a server that only implements /api/monitoring/health", async () => {
|
||||
const originalError = console.error;
|
||||
const originalLog = console.log;
|
||||
const errors: string[] = [];
|
||||
console.error = (...args: unknown[]) => {
|
||||
errors.push(args.map(String).join(" "));
|
||||
};
|
||||
console.log = () => {};
|
||||
|
||||
let exitCode: number;
|
||||
try {
|
||||
exitCode = await runHealthCommand({});
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
exitCode,
|
||||
0,
|
||||
`runHealthCommand() should return 0 against a live server that implements ` +
|
||||
`/api/monitoring/health, but got exit code ${exitCode}. Captured stderr: ${errors.join(" | ")}`
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue