import { mkdtempSync, readFileSync, rmSync } from
"node:fs" ;
import { tmpdir } from
"node:os" ;
import path from
"node:path" ;
import { Command } from
"commander" ;
import { afterEach, describe, expect, it, vi } from
"vitest" ;
import { registerGoogleMeetCli } from
"./cli.js" ;
import { resolveGoogleMeetConfig } from
"./config.js" ;
import type { GoogleMeetRuntime } from
"./runtime.js" ;
const fetchGuardMocks = vi.hoisted(() => ({
fetchWithSsrFGuard: vi.fn(
async (params: {
url: string;
init?: RequestInit;
}): Promise<{
response: Response;
release: () => Promise<
void >;
}> => ({
response: await fetch(params.url, params.init),
release: vi.fn(async () => {}),
}),
),
}));
vi.mock(
"openclaw/plugin-sdk/ssrf-runtime" , () => ({
fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard,
}));
function captureStdout() {
let output =
"" ;
const writeSpy = vi.spyOn(process.stdout,
"write" ).mockImplementation(((chunk: unknow
n) => {
output += String(chunk);
return true ;
}) as typeof process.stdout.write);
return {
output: () => output,
restore: () => writeSpy.mockRestore(),
};
}
function jsonResponse(value: unknown): Response {
return new Response(JSON.stringify(value), {
status: 200 ,
headers: { "Content-Type" : "application/json" },
});
}
function requestUrl(input: RequestInfo | URL): URL {
if (typeof input === "string" ) {
return new URL(input);
}
if (input instanceof URL) {
return input;
}
return new URL(input.url);
}
function stubMeetArtifactsApi() {
vi.stubGlobal(
"fetch" ,
vi.fn(async (input: RequestInfo | URL) => {
const url = requestUrl(input);
if (url.pathname === "/v2/spaces/abc-defg-hij" ) {
return jsonResponse({
name: "spaces/abc-defg-hij" ,
meetingCode: "abc-defg-hij" ,
meetingUri: "https://meet.google.com/abc-defg-hij ",
});
}
if (url.pathname === "/v2/conferenceRecords" ) {
return jsonResponse({
conferenceRecords: [
{
name: "conferenceRecords/rec-1" ,
space: "spaces/abc-defg-hij" ,
startTime: "2026-04-25T10:00:00Z" ,
endTime: "2026-04-25T10:30:00Z" ,
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1" ) {
return jsonResponse({
name: "conferenceRecords/rec-1" ,
space: "spaces/abc-defg-hij" ,
startTime: "2026-04-25T10:00:00Z" ,
endTime: "2026-04-25T10:30:00Z" ,
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/participants" ) {
return jsonResponse({
participants: [
{
name: "conferenceRecords/rec-1/participants/p1" ,
signedinUser: { displayName: "Alice" },
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions" ) {
return jsonResponse({
participantSessions: [
{
name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1" ,
startTime: "2026-04-25T10:00:00Z" ,
endTime: "2026-04-25T10:10:00Z" ,
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/recordings" ) {
return jsonResponse({
recordings: [
{
name: "conferenceRecords/rec-1/recordings/r1" ,
state: "FILE_GENERATED" ,
driveDestination: { file: "drive-file-1" },
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts" ) {
return jsonResponse({
transcripts: [
{
name: "conferenceRecords/rec-1/transcripts/t1" ,
state: "FILE_GENERATED" ,
docsDestination: { document: "doc-1" },
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts/t1/entries" ) {
return jsonResponse({
transcriptEntries: [
{
name: "conferenceRecords/rec-1/transcripts/t1/entries/e1" ,
text: "Hello from the transcript." ,
startTime: "2026-04-25T10:01:00Z" ,
participant: "conferenceRecords/rec-1/participants/p1" ,
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes" ) {
return jsonResponse({
smartNotes: [
{
name: "conferenceRecords/rec-1/smartNotes/sn1" ,
state: "FILE_GENERATED" ,
docsDestination: { document: "notes-1" },
},
],
});
}
return new Response("not found" , { status: 404 });
}),
);
}
function setupCli(params: {
config?: Parameters<typeof resolveGoogleMeetConfig>[0 ];
runtime?: Partial<GoogleMeetRuntime>;
ensureRuntime?: () => Promise<GoogleMeetRuntime>;
}) {
const program = new Command();
registerGoogleMeetCli({
program,
config: resolveGoogleMeetConfig(params.config ?? {}),
ensureRuntime:
params.ensureRuntime ?? (async () => (params.runtime ?? {}) as unknown as GoogleMeetRuntime),
});
return program;
}
describe("google-meet CLI" , () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("prints setup checks as text and JSON" , async () => {
{
const stdout = captureStdout();
try {
await setupCli({
runtime: {
setupStatus: async () => ({
ok: true ,
checks: [
{
id: "audio-bridge" ,
ok: true ,
message: "Chrome command-pair realtime audio bridge configured" ,
},
],
}),
},
}).parseAsync(["googlemeet" , "setup" ], { from: "user" });
expect(stdout.output()).toContain("Google Meet setup: OK" );
expect(stdout.output()).toContain(
"[ok] audio-bridge: Chrome command-pair realtime audio bridge configured" ,
);
expect(stdout.output()).not.toContain('"checks"' );
} finally {
stdout.restore();
}
}
{
const stdout = captureStdout();
try {
await setupCli({
runtime: {
setupStatus: async () => ({
ok: false ,
checks: [{ id: "twilio-voice-call-plugin" , ok: false , message: "missing" }],
}),
},
}).parseAsync(["googlemeet" , "setup" , "--json" ], { from: "user" });
expect(JSON.parse(stdout.output())).toMatchObject({
ok: false ,
checks: [{ id: "twilio-voice-call-plugin" , ok: false }],
});
} finally {
stdout.restore();
}
}
});
it("prints artifacts and attendance output" , async () => {
stubMeetArtifactsApi();
const artifactsStdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet" ,
"artifacts" ,
"--access-token" ,
"token" ,
"--expires-at" ,
String(Date.now() + 120 _000 ),
"--conference-record" ,
"rec-1" ,
"--json" ,
],
{ from: "user" },
);
expect(JSON.parse(artifactsStdout.output())).toMatchObject({
conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
artifacts: [
{
recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
transcriptEntries: [
{
transcript: "conferenceRecords/rec-1/transcripts/t1" ,
entries: [{ text: "Hello from the transcript." }],
},
],
smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
},
],
tokenSource: "cached-access-token" ,
});
} finally {
artifactsStdout.restore();
}
const attendanceStdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet" ,
"attendance" ,
"--access-token" ,
"token" ,
"--expires-at" ,
String(Date.now() + 120 _000 ),
"--conference-record" ,
"rec-1" ,
],
{ from: "user" },
);
expect(attendanceStdout.output()).toContain("attendance rows: 1" );
expect(attendanceStdout.output()).toContain("participant: Alice" );
expect(attendanceStdout.output()).toContain(
"conferenceRecords/rec-1/participants/p1/participantSessions/s1" ,
);
} finally {
attendanceStdout.restore();
}
});
it("prints the latest conference record" , async () => {
stubMeetArtifactsApi();
const stdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet" ,
"latest" ,
"--access-token" ,
"token" ,
"--expires-at" ,
String(Date.now() + 120 _000 ),
"--meeting" ,
"abc-defg-hij" ,
],
{ from: "user" },
);
expect(stdout.output()).toContain("space: spaces/abc-defg-hij" );
expect(stdout.output()).toContain("conference record: conferenceRecords/rec-1" );
} finally {
stdout.restore();
}
});
it("prints markdown artifact and attendance output" , async () => {
stubMeetArtifactsApi();
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-artifacts-" ));
const outputPath = path.join(tempDir, "artifacts.md" );
const artifactsStdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet" ,
"artifacts" ,
"--access-token" ,
"token" ,
"--expires-at" ,
String(Date.now() + 120 _000 ),
"--conference-record" ,
"rec-1" ,
"--format" ,
"markdown" ,
"--output" ,
outputPath,
],
{ from: "user" },
);
const markdown = readFileSync(outputPath, "utf8" );
expect(artifactsStdout.output()).toContain(`wrote: ${outputPath}`);
expect(markdown).toContain("# Google Meet Artifacts" );
expect(markdown).toContain("## conferenceRecords/rec-1" );
expect(markdown).toContain("### Transcript Entries: conferenceRecords/rec-1/transcripts/t1" );
expect(markdown).toContain("Hello from the transcript." );
} finally {
artifactsStdout.restore();
rmSync(tempDir, { recursive: true , force: true });
}
const attendanceStdout = captureStdout();
try {
await setupCli({}).parseAsync(
[
"googlemeet" ,
"attendance" ,
"--access-token" ,
"token" ,
"--expires-at" ,
String(Date.now() + 120 _000 ),
"--conference-record" ,
"rec-1" ,
"--format" ,
"markdown" ,
],
{ from: "user" },
);
expect(attendanceStdout.output()).toContain("# Google Meet Attendance" );
expect(attendanceStdout.output()).toContain("## Alice" );
expect(attendanceStdout.output()).toContain(
"conferenceRecords/rec-1/participants/p1/participantSessions/s1" ,
);
} finally {
attendanceStdout.restore();
}
});
it("prints human-readable session doctor output" , async () => {
const stdout = captureStdout();
try {
await setupCli({
runtime: {
status: () => ({
found: true ,
session: {
id: "meet_1" ,
url: "https://meet.google.com/abc-defg-hij ",
state: "active" ,
transport: "chrome-node" ,
mode: "realtime" ,
participantIdentity: "signed-in Google Chrome profile on a paired node" ,
createdAt: "2026-04-25T00:00:00.000Z" ,
updatedAt: "2026-04-25T00:00:01.000Z" ,
realtime: { enabled: true , provider: "openai" , toolPolicy: "safe-read-only" },
chrome: {
audioBackend: "blackhole-2ch" ,
launched: true ,
nodeId: "node-1" ,
audioBridge: { type: "node-command-pair" , provider: "openai" },
health: {
inCall: true ,
providerConnected: true ,
realtimeReady: true ,
audioInputActive: true ,
audioOutputActive: false ,
lastInputAt: "2026-04-25T00:00:02.000Z" ,
lastInputBytes: 160 ,
lastOutputBytes: 0 ,
},
},
notes: [],
},
}),
},
}).parseAsync(["googlemeet" , "doctor" , "meet_1" ], { from: "user" });
expect(stdout.output()).toContain("session: meet_1" );
expect(stdout.output()).toContain("node: node-1" );
expect(stdout.output()).toContain("provider connected: yes" );
expect(stdout.output()).toContain("audio input active: yes" );
expect(stdout.output()).toContain("audio output active: no" );
} finally {
stdout.restore();
}
});
it("verifies OAuth refresh without printing secrets" , async () => {
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
jsonResponse({
access_token: "new-access-token" ,
expires_in: 3600 ,
token_type: "Bearer" ,
}),
);
vi.stubGlobal("fetch" , fetchMock);
const ensureRuntime = vi.fn(async () => {
throw new Error("runtime should not be loaded for OAuth doctor" );
});
const stdout = captureStdout();
try {
await setupCli({
config: {
oauth: {
clientId: "client-id" ,
clientSecret: "client-secret" ,
refreshToken: "rt-secret" ,
},
},
ensureRuntime: ensureRuntime as unknown as () => Promise<GoogleMeetRuntime>,
}).parseAsync(["googlemeet" , "doctor" , "--oauth" , "--json" ], { from: "user" });
const output = stdout.output();
expect(output).not.toContain("new-access-token" );
expect(output).not.toContain("rt-secret" );
expect(output).not.toContain("client-secret" );
expect(JSON.parse(output)).toMatchObject({
ok: true ,
configured: true ,
tokenSource: "refresh-token" ,
checks: [
{ id: "oauth-config" , ok: true },
{ id: "oauth-token" , ok: true },
],
});
expect(ensureRuntime).not.toHaveBeenCalled();
const body = fetchMock.mock.calls[0 ]?.[1 ]?.body as URLSearchParams;
expect(body.get("grant_type" )).toBe("refresh_token" );
} finally {
stdout.restore();
}
});
it("can prove Google Meet API create access" , async () => {
vi.stubGlobal(
"fetch" ,
vi.fn(async (input: RequestInfo | URL) => {
const url = requestUrl(input).href;
if (url === "https://oauth2.googleapis.com/token ") {
return jsonResponse({
access_token: "new-access-token" ,
expires_in: 3600 ,
token_type: "Bearer" ,
});
}
if (url === "https://meet.googleapis.com/v2/spaces ") {
return jsonResponse({
name: "spaces/new-space" ,
meetingUri: "https://meet.google.com/new-abcd-xyz ",
});
}
return new Response("not found" , { status: 404 });
}),
);
const stdout = captureStdout();
try {
await setupCli({
config: {
oauth: {
clientId: "client-id" ,
refreshToken: "refresh-token" ,
},
},
}).parseAsync(["googlemeet" , "doctor" , "--oauth" , "--create-space" , "--json" ], {
from: "user" ,
});
expect(JSON.parse(stdout.output())).toMatchObject({
ok: true ,
tokenSource: "refresh-token" ,
createdSpace: "spaces/new-space" ,
meetingUri: "https://meet.google.com/new-abcd-xyz ",
checks: [
{ id: "oauth-config" , ok: true },
{ id: "oauth-token" , ok: true },
{ id: "meet-spaces-create" , ok: true },
],
});
} finally {
stdout.restore();
}
});
it("recovers and summarizes an existing Meet tab" , async () => {
const stdout = captureStdout();
try {
await setupCli({
config: { defaultTransport: "chrome-node" },
runtime: {
recoverCurrentTab: async () => ({
nodeId: "node-1" ,
found: true ,
targetId: "tab-1" ,
tab: { targetId: "tab-1" , url: "https://meet.google.com/abc-defg-hij " },
browser: {
inCall: false ,
manualActionRequired: true ,
manualActionReason: "meet-admission-required" ,
manualActionMessage: "Admit the OpenClaw browser participant in Google Meet." ,
browserUrl: "https://meet.google.com/abc-defg-hij ",
},
message: "Admit the OpenClaw browser participant in Google Meet." ,
}),
},
}).parseAsync(["googlemeet" , "recover-tab" ], { from: "user" });
expect(stdout.output()).toContain("Google Meet current tab: found" );
expect(stdout.output()).toContain("target: tab-1" );
expect(stdout.output()).toContain("manual reason: meet-admission-required" );
} finally {
stdout.restore();
}
});
});
Messung V0.5 in Prozent C=99 H=99 G=98
¤ Dauer der Verarbeitung: 0.6 Sekunden
¤
*© Formatika GbR, Deutschland