mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
fix: improve proxy validation output
This commit is contained in:
@@ -43,6 +43,8 @@ describe("proxy cli runtime", () => {
|
||||
"OPENCLAW_DEBUG_PROXY_CERT_DIR",
|
||||
"OPENCLAW_DEBUG_PROXY_SESSION_ID",
|
||||
"OPENCLAW_DEBUG_PROXY_ENABLED",
|
||||
"FORCE_COLOR",
|
||||
"NO_COLOR",
|
||||
] as const;
|
||||
const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
||||
let tempDir = "";
|
||||
@@ -54,6 +56,8 @@ describe("proxy cli runtime", () => {
|
||||
process.env.OPENCLAW_DEBUG_PROXY_CERT_DIR = path.join(tempDir, "certs");
|
||||
delete process.env.OPENCLAW_DEBUG_PROXY_ENABLED;
|
||||
delete process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID;
|
||||
delete process.env.FORCE_COLOR;
|
||||
process.env.NO_COLOR = "1";
|
||||
getRuntimeConfigMock.mockReset();
|
||||
getRuntimeConfigMock.mockReturnValue({
|
||||
proxy: {
|
||||
@@ -311,6 +315,66 @@ describe("proxy cli runtime", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prints check errors on the same line", async () => {
|
||||
runProxyValidationMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
config: {
|
||||
enabled: true,
|
||||
proxyUrl: "http://proxy.example:3128",
|
||||
source: "config",
|
||||
errors: [],
|
||||
},
|
||||
checks: [
|
||||
{
|
||||
kind: "denied",
|
||||
url: "http://127.0.0.1:12345/",
|
||||
ok: true,
|
||||
error: "fetch failed",
|
||||
},
|
||||
],
|
||||
});
|
||||
const { runProxyValidateCommand } = await import("./proxy-cli.runtime.js");
|
||||
|
||||
await runProxyValidateCommand({});
|
||||
|
||||
expect(process.stdout.write).toHaveBeenCalledWith(
|
||||
"Proxy validation passed\n\n" +
|
||||
"Proxy\n" +
|
||||
" Source: config\n" +
|
||||
" URL: http://proxy.example:3128/\n\n" +
|
||||
"Checks\n" +
|
||||
" ✓ denied http://127.0.0.1:12345/ — fetch failed\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies the terminal color theme when rich output is enabled", async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("../terminal/theme.js", () => ({
|
||||
colorize: (rich: boolean, color: (value: string) => string, value: string) =>
|
||||
rich ? color(value) : value,
|
||||
isRich: () => true,
|
||||
theme: {
|
||||
heading: (value: string) => `<heading>${value}</heading>`,
|
||||
success: (value: string) => `<success>${value}</success>`,
|
||||
error: (value: string) => `<error>${value}</error>`,
|
||||
muted: (value: string) => `<muted>${value}</muted>`,
|
||||
warn: (value: string) => `<warn>${value}</warn>`,
|
||||
},
|
||||
}));
|
||||
try {
|
||||
const { runProxyValidateCommand } = await import("./proxy-cli.runtime.js");
|
||||
|
||||
await runProxyValidateCommand({});
|
||||
|
||||
const output = String(vi.mocked(process.stdout.write).mock.calls[0]?.[0] ?? "");
|
||||
expect(output).toContain("<success>Proxy validation passed</success>");
|
||||
expect(output).toContain("<heading>Checks</heading>");
|
||||
expect(output).toContain("<success>✓</success>");
|
||||
} finally {
|
||||
vi.doUnmock("../terminal/theme.js");
|
||||
}
|
||||
});
|
||||
|
||||
it("prints actionable check failure output", async () => {
|
||||
runProxyValidationMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
@@ -347,8 +411,7 @@ describe("proxy cli runtime", () => {
|
||||
" URL: http://proxy.example:3128/\n\n" +
|
||||
"Checks\n" +
|
||||
" ✓ allowed http://target.example/allowed HTTP 200\n" +
|
||||
" ✗ denied http://target.example/allowed HTTP 200\n" +
|
||||
" Denied destination was reachable through the proxy\n\n" +
|
||||
" ✗ denied http://target.example/allowed HTTP 200 — Denied destination was reachable through the proxy\n\n" +
|
||||
"Next steps\n" +
|
||||
" Update the proxy ACL so denied destinations are blocked, or pass the expected --denied-url values.\n",
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getDebugProxyCaptureStore,
|
||||
} from "../proxy-capture/store.sqlite.js";
|
||||
import type { CaptureQueryPreset } from "../proxy-capture/types.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
|
||||
export async function runDebugProxyStartCommand(opts: { host?: string; port?: number }) {
|
||||
const settings = resolveDebugProxySettings();
|
||||
@@ -148,11 +149,41 @@ function redactProxyValidationResult(result: ProxyValidationResult): ProxyValida
|
||||
};
|
||||
}
|
||||
|
||||
function formatProxyCheckLine(check: ProxyValidationResult["checks"][number]): string {
|
||||
const icon = check.ok ? "✓" : "✗";
|
||||
const paddedKind = check.kind.padEnd(7, " ");
|
||||
const status = check.status === undefined ? "" : ` HTTP ${check.status}`;
|
||||
return ` ${icon} ${paddedKind} ${check.url}${status}`;
|
||||
type ProxyValidationTextColors = {
|
||||
heading: (value: string) => string;
|
||||
success: (value: string) => string;
|
||||
error: (value: string) => string;
|
||||
muted: (value: string) => string;
|
||||
warn: (value: string) => string;
|
||||
};
|
||||
|
||||
function getProxyValidationTextColors(): ProxyValidationTextColors {
|
||||
const rich = isRich();
|
||||
const apply = (color: (value: string) => string) => (value: string) =>
|
||||
colorize(rich, color, value);
|
||||
return {
|
||||
heading: apply(theme.heading),
|
||||
success: apply(theme.success),
|
||||
error: apply(theme.error),
|
||||
muted: apply(theme.muted),
|
||||
warn: apply(theme.warn),
|
||||
};
|
||||
}
|
||||
|
||||
function formatProxyCheckLine(
|
||||
check: ProxyValidationResult["checks"][number],
|
||||
colors: ProxyValidationTextColors,
|
||||
): string {
|
||||
const icon = check.ok ? colors.success("✓") : colors.error("✗");
|
||||
const paddedKind = colors.muted(check.kind.padEnd(7, " "));
|
||||
const status =
|
||||
check.status === undefined
|
||||
? ""
|
||||
: ` ${check.ok ? colors.success(`HTTP ${check.status}`) : colors.error(`HTTP ${check.status}`)}`;
|
||||
const detail = check.error
|
||||
? ` — ${check.ok ? colors.muted(check.error) : colors.error(check.error)}`
|
||||
: "";
|
||||
return ` ${icon} ${paddedKind} ${check.url}${status}${detail}`;
|
||||
}
|
||||
|
||||
function formatProxyValidationNextSteps(result: ProxyValidationResult): string[] {
|
||||
@@ -185,37 +216,35 @@ function formatProxyValidationNextSteps(result: ProxyValidationResult): string[]
|
||||
}
|
||||
|
||||
function formatProxyValidationText(result: ProxyValidationResult): string {
|
||||
const colors = getProxyValidationTextColors();
|
||||
const redactedProxyUrl = redactProxyUrl(result.config.proxyUrl);
|
||||
const lines = [
|
||||
`Proxy validation ${result.ok ? "passed" : "failed"}`,
|
||||
result.ok ? colors.success("Proxy validation passed") : colors.error("Proxy validation failed"),
|
||||
"",
|
||||
"Proxy",
|
||||
` Source: ${result.config.source}`,
|
||||
` URL: ${redactedProxyUrl ?? "not configured"}`,
|
||||
colors.heading("Proxy"),
|
||||
` Source: ${colors.muted(result.config.source)}`,
|
||||
` URL: ${redactedProxyUrl ?? colors.muted("not configured")}`,
|
||||
];
|
||||
|
||||
if (result.config.errors.length > 0) {
|
||||
lines.push("", "Problems");
|
||||
lines.push("", colors.heading("Problems"));
|
||||
for (const error of result.config.errors) {
|
||||
lines.push(` - ${error}`);
|
||||
lines.push(` - ${colors.error(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.checks.length > 0) {
|
||||
lines.push("", "Checks");
|
||||
lines.push("", colors.heading("Checks"));
|
||||
for (const check of result.checks) {
|
||||
lines.push(formatProxyCheckLine(check));
|
||||
if (check.error) {
|
||||
lines.push(` ${check.error}`);
|
||||
}
|
||||
lines.push(formatProxyCheckLine(check, colors));
|
||||
}
|
||||
}
|
||||
|
||||
const nextSteps = formatProxyValidationNextSteps(result);
|
||||
if (nextSteps.length > 0) {
|
||||
lines.push("", "Next steps");
|
||||
lines.push("", colors.heading("Next steps"));
|
||||
for (const nextStep of nextSteps) {
|
||||
lines.push(` ${nextStep}`);
|
||||
lines.push(` ${colors.warn(nextStep)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user