





















@@ -9,9 +9,13 @@ const TOKEN = "bundled-plugin-runtime-smoke-token";
99const WATCHDOG_MS = readPositiveInt(process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_WATCHDOG_MS, 1000);
1010const READY_TIMEOUT_MS = readPositiveInt(
1111process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_READY_MS,
12-180000,
12+420000,
1313);
1414const RPC_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_RPC_MS, 60000);
15+const RPC_READY_TIMEOUT_MS = readPositiveInt(
16+process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_RPC_READY_MS,
17+90000,
18+);
15191620function readPositiveInt(raw, fallback) {
1721const parsed = Number.parseInt(String(raw || ""), 10);
@@ -296,6 +300,35 @@ async function rpcCall(method, params, options) {
296300return unwrapRpcPayload(parseJsonOutput(stdout));
297301}
298302303+async function retryRpcCall(method, params, options) {
304+const started = Date.now();
305+let lastError;
306+while (Date.now() - started < RPC_READY_TIMEOUT_MS) {
307+try {
308+return await rpcCall(method, params, options);
309+} catch (error) {
310+lastError = error;
311+if (!isRetryableGatewayCallError(error)) {
312+throw error;
313+}
314+await delay(500);
315+}
316+}
317+throw lastError ?? new Error(`gateway RPC ${method} timed out before retry`);
318+}
319+320+function isRetryableGatewayCallError(error) {
321+const text = error instanceof Error ? error.message : String(error);
322+return (
323+text.includes("gateway starting") ||
324+text.includes("gateway closed") ||
325+text.includes("handshake timeout") ||
326+text.includes("GatewayTransportError") ||
327+text.includes("ECONNREFUSED") ||
328+text.includes("fetch failed")
329+);
330+}
331+299332function parseJsonOutput(stdout) {
300333const trimmed = stdout.trim();
301334if (!trimmed) {
@@ -402,20 +435,28 @@ async function smokePlugin(pluginId, pluginDir, requiresConfig, pluginIndex) {
402435async function assertBaseGatewayProbes(options) {
403436await assertHttpOk(options.port, "/healthz");
404437await assertReadyzProbe(options);
405-await rpcCall("health", {}, options);
438+await retryRpcCall("health", {}, options);
406439}
407440408441async function runManifestProbes(plan, options) {
409442for (const channel of plan.channels) {
410-const status = await rpcCall("channels.status", { probe: false, timeoutMs: 2000 }, options);
443+const status = await retryRpcCall(
444+"channels.status",
445+{ probe: false, timeoutMs: 2000 },
446+options,
447+);
411448if (!isChannelVisible(status, channel)) {
412449console.log(
413450`Runtime channel status smoke skipped for ${options.pluginId}: ${channel} is not visible in dry channels.status`,
414451);
415452}
416453}
417454if (plan.runtimeSlashAliases.length > 0 && plan.activeInThisProbe) {
418-const commands = await rpcCall("commands.list", { scope: "both", includeArgs: true }, options);
455+const commands = await retryRpcCall(
456+"commands.list",
457+{ scope: "both", includeArgs: true },
458+options,
459+);
419460for (const alias of plan.runtimeSlashAliases) {
420461assertCommandVisible(commands, alias);
421462}
@@ -425,7 +466,7 @@ async function runManifestProbes(plan, options) {
425466);
426467}
427468if (plan.tools.length > 0 && plan.activeInThisProbe) {
428-const catalog = await rpcCall("tools.catalog", { includePlugins: true }, options);
469+const catalog = await retryRpcCall("tools.catalog", { includePlugins: true }, options);
429470for (const tool of plan.tools) {
430471assertToolVisible(catalog, tool);
431472}
@@ -435,8 +476,8 @@ async function runManifestProbes(plan, options) {
435476);
436477}
437478if (plan.speechProviders.length > 0) {
438-const providers = await rpcCall("tts.providers", {}, options);
439-const status = await rpcCall("tts.status", {}, options);
479+const providers = await retryRpcCall("tts.providers", {}, options);
480+const status = await retryRpcCall("tts.status", {}, options);
440481const provider = plan.speechProviders[0];
441482assertSpeechProviderVisible(providers, provider, "tts.providers");
442483assertSpeechProviderVisible(status, provider, "tts.status");
@@ -508,7 +549,7 @@ async function runWatchdog(options) {
508549`gateway exited after ready for ${options.pluginId}\n${tailFile(options.logPath)}`,
509550);
510551}
511-await rpcCall("health", {}, options);
552+await retryRpcCall("health", {}, options);
512553assertNoPostReadyRuntimeDepsWork(options.logPath, readyIndex);
513554assertNoRuntimeDepsLocks();
514555await assertNoPackageManagerChildren(options.child.pid);
@@ -650,7 +691,7 @@ async function smokeTtsGlobalDisable(pluginId, pluginDir, provider, pluginIndex)
650691try {
651692await waitForReady({ child, port, logPath });
652693await assertBaseGatewayProbes({ entrypoint, port, env });
653-const providers = await rpcCall("tts.providers", {}, { entrypoint, port, env });
694+const providers = await retryRpcCall("tts.providers", {}, { entrypoint, port, env });
654695assertSpeechProviderVisible(providers, selectedProvider, "tts.providers global-disable");
655696await runWatchdog({
656697 child,
@@ -713,7 +754,7 @@ async function smokeOpenAiTts(pluginIndex) {
713754try {
714755await waitForReady({ child, port, logPath });
715756await assertBaseGatewayProbes({ entrypoint, port, env });
716-const result = await rpcCall(
757+const result = await retryRpcCall(
717758"tts.convert",
718759{ text: "ok", provider: "openai" },
719760{ entrypoint, port, env },
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。