mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * Hide health-check excluded models from /v1/models catalog (#10026) Mirror the request-time exclusion rule (provider_specific_data.excludedModels) in the unified catalog builder: a model is hidden when its provider has connections but none of them is eligible for it. Applied across the PROVIDER_MODELS, synced, custom, alias-backed, and managed-fallback loops so ghost models no longer appear as available. Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> * fix(models): memoize getModelsDevPricing (event loop / healthz) (#10055) * fix(models): memoize getModelsDevPricing for /v1/models catalog resolveCatalogPricing called getModelsDevPricing once per model while building GET /v1/models. Each call re-scanned models_dev_pricing and JSON.parsed every row (~10k SQL scans + multi-GB parse work), pegging the event loop so even /healthz timed out (#9685, #10052). Memoize the parsed map until saveModelsDevPricing / clearModelsDevPricing and add a unit test for invalidation. Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> * fix(db): invalidate modelsDevPricing cache on DB reset (#10055) Copilot review fixes: 1. Register invalidateModelsDevPricingCache() with DB state reset system so resetDbInstance() clears the process-local memo, preventing stale pricing data from surviving across DB reset/restore operations. 2. Add test assertion verifying DB reset bypasses the memo (Copilot #10055). The process-local memo at modelsDevSync.ts:204 caches getModelsDevPricing() results until saveModelsDevPricing()/clearModelsDevPricing() to avoid re-scanning all pricing rows on every /v1/models request. Without this hook, backup restore and test DB resets would serve stale cached data from the previous connection. Tests: npm run test:unit:serial -- tests/unit/modelsDevSync-extended.test.ts --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix(ops): Docker HEALTHCHECK probes /healthz not deep monitoring /api/monitoring/health does a SQLite ping and more. When the event loop is busy the official image HEALTHCHECK (5s timeout) marks the container Unhealthy and orchestrators restart the only replica mid-session. * fix(ops): keep healthcheck PR scoped to the /healthz probe Drop the stray catalog ghost-model exclusion that leaked into this branch from main (already covered upstream). Restore catalog.ts to the release version so the PR contains only the Docker HEALTHCHECK /healthz fix, its tests, and the changelog entry. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: ritheshcn25 <rithesh.chandran@snb.ca> Co-authored-by: ritheshcn25 <ritheshcn25@users.noreply.github.com> Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import http from "node:http";
|
|
|
|
// #3151 — Docker healthcheck always reports unhealthy because the probe only
|
|
// hit 127.0.0.1 and swallowed every error (empty Output, undiagnosable).
|
|
// The hardened script exports an injectable `probeHealth` helper that tries an
|
|
// ordered host list and surfaces the last error on total failure.
|
|
const { probeHealth } = (await import("../../scripts/dev/healthcheck.mjs")) as {
|
|
probeHealth: (opts: {
|
|
port: number | string;
|
|
hosts?: string[];
|
|
fetchImpl?: typeof fetch;
|
|
timeoutMs?: number;
|
|
}) => Promise<string>;
|
|
};
|
|
|
|
/** Start an ephemeral HTTP server bound only to the given host. */
|
|
function startServer(host: string): Promise<{ server: http.Server; port: number }> {
|
|
return new Promise((resolve, reject) => {
|
|
const server = http.createServer((req, res) => {
|
|
if (req.url === "/healthz") {
|
|
res.writeHead(200, { "content-type": "application/json" });
|
|
res.end(JSON.stringify({ status: "ok" }));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end();
|
|
}
|
|
});
|
|
server.on("error", reject);
|
|
server.listen(0, host, () => {
|
|
const addr = server.address();
|
|
if (addr && typeof addr === "object") {
|
|
resolve({ server, port: addr.port });
|
|
} else {
|
|
reject(new Error("no address"));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function closeServer(server: http.Server): Promise<void> {
|
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
}
|
|
|
|
const servers: http.Server[] = [];
|
|
|
|
test.after(async () => {
|
|
for (const s of servers) {
|
|
await closeServer(s);
|
|
}
|
|
});
|
|
|
|
test("probeHealth resolves the host when the server is on 127.0.0.1", async () => {
|
|
const { server, port } = await startServer("127.0.0.1");
|
|
servers.push(server);
|
|
|
|
const ok = await probeHealth({ port, hosts: ["127.0.0.1", "localhost", "::1"] });
|
|
assert.equal(ok, "127.0.0.1");
|
|
});
|
|
|
|
test("probeHealth falls through to a later host when 127.0.0.1 is unreachable", async () => {
|
|
// Bind the real server on 127.0.0.1, but list an unreachable host first so
|
|
// the helper must fall through. We point the first host at a port with no
|
|
// listener to simulate ECONNREFUSED, then the real host on the same port.
|
|
const { server, port } = await startServer("127.0.0.1");
|
|
servers.push(server);
|
|
|
|
// First host resolves to nothing listening (use a host alias that will fail),
|
|
// second host is the working loopback.
|
|
const ok = await probeHealth({
|
|
port,
|
|
hosts: ["192.0.2.1", "127.0.0.1"], // 192.0.2.1 = TEST-NET-1, unroutable
|
|
timeoutMs: 300,
|
|
});
|
|
assert.equal(ok, "127.0.0.1");
|
|
});
|
|
|
|
test("probeHealth throws a non-empty error string when every host fails", async () => {
|
|
// No server started: pick a port unlikely to have a listener.
|
|
await assert.rejects(
|
|
() =>
|
|
probeHealth({
|
|
port: 1, // privileged/closed port → connection refused
|
|
hosts: ["127.0.0.1", "localhost"],
|
|
timeoutMs: 300,
|
|
}),
|
|
(err: unknown) => {
|
|
assert.ok(err instanceof Error);
|
|
assert.ok(err.message.length > 0, "error message must be non-empty (not swallowed)");
|
|
return true;
|
|
}
|
|
);
|
|
});
|