mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: harden google meet node bridge listing (#72372)
This commit is contained in:
@@ -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.
|
||||
|
||||
115
extensions/google-meet/node-host.test.ts
Normal file
115
extensions/google-meet/node-host.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user