diff --git a/package-lock.json b/package-lock.json index 72a1111..4d53747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "microsoft-rewards-bot", - "version": "2.59.1", + "version": "2.60.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "microsoft-rewards-bot", - "version": "2.59.1", + "version": "2.60.0", "license": "CC-BY-NC-SA-4.0", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index ce2c3d6..ee97a45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microsoft-rewards-bot", - "version": "2.59.1", + "version": "2.60.0", "description": "Automate Microsoft Rewards points collection", "private": true, "main": "index.js", @@ -26,7 +26,7 @@ "prestart": "node -e \"const fs = require('fs'); if (!fs.existsSync('dist/index.js')) { console.log('⚠️ Compiled files not found, building...'); require('child_process').execSync('npm run build', {stdio: 'inherit'}); }\"", "start": "node --enable-source-maps ./dist/index.js", "dev": "ts-node ./src/index.ts -dev", - "test": "node --test --loader ts-node/esm tests/**/*.test.ts", + "test": "node --test --require ts-node/register tests/**/*.test.ts", "creator": "ts-node ./src/account-creation/cli.ts", "dashboard": "node --enable-source-maps ./dist/index.js -dashboard", "dashboard:dev": "ts-node ./src/index.ts -dashboard", diff --git a/src/account-creation/HumanBehavior.ts b/src/account-creation/HumanBehavior.ts index e1bc170..ecf682e 100644 --- a/src/account-creation/HumanBehavior.ts +++ b/src/account-creation/HumanBehavior.ts @@ -105,6 +105,8 @@ export class HumanBehavior { */ async microGestures(context: string): Promise { try { + const gestureNotes: string[] = [] + // 60% chance of mouse movement (humans move mouse A LOT) if (Math.random() < 0.6) { const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px @@ -115,8 +117,7 @@ export class HumanBehavior { // Mouse move failed - page may be closed or unavailable }) - // VERBOSE logging disabled - too noisy - // log(false, 'CREATOR', `[${context}] 🖱️ Mouse moved to (${x}, ${y})`, 'log', 'gray') + gestureNotes.push(`mouse→(${x},${y})`) } // 30% chance of scroll (humans scroll to read content) @@ -129,8 +130,11 @@ export class HumanBehavior { // Scroll failed - page may be closed or unavailable }) - // VERBOSE logging disabled - too noisy - // log(false, 'CREATOR', `[${context}] 📜 Scrolled ${direction > 0 ? 'down' : 'up'} ${distance}px`, 'log', 'gray') + gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`) + } + + if (gestureNotes.length > 0) { + log(false, 'CREATOR', `[${context}] micro gestures: ${gestureNotes.join(', ')}`, 'log', 'gray') } } catch { // Gesture execution failed - not critical for operation diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index 0d0e674..df2dbf8 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -170,7 +170,7 @@ export default class BrowserFunc { }) this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG', - `DOM Diagnostic - ` + + 'DOM Diagnostic - ' + `URL: ${diagnosticInfo.url}, ` + `Title: "${diagnosticInfo.pageTitle}", ` + `Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` + diff --git a/src/flows/SummaryReporter.ts b/src/flows/SummaryReporter.ts index 9197411..81e419c 100644 --- a/src/flows/SummaryReporter.ts +++ b/src/flows/SummaryReporter.ts @@ -19,10 +19,10 @@ export interface AccountResult { email: string pointsEarned: number runDuration: number - initialPoints: number // Points avant l'exécution - finalPoints: number // Points après l'exécution - desktopPoints: number // Points gagnés sur Desktop - mobilePoints: number // Points gagnés sur Mobile + initialPoints: number // Points before execution + finalPoints: number // Points after execution + desktopPoints: number // Points earned on desktop + mobilePoints: number // Points earned on mobile errors?: string[] banned?: boolean } @@ -38,11 +38,13 @@ export interface SummaryData { export class SummaryReporter { private config: Config - private jobState: JobState + private jobState?: JobState constructor(config: Config) { this.config = config - this.jobState = new JobState(config) + if (config.jobState?.enabled !== false) { + this.jobState = new JobState(config) + } } /** @@ -85,7 +87,7 @@ export class SummaryReporter { description += ` | **🚫 Banned:** ${bannedCount}` } - description += `\n\n**📊 Account Details**\n` + description += '\n\n**📊 Account Details**\n' const accountsWithErrors: AccountResult[] = [] @@ -100,13 +102,13 @@ export class SummaryReporter { description += `• Duration: ${durationSec}s\n` // Collect accounts with errors for separate webhook - if ((account.errors?.length || account.banned) && account.email) { + if (this.hasAccountFailure(account)) { accountsWithErrors.push(account) } } // Footer summary - description += `\n**🌐 Total Balance**\n` + description += '\n**🌐 Total Balance**\n' description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})` const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00 @@ -145,7 +147,7 @@ export class SummaryReporter { // Error details if (account.banned) { - errorDescription += `• Status: Account Banned/Suspended\n` + errorDescription += '• Status: Account Banned/Suspended\n' if (account.errors?.length && account.errors[0]) { errorDescription += `• Reason: ${account.errors[0]}\n` } @@ -153,14 +155,14 @@ export class SummaryReporter { errorDescription += `• Error: ${account.errors[0]}\n` } - errorDescription += `\n` + errorDescription += '\n' } - errorDescription += `**📋 Recommended Actions:**\n` - errorDescription += `• Check account status manually\n` - errorDescription += `• Review error messages above\n` - errorDescription += `• Verify credentials if login failed\n` - errorDescription += `• Consider proxy rotation if rate-limited` + errorDescription += '**📋 Recommended Actions:**\n' + errorDescription += '• Check account status manually\n' + errorDescription += '• Review error messages above\n' + errorDescription += '• Verify credentials if login failed\n' + errorDescription += '• Consider proxy rotation if rate-limited' await ConclusionWebhook( this.config, @@ -195,6 +197,10 @@ export class SummaryReporter { * Update job state with completion status */ async updateJobState(summary: SummaryData): Promise { + if (!this.jobState) { + return + } + try { const day = summary.endTime.toISOString().split('T')?.[0] if (!day) return @@ -205,7 +211,7 @@ export class SummaryReporter { day, { totalCollected: account.pointsEarned, - banned: false, + banned: account.banned ?? false, errors: account.errors?.length ?? 0 } ) @@ -237,13 +243,15 @@ export class SummaryReporter { log('main', 'SUMMARY', '─'.repeat(80)) for (const account of summary.accounts) { - const status = account.errors?.length ? '❌ FAILED' : '✅ SUCCESS' + const status = this.hasAccountFailure(account) ? (account.banned ? '🚫 BANNED' : '❌ FAILED') : '✅ SUCCESS' const duration = Math.round(account.runDuration / 1000) log('main', 'SUMMARY', `${status} | ${account.email}`) log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`) - if (account.errors?.length) { + if (account.banned) { + log('main', 'SUMMARY', ' Status: Account flagged as banned/suspended', 'error') + } else if (account.errors?.length) { log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error') } } @@ -267,8 +275,8 @@ export class SummaryReporter { endTime: Date ): SummaryData { const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0) - const successCount = accounts.filter(acc => !acc.errors?.length).length - const failureCount = accounts.length - successCount + const failureCount = accounts.filter(acc => this.hasAccountFailure(acc)).length + const successCount = accounts.length - failureCount return { accounts, @@ -279,4 +287,8 @@ export class SummaryReporter { failureCount } } + + private hasAccountFailure(account: AccountResult): boolean { + return Boolean(account.errors?.length) || account.banned === true + } } diff --git a/src/index.ts b/src/index.ts index ac8d16d..e981ddd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -205,12 +205,12 @@ export class MicrosoftRewardsBot { // Only cluster when there's more than 1 cluster demanded if (this.config.clusters > 1) { if (cluster.isPrimary) { - this.runMaster() + await this.runMaster() } else if (cluster.worker) { await this.runWorker() } else { // Neither primary nor worker - something's wrong with clustering - log('main', 'MAIN', `ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.`, 'error') + log('main', 'MAIN', 'ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.', 'error') const passes = this.config.passesPerRun ?? 1 for (let pass = 1; pass <= passes; pass++) { if (passes > 1) { @@ -279,101 +279,108 @@ export class MicrosoftRewardsBot { return this.accountSummaries } - private runMaster() { - log('main', 'MAIN-PRIMARY', 'Primary process started') + private runMaster(): Promise { + return new Promise((resolve) => { + log('main', 'MAIN-PRIMARY', 'Primary process started') - const totalAccounts = this.accounts.length + const totalAccounts = this.accounts.length - // Validate accounts exist - if (totalAccounts === 0) { - log('main', 'MAIN-PRIMARY', 'No accounts found to process. Exiting.', 'warn') - process.exit(0) - } - - // If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers. - const workerCount = Math.min(this.config.clusters, totalAccounts) - const accountChunks = this.utils.chunkArray(this.accounts, workerCount) - // Reset activeWorkers to actual spawn count (constructor used raw clusters) - this.activeWorkers = workerCount - - // Store worker-to-chunk mapping for crash recovery - const workerChunkMap = new Map() - - for (let i = 0; i < workerCount; i++) { - const worker = cluster.fork() - const chunk = accountChunks[i] || [] - - // Validate chunk has accounts - if (chunk.length === 0) { - log('main', 'MAIN-PRIMARY', `Warning: Worker ${i} received empty account chunk`, 'warn') + // Validate accounts exist + if (totalAccounts === 0) { + log('main', 'MAIN-PRIMARY', 'No accounts found to process. Nothing to do.', 'warn') + resolve() + return } - // Store chunk mapping for crash recovery - if (worker.id) { - workerChunkMap.set(worker.id, chunk) + // If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers. + const workerCount = Math.min(this.config.clusters, totalAccounts) + const accountChunks = this.utils.chunkArray(this.accounts, workerCount) + // Reset activeWorkers to actual spawn count (constructor used raw clusters) + this.activeWorkers = workerCount + + // Store worker-to-chunk mapping for crash recovery + const workerChunkMap = new Map() + + let resolved = false + const finishRun = async () => { + if (resolved) return + resolved = true + try { + await this.sendConclusion(this.accountSummaries) + } catch (e) { + log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn') + } + log('main', 'MAIN-WORKER', 'All workers destroyed. Run complete.', 'warn') + resolve() } - // FIXED: Proper type checking before calling send - if (worker.send && typeof worker.send === 'function') { - worker.send({ chunk }) - } else { - log('main', 'MAIN-PRIMARY', `ERROR: Worker ${i} does not have a send function!`, 'error') + for (let i = 0; i < workerCount; i++) { + const worker = cluster.fork() + const chunk = accountChunks[i] || [] + + // Validate chunk has accounts + if (chunk.length === 0) { + log('main', 'MAIN-PRIMARY', `Warning: Worker ${i} received empty account chunk`, 'warn') + } + + // Store chunk mapping for crash recovery + if (worker.id) { + workerChunkMap.set(worker.id, chunk) + } + + // FIXED: Proper type checking before calling send + if (worker.send && typeof worker.send === 'function') { + worker.send({ chunk }) + } else { + log('main', 'MAIN-PRIMARY', `ERROR: Worker ${i} does not have a send function!`, 'error') + } + worker.on('message', (msg: unknown) => { + // IMPROVED: Using type-safe interface and type guard + if (isWorkerMessage(msg)) { + this.accountSummaries.push(...msg.data) + } + }) } - worker.on('message', (msg: unknown) => { - // IMPROVED: Using type-safe interface and type guard - if (isWorkerMessage(msg)) { - this.accountSummaries.push(...msg.data) + + cluster.on('exit', (worker: Worker, code: number) => { + this.activeWorkers -= 1 + + log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn') + + // Optional: restart crashed worker (basic heuristic) if crashRecovery allows + const cr = this.config.crashRecovery + if (cr?.restartFailedWorker && code !== 0 && worker.id) { + const attempts = (worker as { _restartAttempts?: number })._restartAttempts || 0 + if (attempts < (cr.restartFailedWorkerAttempts ?? 1)) { + (worker as { _restartAttempts?: number })._restartAttempts = attempts + 1 + log('main', 'CRASH-RECOVERY', `Respawning worker (attempt ${attempts + 1})`, 'warn') + + const originalChunk = workerChunkMap.get(worker.id) + const newW = cluster.fork() + + if (originalChunk && originalChunk.length > 0 && newW.id) { + (newW as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk: originalChunk }) + workerChunkMap.set(newW.id, originalChunk) + workerChunkMap.delete(worker.id) + log('main', 'CRASH-RECOVERY', `Assigned ${originalChunk.length} account(s) to respawned worker`) + } else { + log('main', 'CRASH-RECOVERY', 'Warning: Could not reassign accounts to respawned worker', 'warn') + } + + newW.on('message', (msg: unknown) => { + // IMPROVED: Using type-safe interface and type guard + if (isWorkerMessage(msg)) { + this.accountSummaries.push(...msg.data) + } + }) + } + } + + // Check if all workers have exited + if (this.activeWorkers === 0) { + void finishRun() } }) - } - - cluster.on('exit', (worker: Worker, code: number) => { - this.activeWorkers -= 1 - - log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn') - - // Optional: restart crashed worker (basic heuristic) if crashRecovery allows - const cr = this.config.crashRecovery - if (cr?.restartFailedWorker && code !== 0 && worker.id) { - const attempts = (worker as { _restartAttempts?: number })._restartAttempts || 0 - if (attempts < (cr.restartFailedWorkerAttempts ?? 1)) { - (worker as { _restartAttempts?: number })._restartAttempts = attempts + 1 - log('main', 'CRASH-RECOVERY', `Respawning worker (attempt ${attempts + 1})`, 'warn') - - const originalChunk = workerChunkMap.get(worker.id) - const newW = cluster.fork() - - if (originalChunk && originalChunk.length > 0 && newW.id) { - (newW as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk: originalChunk }) - workerChunkMap.set(newW.id, originalChunk) - workerChunkMap.delete(worker.id) - log('main', 'CRASH-RECOVERY', `Assigned ${originalChunk.length} account(s) to respawned worker`) - } else { - log('main', 'CRASH-RECOVERY', 'Warning: Could not reassign accounts to respawned worker', 'warn') - } - - newW.on('message', (msg: unknown) => { - // IMPROVED: Using type-safe interface and type guard - if (isWorkerMessage(msg)) { - this.accountSummaries.push(...msg.data) - } - }) - } - } - - // Check if all workers have exited - if (this.activeWorkers === 0) { - // All workers done -> send conclusion and exit (update check moved to startup) - (async () => { - try { - await this.sendConclusion(this.accountSummaries) - } catch (e) { - log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn') - process.exit(0) - })() - } }) } @@ -382,11 +389,21 @@ export class MicrosoftRewardsBot { // Wait for chunk (either already received during init, or will arrive soon) const chunk = await new Promise((resolve) => { - if ((global as any).__workerChunk) { - resolve((global as any).__workerChunk) - } else { - (process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk: c }: { chunk: Account[] }) => resolve(c)) + if (global.__workerChunk) { + const bufferedChunk = global.__workerChunk + global.__workerChunk = undefined + resolve(bufferedChunk) + return } + + const handleMessage = (message: unknown): void => { + if (isWorkerChunkMessage(message)) { + process.off('message', handleMessage) + resolve(message.chunk) + } + } + + process.on('message', handleMessage) }) if (!chunk || chunk.length === 0) { @@ -896,6 +913,20 @@ function isWorkerMessage(msg: unknown): msg is WorkerMessage { return m.type === 'summary' && Array.isArray(m.data) } +interface WorkerChunkMessage { + chunk: Account[] +} + +function isWorkerChunkMessage(message: unknown): message is WorkerChunkMessage { + if (!message || typeof message !== 'object') return false + return Array.isArray((message as WorkerChunkMessage).chunk) +} + +declare global { + // eslint-disable-next-line no-var + var __workerChunk: Account[] | undefined +} + // Use utility functions from Utils.ts const shortErr = shortErrorMessage const formatFullError = formatDetailedError @@ -905,9 +936,14 @@ async function main(): Promise { // Workers initialize for ~2 seconds before reaching runWorker(), so messages // sent by primary during initialization would be lost without this early listener if (!cluster.isPrimary && cluster.worker) { - (process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk }: { chunk: Account[] }) => { - (global as any).__workerChunk = chunk - }) + const bufferChunk = (message: unknown): void => { + if (isWorkerChunkMessage(message)) { + global.__workerChunk = message.chunk + process.off('message', bufferChunk) + } + } + + process.on('message', bufferChunk) } // Check for dashboard mode flag (standalone dashboard) diff --git a/src/util/notifications/ErrorReportingWebhook.ts b/src/util/notifications/ErrorReportingWebhook.ts index efca280..dc158f1 100644 --- a/src/util/notifications/ErrorReportingWebhook.ts +++ b/src/util/notifications/ErrorReportingWebhook.ts @@ -25,9 +25,16 @@ export function obfuscateWebhookUrl(url: string): string { return Buffer.from(url).toString('base64') } +const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ + export function deobfuscateWebhookUrl(encoded: string): string { + const trimmed = encoded.trim() + if (!trimmed || !BASE64_REGEX.test(trimmed)) { + return '' + } + try { - return Buffer.from(encoded, 'base64').toString('utf-8') + return Buffer.from(trimmed, 'base64').toString('utf-8') } catch { return '' } diff --git a/tests/flows/mobileFlow.test.ts b/tests/flows/mobileFlow.test.ts index 28d7636..5df16c7 100644 --- a/tests/flows/mobileFlow.test.ts +++ b/tests/flows/mobileFlow.test.ts @@ -14,12 +14,12 @@ test('MobileFlow module exports correctly', async () => { test('MobileFlow has run method', async () => { const { MobileFlow } = await import('../../src/flows/MobileFlow') - + // Mock bot instance const mockBot = { - log: () => {}, + log: () => { }, isMobile: true, - config: { + config: { workers: {}, runOnZeroPoints: false, searchSettings: { retryMobileSearchAmount: 0 } @@ -29,7 +29,7 @@ test('MobileFlow has run method', async () => { activities: {}, compromisedModeActive: false } - + const flow = new MobileFlow(mockBot as never) assert.ok(flow, 'MobileFlow instance should be created') assert.equal(typeof flow.run, 'function', 'MobileFlow should have run() method') @@ -37,27 +37,27 @@ test('MobileFlow has run method', async () => { test('MobileFlowResult interface has correct structure', async () => { const { MobileFlow } = await import('../../src/flows/MobileFlow') - + // Validate that MobileFlowResult type exports (compile-time check) type MobileFlowResult = Awaited['run']>> - + const mockResult: MobileFlowResult = { initialPoints: 1000, collectedPoints: 30 } - + assert.equal(typeof mockResult.initialPoints, 'number', 'initialPoints should be a number') assert.equal(typeof mockResult.collectedPoints, 'number', 'collectedPoints should be a number') }) test('MobileFlow accepts retry tracker', async () => { const { MobileFlow } = await import('../../src/flows/MobileFlow') - const { MobileRetryTracker } = await import('../../src/util/MobileRetryTracker') - + const { MobileRetryTracker } = await import('../../src/util/state/MobileRetryTracker') + const mockBot = { - log: () => {}, + log: () => { }, isMobile: true, - config: { + config: { workers: {}, runOnZeroPoints: false, searchSettings: { retryMobileSearchAmount: 3 } @@ -67,10 +67,10 @@ test('MobileFlow accepts retry tracker', async () => { activities: {}, compromisedModeActive: false } - + const flow = new MobileFlow(mockBot as never) const tracker = new MobileRetryTracker(3) - + assert.ok(flow, 'MobileFlow should accept retry tracker') assert.equal(typeof tracker.registerFailure, 'function', 'MobileRetryTracker should have registerFailure method') }) diff --git a/tests/flows/summaryReporter.test.ts b/tests/flows/summaryReporter.test.ts index 00f0d90..da65c02 100644 --- a/tests/flows/summaryReporter.test.ts +++ b/tests/flows/summaryReporter.test.ts @@ -14,40 +14,57 @@ test('SummaryReporter module exports correctly', async () => { test('SummaryReporter creates instance with config', async () => { const { SummaryReporter } = await import('../../src/flows/SummaryReporter') - + const mockConfig = { webhook: { enabled: false }, ntfy: { enabled: false }, sessionPath: './sessions', jobState: { enabled: false } } - + const reporter = new SummaryReporter(mockConfig as never) assert.ok(reporter, 'SummaryReporter instance should be created') }) test('SummaryReporter creates summary correctly', async () => { const { SummaryReporter } = await import('../../src/flows/SummaryReporter') - + const mockConfig = { webhook: { enabled: false }, ntfy: { enabled: false }, sessionPath: './sessions', jobState: { enabled: false } } - + const reporter = new SummaryReporter(mockConfig as never) - + const accounts = [ - { email: 'test@example.com', pointsEarned: 100, runDuration: 60000 }, - { email: 'test2@example.com', pointsEarned: 150, runDuration: 70000, errors: ['test error'] } + { + email: 'test@example.com', + pointsEarned: 100, + runDuration: 60000, + initialPoints: 1000, + finalPoints: 1100, + desktopPoints: 60, + mobilePoints: 40 + }, + { + email: 'test2@example.com', + pointsEarned: 150, + runDuration: 70000, + initialPoints: 2000, + finalPoints: 2150, + desktopPoints: 90, + mobilePoints: 60, + errors: ['test error'] + } ] - + const startTime = new Date('2025-01-01T10:00:00Z') const endTime = new Date('2025-01-01T10:05:00Z') - + const summary = reporter.createSummary(accounts, startTime, endTime) - + assert.equal(summary.totalPoints, 250, 'Total points should be 250') assert.equal(summary.successCount, 1, 'Success count should be 1') assert.equal(summary.failureCount, 1, 'Failure count should be 1') @@ -56,22 +73,30 @@ test('SummaryReporter creates summary correctly', async () => { test('SummaryData structure is correct', async () => { const { SummaryReporter } = await import('../../src/flows/SummaryReporter') - + const mockConfig = { webhook: { enabled: false }, ntfy: { enabled: false }, sessionPath: './sessions', jobState: { enabled: false } } - + const reporter = new SummaryReporter(mockConfig as never) - + const summary = reporter.createSummary( - [{ email: 'test@example.com', pointsEarned: 50, runDuration: 30000 }], + [{ + email: 'test@example.com', + pointsEarned: 50, + runDuration: 30000, + initialPoints: 500, + finalPoints: 550, + desktopPoints: 30, + mobilePoints: 20 + }], new Date(), new Date() ) - + assert.ok(summary.startTime instanceof Date, 'startTime should be a Date') assert.ok(summary.endTime instanceof Date, 'endTime should be a Date') assert.equal(typeof summary.totalPoints, 'number', 'totalPoints should be a number') diff --git a/tests/loginStateDetector.test.ts b/tests/loginStateDetector.test.ts index 022094e..18eaca9 100644 --- a/tests/loginStateDetector.test.ts +++ b/tests/loginStateDetector.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' +import { LoginState, LoginStateDetector } from '../src/util/validation/LoginStateDetector' /** * Tests for LoginStateDetector - login flow state machine @@ -10,6 +10,8 @@ import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' // Type helper for mock Page objects in tests type MockPage = Parameters[0] +const asMockPage = (page: T): MockPage => page as unknown as MockPage + test('LoginState enum contains expected states', () => { assert.ok(LoginState.EmailPage, 'Should have EmailPage state') assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state') @@ -60,7 +62,7 @@ test('detectState identifies LoggedIn state on rewards domain', async () => { evaluate: () => Promise.resolve(200) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -88,7 +90,7 @@ test('detectState identifies EmailPage state on login.live.com', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -115,7 +117,7 @@ test('detectState identifies PasswordPage state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -142,7 +144,7 @@ test('detectState identifies TwoFactorRequired state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -170,7 +172,7 @@ test('detectState identifies PasskeyPrompt state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -198,7 +200,7 @@ test('detectState identifies Blocked state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -216,7 +218,7 @@ test('detectState returns Unknown for ambiguous pages', async () => { evaluate: () => Promise.resolve(50) } - const detection = await LoginStateDetector.detectState(mockPage as MockPage) + const detection = await LoginStateDetector.detectState(asMockPage(mockPage)) assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages') assert.equal(detection.confidence, 'low', 'Should have low confidence') @@ -234,7 +236,7 @@ test('detectState handles errors gracefully', async () => { } try { - await LoginStateDetector.detectState(mockPage as MockPage) + await LoginStateDetector.detectState(asMockPage(mockPage)) assert.fail('Should throw error') } catch (e) { assert.ok(e instanceof Error, 'Should throw Error instance') diff --git a/tests/mobileRetryTracker.test.ts b/tests/mobileRetryTracker.test.ts index 5611c1b..2954663 100644 --- a/tests/mobileRetryTracker.test.ts +++ b/tests/mobileRetryTracker.test.ts @@ -1,7 +1,7 @@ -import test from 'node:test' import assert from 'node:assert/strict' +import test from 'node:test' -import { MobileRetryTracker } from '../src/util/MobileRetryTracker' +import { MobileRetryTracker } from '../src/util/state/MobileRetryTracker' test('MobileRetryTracker stops retries after configured limit', () => { const tracker = new MobileRetryTracker(2) diff --git a/tests/queryDiversityEngine.test.ts b/tests/queryDiversityEngine.test.ts index 37b3ada..50e7753 100644 --- a/tests/queryDiversityEngine.test.ts +++ b/tests/queryDiversityEngine.test.ts @@ -1,7 +1,7 @@ -import test from 'node:test' import assert from 'node:assert/strict' +import test from 'node:test' -import { QueryDiversityEngine } from '../src/util/QueryDiversityEngine' +import { QueryDiversityEngine } from '../src/util/network/QueryDiversityEngine' test('QueryDiversityEngine fetches and limits queries', async () => { const engine = new QueryDiversityEngine({ @@ -13,7 +13,7 @@ test('QueryDiversityEngine fetches and limits queries', async () => { assert.ok(queries.length > 0, 'Should return at least one query') assert.ok(queries.length <= 10, 'Should respect count limit') - assert.ok(queries.every(q => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings') + assert.ok(queries.every((q: string) => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings') }) test('QueryDiversityEngine deduplicates queries', async () => { diff --git a/tests/smartWait.test.ts b/tests/smartWait.test.ts index a5dd572..990a387 100644 --- a/tests/smartWait.test.ts +++ b/tests/smartWait.test.ts @@ -3,18 +3,8 @@ * Tests intelligent page readiness and element detection */ -import assert from 'node:assert' -import { describe, it } from 'node:test' - -// Mock Playwright types for testing -type MockPage = { - url: () => string - content: () => Promise - waitForLoadState: (state: string, options?: { timeout: number }) => Promise - waitForTimeout: (ms: number) => Promise - locator: (selector: string) => MockLocator - evaluate: (fn: () => T) => Promise -} +import assert from 'node:assert'; +import { describe, it } from 'node:test'; type MockLocator = { waitFor: (options: { state: string; timeout: number }) => Promise @@ -94,7 +84,7 @@ describe('SmartWait', () => { mockLogFn('✓ Element found quickly (567ms)') assert.strictEqual(logs.length, 2, 'Should capture log messages') - assert.ok(logs[0].includes('1234ms'), 'Should include timing data') + assert.ok(logs[0]?.includes('1234ms'), 'Should include timing data') }) it('should extract performance metrics from logs', () => { @@ -102,8 +92,8 @@ describe('SmartWait', () => { const timeMatch = logMessage.match(/(\d+)ms/) assert.ok(timeMatch, 'Should include parseable timing') - if (timeMatch) { - const time = parseInt(timeMatch[1]) + if (timeMatch && timeMatch[1]) { + const time = parseInt(timeMatch[1], 10) assert.ok(time > 0, 'Should extract valid timing') } })