fix: harden google meet node bridge listing (#72372)

This commit is contained in:
Peter Steinberger
2026-04-27 07:31:23 +01:00
parent 3536018db0
commit d583a6b615
3 changed files with 121 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
- Voice Call: allow SecretRef-backed Twilio auth tokens and call-specific OpenAI/ElevenLabs TTS API keys through the plugin config surface. Fixes #68690. Thanks @joshavant.
- Google Meet: clean stale chrome-node realtime audio bridges by URL before rejoining, expose active node bridge inspection, and tolerate transient node input pull failures instead of dropping the Meet session. Fixes #72371. (#72372) Thanks @BsnizND.
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.

View File

@@ -0,0 +1,115 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
type MockChild = EventEmitter & {
exitCode: number | null;
signalCode: NodeJS.Signals | null;
kill: ReturnType<typeof vi.fn>;
stdout?: EventEmitter;
stderr?: EventEmitter;
stdin?: { write: ReturnType<typeof vi.fn> };
};
const children: MockChild[] = [];
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return {
...actual,
spawnSync: vi.fn(() => ({
status: 0,
stdout: "BlackHole 2ch",
stderr: "",
})),
spawn: vi.fn(() => {
const child = Object.assign(new EventEmitter(), {
exitCode: null,
signalCode: null,
kill: vi.fn((signal?: NodeJS.Signals) => {
child.signalCode = signal ?? "SIGTERM";
return true;
}),
stdout: new EventEmitter(),
stderr: new EventEmitter(),
stdin: { write: vi.fn() },
}) as MockChild;
children.push(child);
return child;
}),
};
});
describe("google-meet node host bridge sessions", () => {
it("lists active bridge sessions and hides closed sessions", async () => {
const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js");
const originalPlatform = process.platform;
children.length = 0;
Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
try {
const start = JSON.parse(
await handleGoogleMeetNodeHostCommand(
JSON.stringify({
action: "start",
url: "https://meet.google.com/abc-defg-hij?authuser=1",
mode: "realtime",
launch: false,
audioInputCommand: ["mock-rec"],
audioOutputCommand: ["mock-play"],
}),
),
);
expect(start).toMatchObject({
audioBridge: { type: "node-command-pair" },
bridgeId: expect.any(String),
});
const activeList = JSON.parse(
await handleGoogleMeetNodeHostCommand(
JSON.stringify({
action: "list",
url: "https://meet.google.com/abc-defg-hij",
mode: "realtime",
}),
),
);
expect(activeList.bridges).toHaveLength(1);
expect(activeList.bridges[0]).toMatchObject({
bridgeId: start.bridgeId,
closed: false,
mode: "realtime",
url: "https://meet.google.com/abc-defg-hij?authuser=1",
});
children[1]?.emit("exit", 0, null);
const afterExitList = JSON.parse(
await handleGoogleMeetNodeHostCommand(
JSON.stringify({
action: "list",
url: "https://meet.google.com/abc-defg-hij",
mode: "realtime",
}),
),
);
expect(afterExitList).toEqual({ bridges: [] });
const stopped = JSON.parse(
await handleGoogleMeetNodeHostCommand(
JSON.stringify({
action: "stopByUrl",
url: "https://meet.google.com/abc-defg-hij",
mode: "realtime",
}),
),
);
expect(stopped).toEqual({ ok: true, stopped: 0 });
} finally {
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
}
});
});

View File

@@ -361,6 +361,7 @@ function listSessions(params: Record<string, unknown>) {
const urlKey = normalizeMeetKey(readString(params.url));
const mode = readString(params.mode);
const bridges = [...sessions.values()]
.filter((session) => !session.closed)
.filter((session) => !urlKey || normalizeMeetKey(session.url) === urlKey)
.filter((session) => !mode || session.mode === mode)
.map(summarizeSession);
@@ -385,9 +386,12 @@ function stopSessionsByUrl(params: Record<string, unknown>) {
if (mode && session.mode !== mode) {
continue;
}
const wasClosed = session.closed;
stopSession(session);
sessions.delete(bridgeId);
stopped += 1;
if (!wasClosed) {
stopped += 1;
}
}
return { ok: true, stopped };
}