Files
modelstudioai__cli/packages/runtime/tests/proxy.e2e.test.ts
clh02467605 7b77bf6a6d fix(e2e): run subprocess harness and proxy probe via tsx
Node's strip-only TypeScript mode cannot execute workspace source that
uses parameter properties. Use the monorepo tsx binary for runNodeMain and
the proxy e2e probe (.mts for top-level await).
2026-07-10 13:01:10 +08:00

116 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { execFile } from "child_process";
import { createServer, type Server } from "http";
import { mkdtempSync, rmSync, writeFileSync } from "fs";
import type { AddressInfo } from "net";
import { tmpdir } from "os";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { promisify } from "util";
import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
import { resolveTsxBin } from "e2e/runner";
const execFileAsync = promisify(execFile);
const runtimePackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
/**
* 代理支持 E2E验证 `setupProxyFromEnv()` 是否把代理 dispatcher 正确装到全局 fetch 上。
* 不经过 CLI 命令CI 无需 api key。
*/
const FAKE_HOST = "bl-proxy-e2e.invalid";
const FAKE_URL = `https://${FAKE_HOST}/probe`;
const PROBE_SCRIPT = `
import { setupProxyFromEnv } from ${JSON.stringify(join(runtimePackageRoot, "src", "proxy.ts"))};
setupProxyFromEnv();
try {
await fetch(${JSON.stringify(FAKE_URL)}, { signal: AbortSignal.timeout(5000) });
} catch {
// 目标不可达/隧道被拒都正常
}
`;
let proxy: Server;
let proxyUrl: string;
let scriptDir: string;
let scriptPath: string;
const connectTargets: string[] = [];
beforeAll(async () => {
proxy = createServer();
proxy.on("connect", (req, clientSocket) => {
connectTargets.push(req.url ?? "");
clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
});
await new Promise<void>((resolve) => proxy.listen(0, "127.0.0.1", resolve));
proxyUrl = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`;
scriptDir = mkdtempSync(join(tmpdir(), "bl-proxy-e2e-"));
scriptPath = join(scriptDir, "probe.mts");
writeFileSync(scriptPath, PROBE_SCRIPT);
});
afterAll(async () => {
await new Promise<void>((resolve) => proxy.close(() => resolve()));
rmSync(scriptDir, { recursive: true, force: true });
});
const PROXY_ENV_CLEARED = {
HTTPS_PROXY: "",
https_proxy: "",
HTTP_PROXY: "",
http_proxy: "",
NO_PROXY: "",
no_proxy: "",
};
async function runProbe(
envOverrides: NodeJS.ProcessEnv,
): Promise<{ exitCode: number; stderr: string }> {
try {
await execFileAsync(resolveTsxBin(), [scriptPath], {
cwd: runtimePackageRoot,
encoding: "utf8",
env: { ...process.env, NODE_NO_WARNINGS: "1", ...PROXY_ENV_CLEARED, ...envOverrides },
});
return { exitCode: 0, stderr: "" };
} catch (err: unknown) {
const e = err as { stderr?: string; code?: number };
return { exitCode: typeof e.code === "number" ? e.code : 1, stderr: e.stderr ?? "" };
}
}
describe("e2e: proxy", () => {
test("设置 HTTPS_PROXY 后 fetch 经过代理CONNECT 到目标主机)", async () => {
connectTargets.length = 0;
await runProbe({ HTTPS_PROXY: proxyUrl });
expect(connectTargets).toContain(`${FAKE_HOST}:443`);
});
test("空字符串小写变量不屏蔽大写 HTTPS_PROXYundici ?? 取值回归)", async () => {
connectTargets.length = 0;
await runProbe({ https_proxy: "", HTTPS_PROXY: proxyUrl });
expect(connectTargets).toContain(`${FAKE_HOST}:443`);
});
test("NO_PROXY 命中目标主机时不走代理", async () => {
connectTargets.length = 0;
await runProbe({ HTTPS_PROXY: proxyUrl, NO_PROXY: FAKE_HOST });
expect(connectTargets.filter((t) => t.startsWith(FAKE_HOST))).toEqual([]);
});
test("未设置代理变量时保持直连(代理收不到任何流量)", async () => {
connectTargets.length = 0;
await runProbe({});
expect(connectTargets).toEqual([]);
});
test("代理 URL 非法时给出明确报错而非堆栈", async () => {
const { exitCode, stderr } = await runProbe({ HTTPS_PROXY: "::::not-a-url" });
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/Invalid proxy configuration/);
expect(stderr).toMatch(/HTTPS_PROXY/);
});
});