mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
perf: speed up google meet tests
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough, Writable } from "node:stream";
|
||||
import { Command } from "commander";
|
||||
import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
import { registerGoogleMeetCli } from "./src/cli.js";
|
||||
import { resolveGoogleMeetConfig, resolveGoogleMeetConfigWithEnv } from "./src/config.js";
|
||||
import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
@@ -18,20 +13,10 @@ import {
|
||||
fetchGoogleMeetSpace,
|
||||
normalizeGoogleMeetSpaceName,
|
||||
} from "./src/meet.js";
|
||||
import {
|
||||
buildGoogleMeetAuthUrl,
|
||||
refreshGoogleMeetAccessToken,
|
||||
resolveGoogleMeetAccessToken,
|
||||
} from "./src/oauth.js";
|
||||
import { startNodeRealtimeAudioBridge } from "./src/realtime-node.js";
|
||||
import { startCommandRealtimeAudioBridge } from "./src/realtime.js";
|
||||
import { normalizeMeetUrl } from "./src/runtime.js";
|
||||
import type { GoogleMeetRuntime } from "./src/runtime.js";
|
||||
import {
|
||||
captureStdout,
|
||||
noopLogger,
|
||||
setupGoogleMeetPlugin,
|
||||
} from "./src/test-support/plugin-harness.js";
|
||||
import { noopLogger, setupGoogleMeetPlugin } from "./src/test-support/plugin-harness.js";
|
||||
import { buildMeetDtmfSequence, normalizeDialInNumber } from "./src/transports/twilio.js";
|
||||
|
||||
const voiceCallMocks = vi.hoisted(() => ({
|
||||
@@ -595,63 +580,6 @@ describe("google-meet plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("builds Meet OAuth URLs and prefers fresh cached access tokens", async () => {
|
||||
const url = new URL(
|
||||
buildGoogleMeetAuthUrl({
|
||||
clientId: "client-id",
|
||||
challenge: "challenge",
|
||||
state: "state",
|
||||
}),
|
||||
);
|
||||
expect(url.hostname).toBe("accounts.google.com");
|
||||
expect(url.searchParams.get("client_id")).toBe("client-id");
|
||||
expect(url.searchParams.get("code_challenge")).toBe("challenge");
|
||||
expect(url.searchParams.get("access_type")).toBe("offline");
|
||||
expect(url.searchParams.get("scope")).toContain("meetings.space.created");
|
||||
expect(url.searchParams.get("scope")).toContain("meetings.conference.media.readonly");
|
||||
|
||||
await expect(
|
||||
resolveGoogleMeetAccessToken({
|
||||
accessToken: "cached-token",
|
||||
expiresAt: Date.now() + 120_000,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
accessToken: "cached-token",
|
||||
expiresAt: expect.any(Number),
|
||||
refreshed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes Google Meet access tokens with a refresh-token grant", async () => {
|
||||
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: "new-access-token",
|
||||
expires_in: 3600,
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await expect(
|
||||
refreshGoogleMeetAccessToken({
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
refreshToken: "refresh-token",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
accessToken: "new-access-token",
|
||||
tokenType: "Bearer",
|
||||
});
|
||||
const body = fetchMock.mock.calls[0]?.[1]?.body;
|
||||
expect(body).toBeInstanceOf(URLSearchParams);
|
||||
const params = body as URLSearchParams;
|
||||
expect(params.get("grant_type")).toBe("refresh_token");
|
||||
expect(params.get("refresh_token")).toBe("refresh-token");
|
||||
});
|
||||
|
||||
it("builds Twilio dial plans from a PIN", () => {
|
||||
expect(normalizeDialInNumber("+1 (555) 123-4567")).toBe("+15551234567");
|
||||
expect(buildMeetDtmfSequence({ pin: "123 456" })).toBe("123456#");
|
||||
@@ -882,459 +810,6 @@ describe("google-meet plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("CLI setup prints human-readable checks by default", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () =>
|
||||
({
|
||||
setupStatus: async () => ({
|
||||
ok: true,
|
||||
checks: [
|
||||
{
|
||||
id: "audio-bridge",
|
||||
ok: true,
|
||||
message: "Chrome command-pair realtime audio bridge configured",
|
||||
},
|
||||
],
|
||||
}),
|
||||
}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI setup preserves JSON output with --json", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () =>
|
||||
({
|
||||
setupStatus: async () => ({
|
||||
ok: false,
|
||||
checks: [{ id: "twilio-voice-call-plugin", ok: false, message: "missing" }],
|
||||
}),
|
||||
}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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("CLI artifacts prints JSON output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"artifacts",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--json",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(JSON.parse(stdout.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 {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI latest prints the latest conference record", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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("CLI artifacts writes markdown output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-artifacts-"));
|
||||
const outputPath = path.join(tempDir, "artifacts.md");
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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(stdout.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 {
|
||||
stdout.restore();
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI attendance prints participant sessions by default", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"attendance",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(stdout.output()).toContain("attendance rows: 1");
|
||||
expect(stdout.output()).toContain("participant: Alice");
|
||||
expect(stdout.output()).toContain(
|
||||
"conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
);
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI attendance prints markdown output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"attendance",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--format",
|
||||
"markdown",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(stdout.output()).toContain("# Google Meet Attendance");
|
||||
expect(stdout.output()).toContain("## Alice");
|
||||
expect(stdout.output()).toContain(
|
||||
"conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
);
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI doctor prints human-readable session health", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () =>
|
||||
({
|
||||
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: [],
|
||||
},
|
||||
}),
|
||||
}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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("CLI doctor verifies Google Meet OAuth refresh without printing secrets", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: "new-access-token",
|
||||
expires_in: 3600,
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const ensureRuntime = vi.fn(async () => {
|
||||
throw new Error("runtime should not be loaded for OAuth doctor");
|
||||
});
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({
|
||||
oauth: {
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
refreshToken: "rt-secret",
|
||||
},
|
||||
}),
|
||||
ensureRuntime: ensureRuntime as unknown as () => Promise<GoogleMeetRuntime>,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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("CLI doctor can prove Google Meet API create access", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
if (url === "https://oauth2.googleapis.com/token") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: "new-access-token",
|
||||
expires_in: 3600,
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "https://meet.googleapis.com/v2/spaces") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "spaces/new-space",
|
||||
meetingUri: "https://meet.google.com/new-abcd-xyz",
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
}),
|
||||
);
|
||||
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({
|
||||
oauth: {
|
||||
clientId: "client-id",
|
||||
refreshToken: "refresh-token",
|
||||
},
|
||||
}),
|
||||
ensureRuntime: async () => ({}) as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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("CLI recover-tab focuses and summarizes an existing Meet tab", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({ defaultTransport: "chrome-node" }),
|
||||
ensureRuntime: async () =>
|
||||
({
|
||||
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.",
|
||||
}),
|
||||
}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.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();
|
||||
}
|
||||
});
|
||||
|
||||
it("launches Chrome after the BlackHole check", async () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
|
||||
@@ -3,18 +3,12 @@ import type { GatewayRequestHandlerOptions } from "openclaw/plugin-sdk/gateway-r
|
||||
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { registerGoogleMeetCli } from "./src/cli.js";
|
||||
import {
|
||||
resolveGoogleMeetConfig,
|
||||
type GoogleMeetConfig,
|
||||
type GoogleMeetMode,
|
||||
type GoogleMeetTransport,
|
||||
} from "./src/config.js";
|
||||
import {
|
||||
createAndJoinMeetFromParams,
|
||||
createMeetFromParams,
|
||||
shouldJoinCreatedMeet,
|
||||
} from "./src/create.js";
|
||||
import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
fetchGoogleMeetArtifacts,
|
||||
@@ -23,7 +17,6 @@ import {
|
||||
fetchGoogleMeetSpace,
|
||||
} from "./src/meet.js";
|
||||
import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
|
||||
import { resolveGoogleMeetAccessToken } from "./src/oauth.js";
|
||||
import { GoogleMeetRuntime } from "./src/runtime.js";
|
||||
import { isGoogleMeetBrowserManualActionError } from "./src/transports/chrome-create.js";
|
||||
|
||||
@@ -244,10 +237,34 @@ function resolveOptionalPositiveInteger(value: unknown): number | undefined {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function shouldJoinCreatedMeet(raw: Record<string, unknown>): boolean {
|
||||
return raw.join !== false && raw.join !== "false";
|
||||
}
|
||||
|
||||
async function createMeetFromParams(params: {
|
||||
config: GoogleMeetConfig;
|
||||
runtime: OpenClawPluginApi["runtime"];
|
||||
raw: Record<string, unknown>;
|
||||
}) {
|
||||
const create = await import("./src/create.js");
|
||||
return create.createMeetFromParams(params);
|
||||
}
|
||||
|
||||
async function createAndJoinMeetFromParams(params: {
|
||||
config: GoogleMeetConfig;
|
||||
runtime: OpenClawPluginApi["runtime"];
|
||||
raw: Record<string, unknown>;
|
||||
ensureRuntime: () => Promise<GoogleMeetRuntime>;
|
||||
}) {
|
||||
const create = await import("./src/create.js");
|
||||
return create.createAndJoinMeetFromParams(params);
|
||||
}
|
||||
|
||||
async function resolveGoogleMeetTokenFromParams(
|
||||
config: GoogleMeetConfig,
|
||||
raw: Record<string, unknown>,
|
||||
) {
|
||||
const { resolveGoogleMeetAccessToken } = await import("./src/oauth.js");
|
||||
return resolveGoogleMeetAccessToken({
|
||||
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
|
||||
clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
|
||||
@@ -661,12 +678,14 @@ export default definePluginEntry({
|
||||
});
|
||||
|
||||
api.registerCli(
|
||||
({ program }) =>
|
||||
async ({ program }) => {
|
||||
const { registerGoogleMeetCli } = await import("./src/cli.js");
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config,
|
||||
ensureRuntime,
|
||||
}),
|
||||
});
|
||||
},
|
||||
{
|
||||
commands: ["googlemeet"],
|
||||
descriptors: [
|
||||
|
||||
555
extensions/google-meet/src/cli.test.ts
Normal file
555
extensions/google-meet/src/cli.test.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
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: unknown) => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
69
extensions/google-meet/src/oauth.test.ts
Normal file
69
extensions/google-meet/src/oauth.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildGoogleMeetAuthUrl,
|
||||
refreshGoogleMeetAccessToken,
|
||||
resolveGoogleMeetAccessToken,
|
||||
} from "./oauth.js";
|
||||
|
||||
describe("Google Meet OAuth", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("builds auth URLs and prefers fresh cached access tokens", async () => {
|
||||
const url = new URL(
|
||||
buildGoogleMeetAuthUrl({
|
||||
clientId: "client-id",
|
||||
challenge: "challenge",
|
||||
state: "state",
|
||||
}),
|
||||
);
|
||||
expect(url.hostname).toBe("accounts.google.com");
|
||||
expect(url.searchParams.get("client_id")).toBe("client-id");
|
||||
expect(url.searchParams.get("code_challenge")).toBe("challenge");
|
||||
expect(url.searchParams.get("access_type")).toBe("offline");
|
||||
expect(url.searchParams.get("scope")).toContain("meetings.space.created");
|
||||
expect(url.searchParams.get("scope")).toContain("meetings.conference.media.readonly");
|
||||
|
||||
await expect(
|
||||
resolveGoogleMeetAccessToken({
|
||||
accessToken: "cached-token",
|
||||
expiresAt: Date.now() + 120_000,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
accessToken: "cached-token",
|
||||
expiresAt: expect.any(Number),
|
||||
refreshed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes access tokens with a refresh-token grant", async () => {
|
||||
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: "new-access-token",
|
||||
expires_in: 3600,
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await expect(
|
||||
refreshGoogleMeetAccessToken({
|
||||
clientId: "client-id",
|
||||
clientSecret: "client-secret",
|
||||
refreshToken: "refresh-token",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
accessToken: "new-access-token",
|
||||
tokenType: "Bearer",
|
||||
});
|
||||
const body = fetchMock.mock.calls[0]?.[1]?.body;
|
||||
expect(body).toBeInstanceOf(URLSearchParams);
|
||||
const params = body as URLSearchParams;
|
||||
expect(params.get("grant_type")).toBe("refresh_token");
|
||||
expect(params.get("refresh_token")).toBe("refresh-token");
|
||||
});
|
||||
});
|
||||
@@ -249,6 +249,10 @@ const TOOLING_TEST_TARGETS = new Map([
|
||||
]);
|
||||
const SOURCE_TEST_TARGETS = new Map([
|
||||
...PRECISE_SOURCE_TEST_TARGETS,
|
||||
["extensions/google-meet/index.ts", ["extensions/google-meet/index.test.ts"]],
|
||||
["extensions/google-meet/src/cli.ts", ["extensions/google-meet/src/cli.test.ts"]],
|
||||
["extensions/google-meet/src/create.ts", ["extensions/google-meet/index.test.ts"]],
|
||||
["extensions/google-meet/src/oauth.ts", ["extensions/google-meet/src/oauth.test.ts"]],
|
||||
["src/agents/live-model-turn-probes.ts", ["src/agents/live-model-turn-probes.test.ts"]],
|
||||
[
|
||||
"src/auto-reply/reply/dispatch-from-config.ts",
|
||||
|
||||
@@ -301,6 +301,28 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes Google Meet CLI edits to the lightweight CLI tests", () => {
|
||||
expect(resolveChangedTestTargetPlan(["extensions/google-meet/src/cli.ts"])).toEqual({
|
||||
mode: "targets",
|
||||
targets: ["extensions/google-meet/src/cli.test.ts"],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes Google Meet OAuth edits to the lightweight OAuth tests", () => {
|
||||
expect(resolveChangedTestTargetPlan(["extensions/google-meet/src/oauth.ts"])).toEqual({
|
||||
mode: "targets",
|
||||
targets: ["extensions/google-meet/src/oauth.test.ts"],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes Google Meet entry edits to the plugin entry tests", () => {
|
||||
expect(resolveChangedTestTargetPlan(["extensions/google-meet/index.ts"])).toEqual({
|
||||
mode: "targets",
|
||||
targets: ["extensions/google-meet/index.test.ts"],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes changed utils and shared files to their light scoped lanes", () => {
|
||||
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
|
||||
"src/shared/string-normalization.ts",
|
||||
|
||||
Reference in New Issue
Block a user