diff --git a/scripts/docker/install-sh-e2e/run.sh b/scripts/docker/install-sh-e2e/run.sh index 81aa51cb182..ecc8af74cc5 100755 --- a/scripts/docker/install-sh-e2e/run.sh +++ b/scripts/docker/install-sh-e2e/run.sh @@ -24,7 +24,6 @@ AGENT_TURN_TIMEOUT_SECONDS="${OPENCLAW_INSTALL_E2E_AGENT_TURN_TIMEOUT_SECONDS:-6 export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-$HOME/.npm-global}" mkdir -p "$NPM_CONFIG_PREFIX" export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" -export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}" if [[ "$MODELS_MODE" != "both" && "$MODELS_MODE" != "openai" && "$MODELS_MODE" != "anthropic" ]]; then echo "ERROR: OPENCLAW_E2E_MODELS must be one of: both|openai|anthropic" >&2 diff --git a/src/infra/unhandled-rejections.fatal-detection.test.ts b/src/infra/unhandled-rejections.fatal-detection.test.ts index f010dfbbd52..6ee3a604010 100644 --- a/src/infra/unhandled-rejections.fatal-detection.test.ts +++ b/src/infra/unhandled-rejections.fatal-detection.test.ts @@ -196,6 +196,32 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { ); }); + it("does not exit on known Bonjour dependency failures", () => { + const bonjourCases: unknown[] = [ + new Error("CIAO ANNOUNCEMENT CANCELLED"), + new Error("CIAO PROBING CANCELLED"), + Object.assign( + new Error("Reached illegal state! IPV4 address change from defined to undefined!"), + { name: "AssertionError" }, + ), + Object.assign( + new Error( + "IP address version must match. Netmask cannot have a version different from the address!", + ), + { name: "AssertionError" }, + ), + ]; + + for (const bonjourErr of bonjourCases) { + expectExitCodeFromUnhandled(bonjourErr, []); + } + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[openclaw] Non-fatal unhandled rejection (continuing):", + expect.stringContaining("CIAO ANNOUNCEMENT CANCELLED"), + ); + }); + it("exits on generic errors without code", () => { const genericErr = new Error("Something went wrong"); diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 219fda7a10f..c2c89702c80 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -116,6 +116,12 @@ const TRANSIENT_SQLITE_MESSAGE_SNIPPETS = [ "disk i/o error", ]; +const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u; +const CIAO_INTERFACE_ASSERTION_MESSAGE_RE = + /REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u; +const CIAO_NETMASK_ASSERTION_MESSAGE_RE = + /IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u; + function hasSqliteSignal(err: unknown): boolean { if (!err || typeof err !== "object") { return false; @@ -335,8 +341,46 @@ export function isTransientSqliteError(err: unknown): boolean { return false; } +export function isKnownBonjourDependencyError(err: unknown): boolean { + if (!err) { + return false; + } + + for (const candidate of collectNestedUnhandledErrorCandidates(err)) { + const rawMessage = + candidate && typeof candidate === "object" + ? (candidate as { message?: unknown }).message + : undefined; + const message = + typeof candidate === "string" + ? candidate + : candidate && typeof candidate === "object" + ? typeof rawMessage === "string" + ? rawMessage + : "" + : ""; + const normalized = message.trim().toUpperCase(); + if (!normalized) { + continue; + } + if ( + CIAO_CANCELLATION_MESSAGE_RE.test(normalized) || + CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(normalized) || + CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(normalized) + ) { + return true; + } + } + + return false; +} + export function isTransientUnhandledRejectionError(err: unknown): boolean { - return isTransientNetworkError(err) || isTransientSqliteError(err); + return ( + isTransientNetworkError(err) || + isTransientSqliteError(err) || + isKnownBonjourDependencyError(err) + ); } export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHandler): () => void { diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 1f5b42f4cfb..288a7b4a547 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest"; const SCRIPT_PATH = "scripts/test-install-sh-docker.sh"; const SMOKE_RUNNER_PATH = "scripts/docker/install-sh-smoke/run.sh"; -const E2E_RUNNER_PATH = "scripts/docker/install-sh-e2e/run.sh"; const BUN_GLOBAL_SMOKE_PATH = "scripts/e2e/bun-global-install-smoke.sh"; const INSTALL_SMOKE_WORKFLOW_PATH = ".github/workflows/install-smoke.yml"; const RELEASE_CHECKS_WORKFLOW_PATH = ".github/workflows/openclaw-release-checks.yml"; @@ -129,14 +128,6 @@ describe("install-sh smoke runner", () => { }); }); -describe("install-sh e2e runner", () => { - it("disables Bonjour for Docker loopback gateway checks", () => { - const script = readFileSync(E2E_RUNNER_PATH, "utf8"); - - expect(script).toContain('export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}"'); - }); -}); - describe("bun global install smoke", () => { it("packs the current tree and verifies image-provider discovery through Bun", () => { const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8");