mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
feat(google-meet): add export dry run manifests
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isLiveTestEnabled } from "../../src/agents/live-test-helpers.js";
|
||||
import { fetchGoogleMeetArtifacts, fetchLatestGoogleMeetConferenceRecord } from "./src/meet.js";
|
||||
import { buildGoogleMeetExportManifest, googleMeetExportFileNames } from "./src/cli.js";
|
||||
import {
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchLatestGoogleMeetConferenceRecord,
|
||||
} from "./src/meet.js";
|
||||
import { resolveGoogleMeetAccessToken } from "./src/oauth.js";
|
||||
|
||||
const LIVE_MEETING = process.env.OPENCLAW_GOOGLE_MEET_LIVE_MEETING?.trim() ?? "";
|
||||
@@ -52,5 +57,29 @@ describeLive("google-meet live", () => {
|
||||
});
|
||||
expect(artifacts.conferenceRecords.length).toBeLessThanOrEqual(1);
|
||||
expect(Array.isArray(artifacts.artifacts)).toBe(true);
|
||||
|
||||
const attendance = await fetchGoogleMeetAttendance({
|
||||
accessToken: token.accessToken,
|
||||
meeting: LIVE_MEETING,
|
||||
pageSize: 5,
|
||||
});
|
||||
expect(attendance.conferenceRecords.length).toBe(artifacts.conferenceRecords.length);
|
||||
|
||||
const manifest = buildGoogleMeetExportManifest({
|
||||
artifacts,
|
||||
attendance,
|
||||
files: googleMeetExportFileNames(),
|
||||
request: {
|
||||
meeting: LIVE_MEETING,
|
||||
pageSize: 5,
|
||||
includeTranscriptEntries: true,
|
||||
includeDocumentBodies: false,
|
||||
allConferenceRecords: false,
|
||||
mergeDuplicateParticipants: true,
|
||||
},
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
expect(manifest.files).toContain("manifest.json");
|
||||
expect(manifest.counts.conferenceRecords).toBe(artifacts.conferenceRecords.length);
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough, Writable } from "node:stream";
|
||||
@@ -938,6 +938,40 @@ describe("google-meet plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("dry-runs export bundles through the tool", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const parentDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-tool-dry-run-"));
|
||||
const outputDir = path.join(parentDir, "bundle");
|
||||
const { tools } = setup();
|
||||
const tool = tools[0] as {
|
||||
execute: (
|
||||
id: string,
|
||||
params: unknown,
|
||||
) => Promise<{ details: { dryRun?: boolean; manifest?: { files?: string[] } } }>;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await tool.execute("id", {
|
||||
action: "export",
|
||||
accessToken: "token",
|
||||
expiresAt: Date.now() + 120_000,
|
||||
conferenceRecord: "rec-1",
|
||||
outputDir,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
dryRun: true,
|
||||
manifest: {
|
||||
files: expect.arrayContaining(["summary.md", "manifest.json"]),
|
||||
},
|
||||
});
|
||||
expect(existsSync(outputDir)).toBe(false);
|
||||
} finally {
|
||||
rmSync(parentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reports the latest conference record through the tool", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const { tools } = setup();
|
||||
|
||||
@@ -211,6 +211,11 @@ const GoogleMeetToolSchema = Type.Object({
|
||||
),
|
||||
outputDir: Type.Optional(Type.String({ description: "For export, output directory" })),
|
||||
zip: Type.Optional(Type.Boolean({ description: "For export, also write a .zip archive" })),
|
||||
dryRun: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "For export, return the manifest without writing files.",
|
||||
}),
|
||||
),
|
||||
includeAllConferenceRecords: Type.Optional(
|
||||
Type.Boolean({
|
||||
description:
|
||||
@@ -403,7 +408,8 @@ async function exportGoogleMeetBundleFromParams(
|
||||
earlyBeforeMinutes: resolved.earlyBeforeMinutes,
|
||||
}),
|
||||
]);
|
||||
const { writeMeetExportBundle } = await import("./src/cli.js");
|
||||
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } =
|
||||
await import("./src/cli.js");
|
||||
const calendarId = normalizeOptionalString(raw.calendarId);
|
||||
const request = {
|
||||
...(resolved.meeting ? { meeting: resolved.meeting } : {}),
|
||||
@@ -427,6 +433,22 @@ async function exportGoogleMeetBundleFromParams(
|
||||
? { earlyBeforeMinutes: resolved.earlyBeforeMinutes }
|
||||
: {}),
|
||||
};
|
||||
const tokenSource = resolved.token.refreshed ? "refresh-token" : "cached-access-token";
|
||||
if (raw.dryRun === true) {
|
||||
return {
|
||||
dryRun: true,
|
||||
manifest: buildGoogleMeetExportManifest({
|
||||
artifacts,
|
||||
attendance,
|
||||
files: googleMeetExportFileNames(),
|
||||
request,
|
||||
tokenSource,
|
||||
...(resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}),
|
||||
}),
|
||||
...(resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}),
|
||||
tokenSource,
|
||||
};
|
||||
}
|
||||
const outputDir = normalizeOptionalString(raw.outputDir) ?? normalizeOptionalString(raw.output);
|
||||
const bundle = await writeMeetExportBundle({
|
||||
...(outputDir ? { outputDir } : {}),
|
||||
@@ -434,13 +456,13 @@ async function exportGoogleMeetBundleFromParams(
|
||||
attendance,
|
||||
zip: raw.zip === true,
|
||||
request,
|
||||
tokenSource: resolved.token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
tokenSource,
|
||||
...(resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}),
|
||||
});
|
||||
return {
|
||||
...bundle,
|
||||
...(resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}),
|
||||
tokenSource: resolved.token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
tokenSource,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
@@ -594,6 +594,54 @@ describe("google-meet CLI", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prints a dry-run export manifest without writing files", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const stdout = captureStdout();
|
||||
const parentDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-export-dry-run-"));
|
||||
const outputDir = path.join(parentDir, "bundle");
|
||||
|
||||
try {
|
||||
await setupCli({}).parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"export",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--include-doc-bodies",
|
||||
"--output",
|
||||
outputDir,
|
||||
"--dry-run",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
const payload = JSON.parse(stdout.output());
|
||||
expect(payload).toMatchObject({
|
||||
dryRun: true,
|
||||
manifest: {
|
||||
request: {
|
||||
conferenceRecord: "rec-1",
|
||||
includeDocumentBodies: true,
|
||||
},
|
||||
counts: {
|
||||
attendanceRows: 1,
|
||||
transcriptEntries: 1,
|
||||
warnings: 0,
|
||||
},
|
||||
files: expect.arrayContaining(["summary.md", "manifest.json"]),
|
||||
},
|
||||
tokenSource: "cached-access-token",
|
||||
});
|
||||
expect(existsSync(outputDir)).toBe(false);
|
||||
} finally {
|
||||
stdout.restore();
|
||||
rmSync(parentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prints human-readable session doctor output", async () => {
|
||||
const stdout = captureStdout();
|
||||
try {
|
||||
|
||||
@@ -71,6 +71,7 @@ type MeetArtifactOptions = ResolveSpaceOptions & {
|
||||
lateAfterMinutes?: string;
|
||||
earlyBeforeMinutes?: string;
|
||||
zip?: boolean;
|
||||
dryRun?: boolean;
|
||||
format?: "summary" | "markdown" | "csv";
|
||||
output?: string;
|
||||
};
|
||||
@@ -1051,7 +1052,7 @@ export function collectGoogleMeetArtifactWarnings(
|
||||
return warnings;
|
||||
}
|
||||
|
||||
function buildGoogleMeetExportManifest(params: {
|
||||
export function buildGoogleMeetExportManifest(params: {
|
||||
artifacts: GoogleMeetArtifactsResult;
|
||||
attendance: GoogleMeetAttendanceResult;
|
||||
files: string[];
|
||||
@@ -1105,6 +1106,17 @@ function buildGoogleMeetExportManifest(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function googleMeetExportFileNames(): string[] {
|
||||
return [
|
||||
"summary.md",
|
||||
"attendance.csv",
|
||||
"transcript.md",
|
||||
"artifacts.json",
|
||||
"attendance.json",
|
||||
"manifest.json",
|
||||
];
|
||||
}
|
||||
|
||||
function defaultExportDirectory(): string {
|
||||
return `google-meet-export-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
}
|
||||
@@ -1203,14 +1215,7 @@ export async function writeMeetExportBundle(params: {
|
||||
const outputDir = params.outputDir?.trim() || defaultExportDirectory();
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const zipFile = params.zip ? `${outputDir.replace(/\/$/, "")}.zip` : undefined;
|
||||
const fileNames = [
|
||||
"summary.md",
|
||||
"attendance.csv",
|
||||
"transcript.md",
|
||||
"artifacts.json",
|
||||
"attendance.json",
|
||||
"manifest.json",
|
||||
];
|
||||
const fileNames = googleMeetExportFileNames();
|
||||
const files = [
|
||||
{
|
||||
name: "summary.md",
|
||||
@@ -1813,6 +1818,7 @@ export function registerGoogleMeetCli(params: {
|
||||
.option("--early-before-minutes <n>", "Mark early leavers before this many minutes", "5")
|
||||
.option("--output <dir>", "Output directory")
|
||||
.option("--zip", "Also write a portable .zip archive")
|
||||
.option("--dry-run", "Fetch export data and print the manifest without writing files", false)
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: MeetArtifactOptions) => {
|
||||
const resolved = resolveArtifactTokenOptions(params.config, options);
|
||||
@@ -1868,6 +1874,22 @@ export function registerGoogleMeetCli(params: {
|
||||
? { earlyBeforeMinutes: resolved.earlyBeforeMinutes }
|
||||
: {}),
|
||||
};
|
||||
if (options.dryRun) {
|
||||
writeStdoutJson({
|
||||
dryRun: true,
|
||||
manifest: buildGoogleMeetExportManifest({
|
||||
artifacts,
|
||||
attendance,
|
||||
files: googleMeetExportFileNames(),
|
||||
request,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
...(meetingResult.calendarEvent ? { calendarEvent: meetingResult.calendarEvent } : {}),
|
||||
}),
|
||||
...(meetingResult.calendarEvent ? { calendarEvent: meetingResult.calendarEvent } : {}),
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const bundle = await writeMeetExportBundle({
|
||||
outputDir: options.output,
|
||||
artifacts,
|
||||
|
||||
Reference in New Issue
Block a user