






















@@ -9,6 +9,8 @@ import { resolveGoogleMeetConfig, resolveGoogleMeetConfigWithEnv } from "./src/c
99import {
1010buildGoogleMeetPreflightReport,
1111createGoogleMeetSpace,
12+fetchGoogleMeetArtifacts,
13+fetchGoogleMeetAttendance,
1214fetchGoogleMeetSpace,
1315normalizeGoogleMeetSpaceName,
1416} from "./src/meet.js";
@@ -64,6 +66,112 @@ function setup(
6466return setupGoogleMeetPlugin(plugin, config, options);
6567}
666869+function jsonResponse(value: unknown): Response {
70+return new Response(JSON.stringify(value), {
71+status: 200,
72+headers: { "Content-Type": "application/json" },
73+});
74+}
75+76+function requestUrl(input: RequestInfo | URL): URL {
77+if (typeof input === "string") {
78+return new URL(input);
79+}
80+if (input instanceof URL) {
81+return input;
82+}
83+return new URL(input.url);
84+}
85+86+function stubMeetArtifactsApi() {
87+const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
88+const url = requestUrl(input);
89+if (url.pathname === "/v2/spaces/abc-defg-hij") {
90+return jsonResponse({
91+name: "spaces/abc-defg-hij",
92+meetingCode: "abc-defg-hij",
93+meetingUri: "https://meet.google.com/abc-defg-hij",
94+});
95+}
96+if (url.pathname === "/v2/conferenceRecords") {
97+return jsonResponse({
98+conferenceRecords: [
99+{
100+name: "conferenceRecords/rec-1",
101+space: "spaces/abc-defg-hij",
102+startTime: "2026-04-25T10:00:00Z",
103+endTime: "2026-04-25T10:30:00Z",
104+},
105+],
106+});
107+}
108+if (url.pathname === "/v2/conferenceRecords/rec-1") {
109+return jsonResponse({
110+name: "conferenceRecords/rec-1",
111+space: "spaces/abc-defg-hij",
112+startTime: "2026-04-25T10:00:00Z",
113+endTime: "2026-04-25T10:30:00Z",
114+});
115+}
116+if (url.pathname === "/v2/conferenceRecords/rec-1/participants") {
117+return jsonResponse({
118+participants: [
119+{
120+name: "conferenceRecords/rec-1/participants/p1",
121+earliestStartTime: "2026-04-25T10:00:00Z",
122+latestEndTime: "2026-04-25T10:30:00Z",
123+signedinUser: { user: "users/alice", displayName: "Alice" },
124+},
125+],
126+});
127+}
128+if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions") {
129+return jsonResponse({
130+participantSessions: [
131+{
132+name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
133+startTime: "2026-04-25T10:00:00Z",
134+endTime: "2026-04-25T10:30:00Z",
135+},
136+],
137+});
138+}
139+if (url.pathname === "/v2/conferenceRecords/rec-1/recordings") {
140+return jsonResponse({
141+recordings: [
142+{
143+name: "conferenceRecords/rec-1/recordings/r1",
144+driveDestination: { file: "drive/file-1" },
145+},
146+],
147+});
148+}
149+if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts") {
150+return jsonResponse({
151+transcripts: [
152+{
153+name: "conferenceRecords/rec-1/transcripts/t1",
154+docsDestination: { document: "docs/doc-1" },
155+},
156+],
157+});
158+}
159+if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes") {
160+return jsonResponse({
161+smartNotes: [
162+{
163+name: "conferenceRecords/rec-1/smartNotes/sn1",
164+docsDestination: { document: "docs/doc-2" },
165+},
166+],
167+});
168+}
169+return new Response(`unexpected ${url.pathname}`, { status: 404 });
170+});
171+vi.stubGlobal("fetch", fetchMock);
172+return fetchMock;
173+}
174+67175type TestBridgeProcess = {
68176stdin?: { write(chunk: unknown): unknown } | null;
69177stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null;
@@ -218,6 +326,8 @@ describe("google-meet plugin", () => {
218326"setup_status",
219327"resolve_space",
220328"preflight",
329+"artifacts",
330+"attendance",
221331"recover_current_tab",
222332"leave",
223333"speak",
@@ -310,6 +420,82 @@ describe("google-meet plugin", () => {
310420);
311421});
312422423+it("lists Meet artifact metadata for conference records", async () => {
424+const fetchMock = stubMeetArtifactsApi();
425+426+await expect(
427+fetchGoogleMeetArtifacts({
428+accessToken: "token",
429+meeting: "abc-defg-hij",
430+pageSize: 2,
431+}),
432+).resolves.toMatchObject({
433+input: "abc-defg-hij",
434+space: { name: "spaces/abc-defg-hij" },
435+conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
436+artifacts: [
437+{
438+conferenceRecord: { name: "conferenceRecords/rec-1" },
439+participants: [{ name: "conferenceRecords/rec-1/participants/p1" }],
440+recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
441+transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
442+smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
443+},
444+],
445+});
446+447+const listCall = fetchMock.mock.calls.find(([input]) => {
448+const url = requestUrl(input);
449+return url.pathname === "/v2/conferenceRecords";
450+});
451+if (!listCall) {
452+throw new Error("Expected conferenceRecords.list fetch call");
453+}
454+const listUrl = requestUrl(listCall[0]);
455+expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"');
456+expect(listUrl.searchParams.get("pageSize")).toBe("2");
457+expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
458+expect.objectContaining({
459+url: "https://meet.googleapis.com/v2/conferenceRecords/rec-1/smartNotes?pageSize=2",
460+auditContext: "google-meet.conferenceRecords.smartNotes.list",
461+}),
462+);
463+});
464+465+it("lists Meet attendance rows with participant sessions", async () => {
466+const fetchMock = stubMeetArtifactsApi();
467+468+await expect(
469+fetchGoogleMeetAttendance({
470+accessToken: "token",
471+conferenceRecord: "rec-1",
472+pageSize: 3,
473+}),
474+).resolves.toMatchObject({
475+input: "rec-1",
476+conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
477+attendance: [
478+{
479+conferenceRecord: "conferenceRecords/rec-1",
480+participant: "conferenceRecords/rec-1/participants/p1",
481+displayName: "Alice",
482+user: "users/alice",
483+sessions: [
484+{
485+name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
486+},
487+],
488+},
489+],
490+});
491+expect(fetchMock).toHaveBeenCalledWith(
492+"https://meet.googleapis.com/v2/conferenceRecords/rec-1",
493+expect.objectContaining({
494+headers: expect.objectContaining({ Authorization: "Bearer token" }),
495+}),
496+);
497+});
498+313499it("surfaces Developer Preview acknowledgment blockers in preflight reports", () => {
314500expect(
315501buildGoogleMeetPreflightReport({
@@ -454,6 +640,27 @@ describe("google-meet plugin", () => {
454640expect(result.details.ok).toBe(true);
455641});
456642643+it("reports attendance through the tool", async () => {
644+stubMeetArtifactsApi();
645+const { tools } = setup();
646+const tool = tools[0] as {
647+execute: (
648+id: string,
649+params: unknown,
650+) => Promise<{ details: { attendance?: Array<{ displayName?: string }> } }>;
651+};
652+653+const result = await tool.execute("id", {
654+action: "attendance",
655+accessToken: "token",
656+expiresAt: Date.now() + 120_000,
657+conferenceRecord: "rec-1",
658+pageSize: 3,
659+});
660+661+expect(result.details.attendance).toEqual([expect.objectContaining({ displayName: "Alice" })]);
662+});
663+457664it("fails setup status when the configured Chrome node is not connected", async () => {
458665const { tools } = setup(
459666{
@@ -630,6 +837,81 @@ describe("google-meet plugin", () => {
630837}
631838});
632839840+it("CLI artifacts prints JSON output", async () => {
841+stubMeetArtifactsApi();
842+const program = new Command();
843+const stdout = captureStdout();
844+registerGoogleMeetCli({
845+ program,
846+config: resolveGoogleMeetConfig({}),
847+ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
848+});
849+850+try {
851+await program.parseAsync(
852+[
853+"googlemeet",
854+"artifacts",
855+"--access-token",
856+"token",
857+"--expires-at",
858+String(Date.now() + 120_000),
859+"--conference-record",
860+"rec-1",
861+"--json",
862+],
863+{ from: "user" },
864+);
865+expect(JSON.parse(stdout.output())).toMatchObject({
866+conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
867+artifacts: [
868+{
869+recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
870+transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
871+smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
872+},
873+],
874+tokenSource: "cached-access-token",
875+});
876+} finally {
877+stdout.restore();
878+}
879+});
880+881+it("CLI attendance prints participant sessions by default", async () => {
882+stubMeetArtifactsApi();
883+const program = new Command();
884+const stdout = captureStdout();
885+registerGoogleMeetCli({
886+ program,
887+config: resolveGoogleMeetConfig({}),
888+ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
889+});
890+891+try {
892+await program.parseAsync(
893+[
894+"googlemeet",
895+"attendance",
896+"--access-token",
897+"token",
898+"--expires-at",
899+String(Date.now() + 120_000),
900+"--conference-record",
901+"rec-1",
902+],
903+{ from: "user" },
904+);
905+expect(stdout.output()).toContain("attendance rows: 1");
906+expect(stdout.output()).toContain("participant: Alice");
907+expect(stdout.output()).toContain(
908+"conferenceRecords/rec-1/participants/p1/participantSessions/s1",
909+);
910+} finally {
911+stdout.restore();
912+}
913+});
914+633915it("CLI doctor prints human-readable session health", async () => {
634916const program = new Command();
635917const stdout = captureStdout();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。