mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merged into release/v3.8.45. Delta do par #6246 (Test-All read-only + bulk enable/disable), reconciliado com o núcleo #6296 já mergeado. CHANGELOG restaurado (ambos bullets do #6246 coexistem). Reds pré-existentes: dast-smoke (infra) + file-size drift.
This commit is contained in:
committed by
GitHub
parent
f680aacff0
commit
b6ffe8ce20
@@ -1521,6 +1521,11 @@ APP_LOG_TO_FILE=true
|
||||
# PROXY_AUTO_REMOVE=false
|
||||
# Consecutive failures before an auto-remove fires. Default: 3.
|
||||
# PROXY_AUTO_REMOVE_AFTER=3
|
||||
# Let automated reachability probes (the scheduler + the "Test All" button) WRITE
|
||||
# a proxy's status. Default "false": probes are read-only and never deactivate a
|
||||
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
|
||||
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
|
||||
# PROXY_HEALTH_AUTO_DEACTIVATE=false
|
||||
|
||||
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
|
||||
# directly when proxy reachability pre-checks fail. Default: false.
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
- **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Two coupled defects from the new health scheduler: (1) **IP leak** — when a proxy assigned to a connection was marked `inactive`, resolution fell through to a **direct** egress instead of blocking, exposing the operator's real IP; (2) **over-deactivation** — the sweep flipped a proxy to `inactive` on the **first** failed probe and counted our own 5s timeout / a probe-target `5xx` as the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-free `decideProxyHealthAction` (`src/lib/proxyHealth/decision.ts`) — by default the health check now **only counts/logs and never downgrades status** (a proxy is downgraded/removed only with `PROXY_AUTO_REMOVE=true`, after `PROXY_AUTO_REMOVE_AFTER` **consecutive** conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a `5xx` from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, `safeResolveProxy` now **fails closed** via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (`hasBlockingProxyAssignment`), honoring the explicit `proxy off` toggles and the `PROXY_FAIL_OPEN=true` opt-out. Existing proxies stuck `inactive` by the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards: `tests/unit/proxy-health-decide-action-6246.test.ts`, `tests/unit/proxy-assigned-unavailable-6246.test.ts`.
|
||||
|
||||
- **fix(proxy):** make "Test All" read-only and add bulk enable/disable ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/6246)). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The **"Test All" button** (`POST /api/settings/proxies/auto-test`) used to flip a proxy to `inactive` on a failed reachability probe; since the egress selector excludes `inactive` proxies, a flaky probe (an unreachable `httpbin.org`, a proxy that blocks `HEAD`, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now **read-only by default** (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set with `PROXY_HEALTH_AUTO_DEACTIVATE=true`). (2) Adds a **bulk enable/disable** proxies endpoint + toolbar action (`POST /api/settings/proxies/batch-activate`) so an operator can re-activate proxies in one click. Regression guard: `tests/unit/proxy-health-6246.test.ts`. (thanks @tenshiak)
|
||||
|
||||
- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`.
|
||||
|
||||
- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub)
|
||||
|
||||
@@ -836,6 +836,7 @@ Anthropic-compatible provider instead.
|
||||
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
|
||||
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
|
||||
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
|
||||
| `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. |
|
||||
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
|
||||
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |
|
||||
| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. |
|
||||
|
||||
@@ -7,7 +7,9 @@ interface ProxyBatchActionsProps {
|
||||
selectedCount: number;
|
||||
batchDeleting: boolean;
|
||||
autoTesting: boolean;
|
||||
batchActivating: boolean;
|
||||
onBatchDelete: () => void;
|
||||
onBatchActivate: () => void;
|
||||
onAutoTestAll: () => void;
|
||||
}
|
||||
|
||||
@@ -15,7 +17,9 @@ export function ProxyBatchActions({
|
||||
selectedCount,
|
||||
batchDeleting,
|
||||
autoTesting,
|
||||
batchActivating,
|
||||
onBatchDelete,
|
||||
onBatchActivate,
|
||||
onAutoTestAll,
|
||||
}: ProxyBatchActionsProps) {
|
||||
const t = useTranslations("proxyRegistry");
|
||||
@@ -27,6 +31,16 @@ export function ProxyBatchActions({
|
||||
<span className="text-xs text-text-muted">
|
||||
{t("batchSelectedCount", { count: selectedCount })}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="check_circle"
|
||||
onClick={onBatchActivate}
|
||||
loading={batchActivating}
|
||||
data-testid="proxy-registry-batch-activate"
|
||||
>
|
||||
{t("batchActivateSelected", { count: selectedCount })}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
@@ -267,9 +267,11 @@ export default function ProxyRegistryManager() {
|
||||
setSelectedIds,
|
||||
batchDeleting,
|
||||
autoTesting,
|
||||
batchActivating,
|
||||
toggleSelectAll: hookToggleSelectAll,
|
||||
toggleSelect,
|
||||
handleBatchDelete: hookHandleBatchDelete,
|
||||
handleBatchActivate: hookHandleBatchActivate,
|
||||
handleAutoTestAll: hookHandleAutoTestAll,
|
||||
} = useProxyBatchOperations(load);
|
||||
|
||||
@@ -279,6 +281,10 @@ export default function ProxyRegistryManager() {
|
||||
hookHandleBatchDelete(setError);
|
||||
}, [hookHandleBatchDelete, setError]);
|
||||
|
||||
const handleBatchActivate = useCallback(() => {
|
||||
hookHandleBatchActivate(setError, "active");
|
||||
}, [hookHandleBatchActivate, setError]);
|
||||
|
||||
const handleAutoTestAll = useCallback(() => {
|
||||
hookHandleAutoTestAll(setError, setTestById);
|
||||
}, [hookHandleAutoTestAll, setError, setTestById]);
|
||||
@@ -636,7 +642,9 @@ export default function ProxyRegistryManager() {
|
||||
selectedCount={selectedIds.size}
|
||||
batchDeleting={batchDeleting}
|
||||
autoTesting={autoTesting}
|
||||
batchActivating={batchActivating}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onBatchActivate={handleBatchActivate}
|
||||
onAutoTestAll={handleAutoTestAll}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useProxyBatchOperations(load: () => Promise<void>) {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||
const [autoTesting, setAutoTesting] = useState(false);
|
||||
const [batchActivating, setBatchActivating] = useState(false);
|
||||
|
||||
const toggleSelectAll = useCallback(
|
||||
(allSelected: boolean, items: Array<{ id: string }>) => {
|
||||
@@ -63,6 +64,34 @@ export function useProxyBatchOperations(load: () => Promise<void>) {
|
||||
[selectedIds, load]
|
||||
);
|
||||
|
||||
// #6246: bulk enable/disable — the only automated path that writes proxy
|
||||
// status (an explicit operator action). Health probes are read-only by default.
|
||||
const handleBatchActivate = useCallback(
|
||||
async (setError: (msg: string | null) => void, status: "active" | "inactive" = "active") => {
|
||||
if (selectedIds.size === 0) return;
|
||||
setBatchActivating(true);
|
||||
try {
|
||||
const res = await fetch("/api/settings/proxies/batch-activate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ids: Array.from(selectedIds), status }),
|
||||
});
|
||||
const data: { error?: { message?: string } } = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
setSelectedIds(new Set());
|
||||
await load();
|
||||
} else {
|
||||
setError(data?.error?.message || "Batch update failed");
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : "Batch update failed");
|
||||
} finally {
|
||||
setBatchActivating(false);
|
||||
}
|
||||
},
|
||||
[selectedIds, load]
|
||||
);
|
||||
|
||||
const handleAutoTestAll = useCallback(
|
||||
async (
|
||||
setError: (msg: string | null) => void,
|
||||
@@ -106,9 +135,11 @@ export function useProxyBatchOperations(load: () => Promise<void>) {
|
||||
setSelectedIds,
|
||||
batchDeleting,
|
||||
autoTesting,
|
||||
batchActivating,
|
||||
toggleSelectAll,
|
||||
toggleSelect,
|
||||
handleBatchDelete,
|
||||
handleBatchActivate,
|
||||
handleAutoTestAll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { createProxyDispatcher } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
import { fetch as undiciFetch } from "undici";
|
||||
import { resolveHealthCheckStatusWrite } from "@/lib/proxyHealth/statusPolicy";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
|
||||
@@ -43,11 +44,16 @@ async function testSingleProxy(proxy: { id: string; type: string; host: string;
|
||||
});
|
||||
const latencyMs = Date.now() - start;
|
||||
const alive = resp.status < 500;
|
||||
await updateProxy(proxy.id, { status: alive ? "active" : "inactive" }).catch(() => {});
|
||||
// #6246: "Test All" is a test, not test-and-set. By default an automated probe
|
||||
// never mutates a proxy's status (only the operator does). Opt back into the
|
||||
// legacy write with PROXY_HEALTH_AUTO_DEACTIVATE=true.
|
||||
const statusWrite = resolveHealthCheckStatusWrite(alive);
|
||||
if (statusWrite) await updateProxy(proxy.id, { status: statusWrite }).catch(() => {});
|
||||
return { proxyId: proxy.id, host: proxy.host, port: proxy.port, alive, latencyMs };
|
||||
} catch (err) {
|
||||
const latencyMs = Date.now() - start;
|
||||
await updateProxy(proxy.id, { status: "inactive" }).catch(() => {});
|
||||
const statusWrite = resolveHealthCheckStatusWrite(false);
|
||||
if (statusWrite) await updateProxy(proxy.id, { status: statusWrite }).catch(() => {});
|
||||
return {
|
||||
proxyId: proxy.id,
|
||||
host: proxy.host,
|
||||
|
||||
72
src/app/api/settings/proxies/batch-activate/route.ts
Normal file
72
src/app/api/settings/proxies/batch-activate/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { z } from "zod";
|
||||
import { updateProxy } from "@/lib/localDb";
|
||||
import { createErrorResponse, createErrorResponseFromUnknown } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { clearDispatcherCache } from "@omniroute/open-sse/utils/proxyDispatcher";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
const batchActivateSchema = z.object({
|
||||
ids: z.array(z.string()).min(1).max(500),
|
||||
// "active" (bulk enable) or "inactive" (bulk disable). Defaults to enable —
|
||||
// the #6246 ask was a way to re-enable proxies an operator had disabled.
|
||||
status: z.enum(["active", "inactive"]).optional().default("active"),
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/settings/proxies/batch-activate
|
||||
*
|
||||
* Bulk-set the status of multiple proxies in one request (#6246). This is the
|
||||
* ONLY automated path allowed to change proxy status — it is an explicit
|
||||
* operator action, unlike the reachability probes which are read-only by
|
||||
* default (see src/lib/proxyHealth/statusPolicy.ts).
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body", type: "invalid_request" });
|
||||
}
|
||||
|
||||
const validation = validateBody(batchActivateSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return createErrorResponse({ status: 400, message: validation.error.message, type: "invalid_request" });
|
||||
}
|
||||
|
||||
const { ids, status } = validation.data;
|
||||
|
||||
try {
|
||||
const results: Array<{ id: string; success: boolean; error?: string }> = [];
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const id of ids) {
|
||||
try {
|
||||
if (await updateProxy(id, { status })) {
|
||||
results.push({ id, success: true });
|
||||
updatedCount++;
|
||||
} else {
|
||||
results.push({ id, success: false, error: "Proxy not found" });
|
||||
}
|
||||
} catch (err) {
|
||||
results.push({ id, success: false, error: err instanceof Error ? err.message : "Unknown error" });
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
try { clearDispatcherCache(); } catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: updatedCount > 0,
|
||||
status,
|
||||
updated: updatedCount,
|
||||
failed: ids.length - updatedCount,
|
||||
results,
|
||||
});
|
||||
} catch (error) {
|
||||
return createErrorResponseFromUnknown(error, "Failed to batch update proxy status");
|
||||
}
|
||||
}
|
||||
@@ -7940,6 +7940,7 @@
|
||||
"errorTestFailed": "Failed to test proxies",
|
||||
"batchSelectedCount": "{count} selected",
|
||||
"batchDeleteSelected": "Delete {count} selected",
|
||||
"batchActivateSelected": "Enable {count} selected",
|
||||
"testPassed": "✓ OK"
|
||||
},
|
||||
"playground": {
|
||||
|
||||
41
src/lib/proxyHealth/statusPolicy.ts
Normal file
41
src/lib/proxyHealth/statusPolicy.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Proxy health-check status-write policy (#6246).
|
||||
*
|
||||
* The manual "Test All" reachability probe (`/api/settings/proxies/auto-test`)
|
||||
* used to flip a proxy's `status` to `inactive` on a single failed probe. Because
|
||||
* the egress selector excludes `inactive` proxies (PROXY_ALIVE_PREDICATE), a flaky
|
||||
* external probe (an unreachable httpbin.org, a proxy that blocks HEAD, or a slow
|
||||
* paid proxy) silently disabled every proxy that failed — "Test All" is a test,
|
||||
* not test-and-set.
|
||||
*
|
||||
* Policy: automated reachability probes are **read-only by default** — they never
|
||||
* write a proxy's status. Only the operator sets a proxy active/inactive (same
|
||||
* contract as provider accounts, which are auto-disabled only when the operator
|
||||
* opts in). Set `PROXY_HEALTH_AUTO_DEACTIVATE=true` to restore the legacy
|
||||
* test-and-set behavior (probe result writes `active`/`inactive`).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve the status an automated health probe should WRITE for a proxy, or
|
||||
* `null` to leave the status untouched (the default). Pure + unit-testable.
|
||||
*
|
||||
* @param alive whether the reachability probe succeeded
|
||||
* @param env environment source (defaults to process.env)
|
||||
*/
|
||||
export function resolveHealthCheckStatusWrite(
|
||||
alive: boolean,
|
||||
env: { PROXY_HEALTH_AUTO_DEACTIVATE?: string } = process.env
|
||||
): "active" | "inactive" | null {
|
||||
if ((env.PROXY_HEALTH_AUTO_DEACTIVATE ?? "").trim().toLowerCase() !== "true") {
|
||||
// Default: never let an automated probe change a proxy's status.
|
||||
return null;
|
||||
}
|
||||
return alive ? "active" : "inactive";
|
||||
}
|
||||
|
||||
/** True when automated probes are allowed to write proxy status (opt-in). */
|
||||
export function isProxyHealthAutoDeactivateEnabled(
|
||||
env: { PROXY_HEALTH_AUTO_DEACTIVATE?: string } = process.env
|
||||
): boolean {
|
||||
return (env.PROXY_HEALTH_AUTO_DEACTIVATE ?? "").trim().toLowerCase() === "true";
|
||||
}
|
||||
161
tests/unit/proxy-health-6246.test.ts
Normal file
161
tests/unit/proxy-health-6246.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* #6246 — "Massive issue with proxies since 3.8.44 - IP leak" (delta scope).
|
||||
*
|
||||
* The core fail-closed + scheduler regression is fixed in PR #6296. This suite
|
||||
* covers the two remaining reporter asks that #6296 does NOT touch:
|
||||
* (#3) the "Test All" button (`/api/settings/proxies/auto-test`) must be a test,
|
||||
* not test-and-set — it flipped a proxy to `inactive` on a failed probe,
|
||||
* and the egress selector excludes `inactive` proxies, so "Test All"
|
||||
* silently disabled every proxy that failed a flaky probe;
|
||||
* (#4) a bulk enable/disable endpoint so an operator can re-activate proxies
|
||||
* in one action.
|
||||
*
|
||||
* DB-backed and network-free: the probe targets a dead localhost proxy
|
||||
* (immediate ECONNREFUSED — no outbound traffic).
|
||||
*/
|
||||
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-proxy-6246-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "test-secret";
|
||||
delete process.env.INITIAL_PASSWORD; // auth not required in this test env
|
||||
delete process.env.PROXY_HEALTH_AUTO_DEACTIVATE;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const proxiesDb = await import("../../src/lib/db/proxies.ts");
|
||||
const { resolveHealthCheckStatusWrite, isProxyHealthAutoDeactivateEnabled } = await import(
|
||||
"../../src/lib/proxyHealth/statusPolicy.ts"
|
||||
);
|
||||
const { POST: autoTestPost } = await import(
|
||||
"../../src/app/api/settings/proxies/auto-test/route.ts"
|
||||
);
|
||||
const { POST: batchActivatePost } = await import(
|
||||
"../../src/app/api/settings/proxies/batch-activate/route.ts"
|
||||
);
|
||||
|
||||
function resetStorage() {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
delete process.env.PROXY_HEALTH_AUTO_DEACTIVATE;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── status-write policy ───────────────────────────────────────────────────────
|
||||
test("policy: automated probes never write status by default", () => {
|
||||
assert.equal(resolveHealthCheckStatusWrite(false, {}), null);
|
||||
assert.equal(resolveHealthCheckStatusWrite(true, {}), null);
|
||||
assert.equal(isProxyHealthAutoDeactivateEnabled({}), false);
|
||||
});
|
||||
|
||||
test("policy: PROXY_HEALTH_AUTO_DEACTIVATE=true restores legacy test-and-set", () => {
|
||||
const env = { PROXY_HEALTH_AUTO_DEACTIVATE: "true" };
|
||||
assert.equal(resolveHealthCheckStatusWrite(false, env), "inactive");
|
||||
assert.equal(resolveHealthCheckStatusWrite(true, env), "active");
|
||||
assert.equal(isProxyHealthAutoDeactivateEnabled(env), true);
|
||||
});
|
||||
|
||||
// ── (#3) "Test All" must not deactivate proxies by default ─────────────────────
|
||||
test("auto-test does NOT deactivate a failing proxy by default", async () => {
|
||||
resetStorage();
|
||||
// Dead proxy: nothing listens on this port → the probe fails immediately.
|
||||
const p = await proxiesDb.createProxy({
|
||||
name: "dead",
|
||||
type: "http",
|
||||
host: "127.0.0.1",
|
||||
port: 1,
|
||||
});
|
||||
assert.ok(p?.id);
|
||||
assert.equal(p.status, "active");
|
||||
|
||||
const req = new Request("http://localhost/api/settings/proxies/auto-test", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ids: [p.id] }),
|
||||
});
|
||||
const res = await autoTestPost(req);
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
// The probe reports it dead...
|
||||
assert.equal(body.results?.[0]?.alive, false);
|
||||
// ...but the stored status is untouched (the regression flipped it to inactive).
|
||||
const after = await proxiesDb.getProxyById(p.id, { includeSecrets: false });
|
||||
assert.equal(after?.status, "active", "auto-test must not mutate proxy status by default");
|
||||
});
|
||||
|
||||
test("auto-test still deactivates when PROXY_HEALTH_AUTO_DEACTIVATE=true (opt-in)", async () => {
|
||||
resetStorage();
|
||||
process.env.PROXY_HEALTH_AUTO_DEACTIVATE = "true";
|
||||
const p = await proxiesDb.createProxy({
|
||||
name: "dead",
|
||||
type: "http",
|
||||
host: "127.0.0.1",
|
||||
port: 1,
|
||||
});
|
||||
assert.ok(p?.id);
|
||||
const req = new Request("http://localhost/api/settings/proxies/auto-test", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ids: [p.id] }),
|
||||
});
|
||||
const res = await autoTestPost(req);
|
||||
assert.equal(res.status, 200);
|
||||
const after = await proxiesDb.getProxyById(p.id, { includeSecrets: false });
|
||||
assert.equal(after?.status, "inactive");
|
||||
delete process.env.PROXY_HEALTH_AUTO_DEACTIVATE;
|
||||
});
|
||||
|
||||
// ── (#4) bulk enable/disable proxies ──────────────────────────────────────────
|
||||
test("batch-activate bulk-enables multiple proxies (default status=active)", async () => {
|
||||
resetStorage();
|
||||
const a = await proxiesDb.createProxy({ name: "a", type: "http", host: "127.0.0.1", port: 8080 });
|
||||
const b = await proxiesDb.createProxy({ name: "b", type: "http", host: "127.0.0.1", port: 8081 });
|
||||
await proxiesDb.updateProxy(a!.id, { status: "inactive" });
|
||||
await proxiesDb.updateProxy(b!.id, { status: "inactive" });
|
||||
|
||||
const req = new Request("http://localhost/api/settings/proxies/batch-activate", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ids: [a!.id, b!.id] }),
|
||||
});
|
||||
const res = await batchActivatePost(req);
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.updated, 2);
|
||||
assert.equal(body.status, "active");
|
||||
assert.equal((await proxiesDb.getProxyById(a!.id, { includeSecrets: false }))?.status, "active");
|
||||
assert.equal((await proxiesDb.getProxyById(b!.id, { includeSecrets: false }))?.status, "active");
|
||||
});
|
||||
|
||||
test("batch-activate can bulk-disable with status=inactive", async () => {
|
||||
resetStorage();
|
||||
const a = await proxiesDb.createProxy({ name: "a", type: "http", host: "127.0.0.1", port: 8080 });
|
||||
const req = new Request("http://localhost/api/settings/proxies/batch-activate", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ids: [a!.id], status: "inactive" }),
|
||||
});
|
||||
const res = await batchActivatePost(req);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal((await proxiesDb.getProxyById(a!.id, { includeSecrets: false }))?.status, "inactive");
|
||||
});
|
||||
|
||||
test("batch-activate rejects an empty ids array with 400", async () => {
|
||||
resetStorage();
|
||||
const req = new Request("http://localhost/api/settings/proxies/batch-activate", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ids: [] }),
|
||||
});
|
||||
const res = await batchActivatePost(req);
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
Reference in New Issue
Block a user