feat: add debugMode flag to gate screenshots, filter bot own captions

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-18 23:52:50 +01:00
parent 003e21efcd
commit 9a2994672c
4 changed files with 21 additions and 16 deletions

View file

@ -33,6 +33,7 @@ export interface OrchestratorOptions {
botAccountEmail?: string; botAccountEmail?: string;
botAccountPassword?: string; botAccountPassword?: string;
transferMode?: string; transferMode?: string;
debugMode?: boolean;
} }
/** /**
@ -69,6 +70,7 @@ export class BotOrchestrator {
private _state: BotState = 'idle'; private _state: BotState = 'idle';
private _isShuttingDown: boolean = false; private _isShuttingDown: boolean = false;
private _isDebugMode: boolean = false;
private _keepAliveInterval: NodeJS.Timeout | null = null; private _keepAliveInterval: NodeJS.Timeout | null = null;
constructor( constructor(
@ -83,6 +85,7 @@ export class BotOrchestrator {
this._botName = botName || config.botName; this._botName = botName || config.botName;
this._callbacks = callbacks; this._callbacks = callbacks;
this._options = options; this._options = options;
this._isDebugMode = !!options.debugMode;
this._logger = createSessionLogger(sessionId); this._logger = createSessionLogger(sessionId);
} }
@ -233,7 +236,7 @@ export class BotOrchestrator {
); );
if (!emailInput) { if (!emailInput) {
this._logger.warn(`No login page found, current URL: ${this._page!.url().substring(0, 150)}`); this._logger.warn(`No login page found, current URL: ${this._page!.url().substring(0, 150)}`);
await this._takeScreenshot('step1-no-login-page', true); await this._takeScreenshot('step1-no-login-page', this._isDebugMode);
} }
// STEP 2: Microsoft Authentication // STEP 2: Microsoft Authentication
@ -246,11 +249,11 @@ export class BotOrchestrator {
); );
if (!authSuccess) { if (!authSuccess) {
await this._takeScreenshot('step2-auth-failed', true); await this._takeScreenshot('step2-auth-failed', this._isDebugMode);
throw new Error('Microsoft authentication failed'); throw new Error('Microsoft authentication failed');
} }
this._logger.info('STEP 2: authentication successful'); this._logger.info('STEP 2: authentication successful');
await this._takeScreenshot('step2-auth-done', true); await this._takeScreenshot('step2-auth-done', this._isDebugMode);
// STEP 3: Wait for Teams to load after auth // STEP 3: Wait for Teams to load after auth
this._logger.info('STEP 3: waiting for Teams to load after auth...'); this._logger.info('STEP 3: waiting for Teams to load after auth...');
@ -262,7 +265,7 @@ export class BotOrchestrator {
} catch { } catch {
this._logger.warn(`Unexpected URL after auth: ${this._page!.url().substring(0, 150)}`); this._logger.warn(`Unexpected URL after auth: ${this._page!.url().substring(0, 150)}`);
} }
await this._takeScreenshot('step3-teams-loaded', true); await this._takeScreenshot('step3-teams-loaded', this._isDebugMode);
// STEP 4: Navigate to the meeting URL with proper launch params. // STEP 4: Navigate to the meeting URL with proper launch params.
// CRITICAL: The suppress params (msLaunch, suppressPrompt, directDl) must // CRITICAL: The suppress params (msLaunch, suppressPrompt, directDl) must
@ -291,7 +294,7 @@ export class BotOrchestrator {
timeout: 30000, timeout: 30000,
}); });
this._logger.info(`STEP 4: URL after navigation: ${this._page!.url().substring(0, 150)}`); this._logger.info(`STEP 4: URL after navigation: ${this._page!.url().substring(0, 150)}`);
await this._takeScreenshot('step4-meeting-url-loaded', true); await this._takeScreenshot('step4-meeting-url-loaded', this._isDebugMode);
// STEP 4a: Poll for first actionable button (interstitial OR pre-join) // STEP 4a: Poll for first actionable button (interstitial OR pre-join)
const interstitialSelectors = [ const interstitialSelectors = [
@ -327,32 +330,32 @@ export class BotOrchestrator {
if (!isPreJoin) { if (!isPreJoin) {
await firstBtn.click(); await firstBtn.click();
this._logger.info(`STEP 4a: clicked interstitial: "${btnText}" (data-tid="${btnTid}")`); this._logger.info(`STEP 4a: clicked interstitial: "${btnText}" (data-tid="${btnTid}")`);
await this._takeScreenshot('step4a-after-interstitial', true); await this._takeScreenshot('step4a-after-interstitial', this._isDebugMode);
} else { } else {
this._logger.info(`STEP 4a: pre-join button already visible: "${btnText}"`); this._logger.info(`STEP 4a: pre-join button already visible: "${btnText}"`);
} }
} else { } else {
await this._takeScreenshot('step4a-no-buttons-found', true); await this._takeScreenshot('step4a-no-buttons-found', this._isDebugMode);
} }
// STEP 5: Poll for "Join now" on the pre-join screen (mic is NOT touched) // STEP 5: Poll for "Join now" on the pre-join screen (mic is NOT touched)
await this._takeScreenshot('step5-before-join-now', true); await this._takeScreenshot('step5-before-join-now', this._isDebugMode);
const joinNowBtn = await this._pollForElement(preJoinSelectors, 30000, 'Join now button'); const joinNowBtn = await this._pollForElement(preJoinSelectors, 30000, 'Join now button');
if (!joinNowBtn) { if (!joinNowBtn) {
await this._takeScreenshot('step5-no-join-now', true); await this._takeScreenshot('step5-no-join-now', this._isDebugMode);
throw new Error('"Join now" button not found on pre-join screen'); throw new Error('"Join now" button not found on pre-join screen');
} }
await joinNowBtn.click(); await joinNowBtn.click();
this._logger.info('STEP 5: clicked "Join now", waiting for meeting'); this._logger.info('STEP 5: clicked "Join now", waiting for meeting');
await this._takeScreenshot('step5-join-now-clicked', true); await this._takeScreenshot('step5-join-now-clicked', this._isDebugMode);
// STEP 6: Wait for meeting admission (hangup button = in meeting) // STEP 6: Wait for meeting admission (hangup button = in meeting)
await this._waitForMeetingAdmission(); await this._waitForMeetingAdmission();
this._setState('in_meeting'); this._setState('in_meeting');
this._logger.info(`STEP 6: bot joined the meeting (authenticated as ${this._options.botAccountEmail})`); this._logger.info(`STEP 6: bot joined the meeting (authenticated as ${this._options.botAccountEmail})`);
await this._takeScreenshot('step6-in-meeting', true); await this._takeScreenshot('step6-in-meeting', this._isDebugMode);
this._startKeepAlive(); this._startKeepAlive();
await this._audioProcedure!.initialize(); await this._audioProcedure!.initialize();

View file

@ -19,8 +19,8 @@ async function main(): Promise<void> {
// Start HTTP server // Start HTTP server
httpServer = new HttpServer({ httpServer = new HttpServer({
onJoinRequest: async (sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode) => { onJoinRequest: async (sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode) => {
await sessionManager.createSession(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode); await sessionManager.createSession(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode);
}, },
onLeaveRequest: async (sessionId) => { onLeaveRequest: async (sessionId) => {
await sessionManager.endSession(sessionId); await sessionManager.endSession(sessionId);

View file

@ -7,7 +7,7 @@ import { config } from '../config';
import { runAuthTests, runSingleVariant, getVariantIds } from '../bot/authTestProcedure'; import { runAuthTests, runSingleVariant, getVariantIds } from '../bot/authTestProcedure';
export interface HttpServerCallbacks { export interface HttpServerCallbacks {
onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string, gatewayWsUrl?: string, language?: string, botAccountEmail?: string, botAccountPassword?: string, transferMode?: string) => Promise<void>; onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string, gatewayWsUrl?: string, language?: string, botAccountEmail?: string, botAccountPassword?: string, transferMode?: string, debugMode?: boolean) => Promise<void>;
onLeaveRequest: (sessionId: string) => Promise<void>; onLeaveRequest: (sessionId: string) => Promise<void>;
onStatusRequest: (sessionId: string) => { state: string; error?: string } | null; onStatusRequest: (sessionId: string) => { state: string; error?: string } | null;
} }
@ -80,14 +80,14 @@ export class HttpServer {
// Deploy a new bot // Deploy a new bot
this._app.post('/api/bot', async (req: Request, res: Response) => { this._app.post('/api/bot', async (req: Request, res: Response) => {
try { try {
const { sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode } = req.body; const { sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode } = req.body;
if (!sessionId || !meetingUrl) { if (!sessionId || !meetingUrl) {
res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' }); res.status(400).json({ error: 'Missing required fields: sessionId, meetingUrl' });
return; return;
} }
await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode); await this._callbacks.onJoinRequest(sessionId, meetingUrl, botName, instanceId, gatewayWsUrl, language, botAccountEmail, botAccountPassword, transferMode, debugMode);
res.json({ res.json({
success: true, success: true,

View file

@ -42,6 +42,7 @@ export class SessionManager {
botAccountEmail?: string, botAccountEmail?: string,
botAccountPassword?: string, botAccountPassword?: string,
transferMode?: string, transferMode?: string,
debugMode?: boolean,
): Promise<void> { ): Promise<void> {
if (this._sessions.has(sessionId)) { if (this._sessions.has(sessionId)) {
logger.warn(`Session ${sessionId} already exists`); logger.warn(`Session ${sessionId} already exists`);
@ -76,6 +77,7 @@ export class SessionManager {
botAccountEmail: botAccountEmail, botAccountEmail: botAccountEmail,
botAccountPassword: botAccountPassword, botAccountPassword: botAccountPassword,
transferMode: transferMode, transferMode: transferMode,
debugMode: debugMode,
}; };
const orchestrator = new BotOrchestrator( const orchestrator = new BotOrchestrator(