mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
Validated in a combined 3-PR batch worktree off release/v3.8.51 tip. - Focused test: protocol-e2e-server-stamping-11535.test.ts — part of batch's 165/165 node:test run - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity, check:docs-counts-sync — all OK - Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff Thanks for the meticulous root-causing here — three distinct issues (server flavor, HOST pin, open-bootstrap env leak) traced to exact line numbers with live-boot before/after evidence.
128 lines
4.2 KiB
JavaScript
128 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { join } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { sanitizeColorEnv } from "../build/runtime-env.mjs";
|
|
|
|
function parsePort(value, fallback) {
|
|
const parsed = Number.parseInt(String(value), 10);
|
|
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
|
|
}
|
|
|
|
const explicitBaseUrl = process.env.OMNIROUTE_BASE_URL || "";
|
|
const isolatedPort = parsePort(
|
|
process.env.DASHBOARD_PORT || process.env.PORT,
|
|
23000 + (process.pid % 1000)
|
|
);
|
|
const isolatedDataDir =
|
|
process.env.DATA_DIR || join(process.cwd(), ".tmp", "protocol-clients-data", String(process.pid));
|
|
const port = explicitBaseUrl ? null : isolatedPort;
|
|
const baseUrl = explicitBaseUrl || `http://127.0.0.1:${isolatedPort}`;
|
|
const healthUrl = `${baseUrl}/api/monitoring/health`;
|
|
const maxWaitMs = Number(process.env.ECOSYSTEM_SERVER_WAIT_MS || 180000);
|
|
const pollMs = 2000;
|
|
|
|
async function isServerReady() {
|
|
const timeout = AbortSignal.timeout(2000);
|
|
try {
|
|
const res = await fetch(healthUrl, { signal: timeout });
|
|
return res.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function waitForServerReady() {
|
|
const maxAttempts = Math.ceil(maxWaitMs / pollMs);
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
if (await isServerReady()) return;
|
|
await delay(pollMs);
|
|
}
|
|
throw new Error(`Timed out waiting for ${healthUrl} after ${maxWaitMs}ms`);
|
|
}
|
|
|
|
async function main() {
|
|
let serverProcess = null;
|
|
let startedHere = false;
|
|
const testEnv = {
|
|
...sanitizeColorEnv(process.env),
|
|
DATA_DIR: isolatedDataDir,
|
|
...(explicitBaseUrl
|
|
? {}
|
|
: {
|
|
PORT: String(port),
|
|
DASHBOARD_PORT: String(port),
|
|
API_PORT: String(port),
|
|
OMNIROUTE_BASE_URL: baseUrl,
|
|
}),
|
|
OMNIROUTE_E2E_BOOTSTRAP_MODE: process.env.OMNIROUTE_E2E_BOOTSTRAP_MODE || "open",
|
|
// Pin the custom server's bind address to loopback (#11535): under the
|
|
// programmatic next() entry the middleware's nextUrl.hostname mirrors the
|
|
// configured HOST (default "0.0.0.0"), and apiAuth.isLoopbackRequest() reads
|
|
// nextUrl.hostname FIRST — an unpinned boot makes every request look remote,
|
|
// so the anonymous open-bootstrap allow never fires (401 green-shallow).
|
|
HOST: process.env.HOST || "127.0.0.1",
|
|
};
|
|
|
|
if (!(await isServerReady())) {
|
|
// Boot the REAL custom server (run-next.mjs), not the bare `next dev` CLI.
|
|
// Only the custom Node server stamps the trusted PEER_IP_HEADER from the TCP
|
|
// socket; without that stamp the authz middleware fails closed on locality and
|
|
// every LOCAL_ONLY route (e.g. /api/mcp/audit) answers 403 even from loopback
|
|
// (#11535). run-next.mjs honors OMNIROUTE_E2E_BOOTSTRAP_MODE=open by clearing
|
|
// bootstrap credentials after its env merge, keeping the audit assertions live
|
|
// (200) instead of masking them behind a 401. The Playwright webServer runner is
|
|
// intentionally left untouched — it serves the whole blocking test-e2e suite.
|
|
serverProcess = spawn(process.execPath, ["scripts/dev/run-next.mjs", "dev"], {
|
|
stdio: "inherit",
|
|
env: testEnv,
|
|
});
|
|
startedHere = true;
|
|
await waitForServerReady();
|
|
}
|
|
|
|
const vitestProcess = spawn(
|
|
process.execPath,
|
|
[
|
|
"./node_modules/vitest/vitest.mjs",
|
|
"run",
|
|
// Without --config, Vitest loads vitest.config.ts, whose exclude list drops
|
|
// this file — the run then dies with "No test files found". The config also
|
|
// sets environment: node, so the flag is no longer needed here.
|
|
"--config",
|
|
"vitest.e2e-live.config.ts",
|
|
"tests/e2e/protocol-clients.test.ts",
|
|
],
|
|
{
|
|
stdio: "inherit",
|
|
env: testEnv,
|
|
}
|
|
);
|
|
|
|
const exitCode = await new Promise((resolve) => {
|
|
vitestProcess.on("exit", (code, signal) => {
|
|
if (signal) {
|
|
resolve(1);
|
|
return;
|
|
}
|
|
resolve(code ?? 1);
|
|
});
|
|
});
|
|
|
|
if (startedHere && serverProcess) {
|
|
serverProcess.kill("SIGTERM");
|
|
await delay(1000);
|
|
if (!serverProcess.killed) {
|
|
serverProcess.kill("SIGKILL");
|
|
}
|
|
}
|
|
|
|
process.exit(exitCode);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[test:protocols:e2e] Failed:", error?.message || error);
|
|
process.exit(1);
|
|
});
|