mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 12:54:07 +00:00
fix(voice-call): bound ngrok diagnostics
This commit is contained in:
17
extensions/voice-call/src/bounded-child-output.test.ts
Normal file
17
extensions/voice-call/src/bounded-child-output.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendBoundedChildOutput,
|
||||
emptyBoundedChildOutput,
|
||||
formatBoundedChildOutput,
|
||||
} from "./bounded-child-output.js";
|
||||
|
||||
describe("bounded child output", () => {
|
||||
it("keeps a bounded tail and records truncation", () => {
|
||||
const first = appendBoundedChildOutput(emptyBoundedChildOutput(), "abcdef", 5);
|
||||
expect(first).toEqual({ text: "bcdef", truncated: true });
|
||||
|
||||
const second = appendBoundedChildOutput(first, "ghij", 5);
|
||||
expect(second).toEqual({ text: "fghij", truncated: true });
|
||||
expect(formatBoundedChildOutput(second)).toBe("[output truncated]\nfghij");
|
||||
});
|
||||
});
|
||||
29
extensions/voice-call/src/bounded-child-output.ts
Normal file
29
extensions/voice-call/src/bounded-child-output.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const DEFAULT_MAX_OUTPUT_CHARS = 16_384;
|
||||
|
||||
export type BoundedChildOutput = {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
export function emptyBoundedChildOutput(): BoundedChildOutput {
|
||||
return { text: "", truncated: false };
|
||||
}
|
||||
|
||||
export function appendBoundedChildOutput(
|
||||
current: BoundedChildOutput,
|
||||
chunk: string,
|
||||
maxChars = DEFAULT_MAX_OUTPUT_CHARS,
|
||||
): BoundedChildOutput {
|
||||
const appended = current.text + chunk;
|
||||
if (appended.length <= maxChars) {
|
||||
return { text: appended, truncated: current.truncated };
|
||||
}
|
||||
return {
|
||||
text: appended.slice(-maxChars),
|
||||
truncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatBoundedChildOutput(output: BoundedChildOutput): string {
|
||||
return output.truncated ? `[output truncated]\n${output.text}` : output.text;
|
||||
}
|
||||
@@ -90,6 +90,27 @@ describe("voice-call tunnels", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("parses complete ngrok log lines before bounding the incomplete tail", async () => {
|
||||
const proc = nextProcess();
|
||||
const result = startNgrokTunnel({ port: 3334, path: "/voice/webhook" });
|
||||
|
||||
proc.stdout.emit(
|
||||
"data",
|
||||
Buffer.from(
|
||||
`${JSON.stringify({ msg: "started tunnel", url: "https://large.ngrok.io" })}\n${"x".repeat(20_000)}`,
|
||||
),
|
||||
);
|
||||
|
||||
const settled = await Promise.race([
|
||||
result.then(() => true),
|
||||
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 20)),
|
||||
]);
|
||||
expect(settled).toBe(true);
|
||||
|
||||
const tunnel = await result;
|
||||
expect(tunnel.publicUrl).toBe("https://large.ngrok.io/voice/webhook");
|
||||
});
|
||||
|
||||
it("sets ngrok auth token before starting the tunnel", async () => {
|
||||
const authProc = nextProcess();
|
||||
const tunnelProc = nextProcess();
|
||||
@@ -111,6 +132,22 @@ describe("voice-call tunnels", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds ngrok command failure output", async () => {
|
||||
const authProc = nextProcess();
|
||||
const result = startNgrokTunnel({
|
||||
port: 3334,
|
||||
path: "/hook",
|
||||
authToken: "token",
|
||||
});
|
||||
|
||||
authProc.stderr.emit("data", Buffer.from(`start-${"x".repeat(20_000)}-end`));
|
||||
authProc.close(1);
|
||||
|
||||
await expect(result).rejects.toThrow("[output truncated]");
|
||||
await expect(result).rejects.toThrow("-end");
|
||||
await expect(result).rejects.not.toThrow("start-");
|
||||
});
|
||||
|
||||
it("rejects ngrok startup errors from stderr", async () => {
|
||||
const proc = nextProcess();
|
||||
const result = startNgrokTunnel({ port: 3334, path: "/hook" });
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import {
|
||||
appendBoundedChildOutput,
|
||||
emptyBoundedChildOutput,
|
||||
formatBoundedChildOutput,
|
||||
} from "./bounded-child-output.js";
|
||||
import { getTailscaleDnsName } from "./webhook/tailscale.js";
|
||||
|
||||
const NGROK_LOG_BUFFER_MAX_CHARS = 16_384;
|
||||
|
||||
/**
|
||||
* Tunnel configuration for exposing the webhook server.
|
||||
*/
|
||||
@@ -117,9 +124,11 @@ export async function startNgrokTunnel(config: {
|
||||
};
|
||||
|
||||
proc.stdout.on("data", (data: Buffer) => {
|
||||
outputBuffer += data.toString();
|
||||
const lines = outputBuffer.split("\n");
|
||||
const lines = (outputBuffer + data.toString()).split("\n");
|
||||
outputBuffer = lines.pop() || "";
|
||||
if (outputBuffer.length > NGROK_LOG_BUFFER_MAX_CHARS) {
|
||||
outputBuffer = outputBuffer.slice(-NGROK_LOG_BUFFER_MAX_CHARS);
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
@@ -135,7 +144,8 @@ export async function startNgrokTunnel(config: {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`ngrok error: ${msg}`));
|
||||
const output = appendBoundedChildOutput(emptyBoundedChildOutput(), msg);
|
||||
reject(new Error(`ngrok error: ${formatBoundedChildOutput(output)}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -167,21 +177,22 @@ async function runNgrokCommand(args: string[]): Promise<string> {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdout = emptyBoundedChildOutput();
|
||||
let stderr = emptyBoundedChildOutput();
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
stdout = appendBoundedChildOutput(stdout, data.toString());
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
stderr = appendBoundedChildOutput(stderr, data.toString());
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
resolve(stdout.text);
|
||||
} else {
|
||||
reject(new Error(`ngrok command failed: ${stderr || stdout}`));
|
||||
const output = stderr.text ? stderr : stdout;
|
||||
reject(new Error(`ngrok command failed: ${formatBoundedChildOutput(output)}`));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user