fix(bonjour): keep ciao failure handling extension-owned

This commit is contained in:
Peter Steinberger
2026-04-26 23:29:40 +01:00
parent 0472b6197a
commit c9c0ab3a44
4 changed files with 81 additions and 81 deletions

View File

@@ -48,6 +48,34 @@ describe("bonjour-ciao", () => {
expect(ignoreCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true);
});
it("suppresses wrapped ciao cancellation rejections", () => {
expect(
classifyCiaoUnhandledRejection({
reason: new Error("CIAO ANNOUNCEMENT CANCELLED"),
}),
).toEqual({
kind: "cancellation",
formatted: "CIAO ANNOUNCEMENT CANCELLED",
});
});
it("suppresses aggregate ciao assertion rejections", () => {
expect(
classifyCiaoUnhandledRejection(
new AggregateError([
Object.assign(
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
{ name: "AssertionError" },
),
]),
),
).toEqual({
kind: "interface-assertion",
formatted:
"AssertionError: Reached illegal state! IPV4 address change from defined to undefined!",
});
});
it("suppresses lower-case string cancellation reasons too", () => {
expect(ignoreCiaoUnhandledRejection("ciao announcement cancelled during cleanup")).toBe(true);
});

View File

@@ -11,17 +11,59 @@ export type CiaoProcessErrorClassification =
| { kind: "interface-assertion"; formatted: string }
| { kind: "netmask-assertion"; formatted: string };
function collectCiaoProcessErrorCandidates(reason: unknown): unknown[] {
const queue: unknown[] = [reason];
const seen = new Set<unknown>();
const candidates: unknown[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current == null || seen.has(current)) {
continue;
}
seen.add(current);
candidates.push(current);
if (!current || typeof current !== "object") {
continue;
}
const record = current as Record<string, unknown>;
for (const nested of [
record.cause,
record.reason,
record.original,
record.error,
record.data,
]) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
if (Array.isArray(record.errors)) {
for (const nested of record.errors) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
}
}
return candidates;
}
export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClassification | null {
const formatted = formatBonjourError(reason);
const message = formatted.toUpperCase();
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return { kind: "cancellation", formatted };
}
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "interface-assertion", formatted };
}
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "netmask-assertion", formatted };
for (const candidate of collectCiaoProcessErrorCandidates(reason)) {
const formatted = formatBonjourError(candidate);
const message = formatted.toUpperCase();
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return { kind: "cancellation", formatted };
}
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "interface-assertion", formatted };
}
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "netmask-assertion", formatted };
}
}
return null;
}

View File

@@ -196,32 +196,6 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
);
});
it("does not exit on known Bonjour advertiser 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");

View File

@@ -116,12 +116,6 @@ 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;
@@ -341,46 +335,8 @@ export function isTransientSqliteError(err: unknown): boolean {
return false;
}
export function isNonFatalBonjourAdvertiserError(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) ||
isNonFatalBonjourAdvertiserError(err)
);
return isTransientNetworkError(err) || isTransientSqliteError(err);
}
export function registerUnhandledRejectionHandler(handler: UnhandledRejectionHandler): () => void {