diff --git a/README.md b/README.md index a672b76..38c1f81 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,12 @@ All while maintaining **natural behavior patterns** to minimize detection risk. --- +## ✅ Tests + +- `npm run test`: runs the node:test suite with ts-node to validate critical utilities. + +--- + ## 🆘 Getting Help - 💬 **[Join our Discord](https://discord.gg/h6Z69ZPPCz)** — Community support and updates diff --git a/package.json b/package.json index b6344fc..2ef9cae 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "pre-build": "npm i && npm run clean && node -e \"process.exit(process.env.SKIP_PLAYWRIGHT_INSTALL?0:1)\" || npx playwright install chromium", "typecheck": "tsc --noEmit", "build": "tsc", + "test": "node --test --loader ts-node/esm tests", "start": "node --enable-source-maps ./dist/index.js", "ts-start": "node --loader ts-node/esm ./src/index.ts", "dev": "ts-node ./src/index.ts -dev", @@ -27,7 +28,7 @@ "start:schedule": "node --enable-source-maps ./dist/scheduler.js", "lint": "eslint \"src/**/*.{ts,tsx}\"", "prepare": "npm run build", - "setup": "node ./setup/update/setup.mjs", + "setup": "node ./setup/update/setup.mjs", "kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"", "create-docker": "docker build -t microsoft-rewards-bot ." }, diff --git a/src/index.ts b/src/index.ts index 33a34b7..a15bed1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { Analytics } from './util/Analytics' import { QueryDiversityEngine } from './util/QueryDiversityEngine' import JobState from './util/JobState' import { StartupValidator } from './util/StartupValidator' +import { MobileRetryTracker } from './util/MobileRetryTracker' // Main bot class @@ -58,7 +59,6 @@ export class MicrosoftRewardsBot { private pointsInitial: number = 0 private activeWorkers: number - private mobileRetryAttempts: number private browserFactory: Browser = new Browser(this) private accounts: Account[] private workers: Workers @@ -103,7 +103,6 @@ export class MicrosoftRewardsBot { this.workers = new Workers(this) this.humanizer = new Humanizer(this.utils, this.config.humanization) this.activeWorkers = this.config.clusters - this.mobileRetryAttempts = 0 if (this.config.queryDiversity?.enabled) { this.queryEngine = new QueryDiversityEngine({ @@ -1070,7 +1069,10 @@ export class MicrosoftRewardsBot { } } - async Mobile(account: Account): Promise<{ initialPoints: number; collectedPoints: number }> { + async Mobile( + account: Account, + retryTracker = new MobileRetryTracker(this.config.searchSettings.retryMobileSearchAmount) + ): Promise<{ initialPoints: number; collectedPoints: number }> { log(true,'FLOW','Mobile() invoked') const browser = await this.browserFactory.createBrowser(account.proxy, account.email) this.homePage = await browser.newPage() @@ -1139,6 +1141,9 @@ export class MicrosoftRewardsBot { } // Do mobile searches + const configuredRetries = Number(this.config.searchSettings.retryMobileSearchAmount ?? 0) + const maxMobileRetries = Number.isFinite(configuredRetries) ? configuredRetries : 0 + if (this.config.workers.doMobileSearch) { // If no mobile searches data found, stop (Does not always exist on new accounts) if (data.userStatus.counters.mobileSearch) { @@ -1154,21 +1159,21 @@ export class MicrosoftRewardsBot { const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] if (mobileSearchPoints && (mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0) { - // Increment retry count - this.mobileRetryAttempts++ - } + const shouldRetry = retryTracker.registerFailure() - // Exit if retries are exhausted - if (this.mobileRetryAttempts > this.config.searchSettings.retryMobileSearchAmount) { - log(this.isMobile, 'MAIN', `Max retry limit of ${this.config.searchSettings.retryMobileSearchAmount} reached. Exiting retry loop`, 'warn') - } else if (this.mobileRetryAttempts !== 0) { - log(this.isMobile, 'MAIN', `Attempt ${this.mobileRetryAttempts}/${this.config.searchSettings.retryMobileSearchAmount}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow') + if (!shouldRetry) { + const exhaustedAttempts = retryTracker.getAttemptCount() + log(this.isMobile, 'MAIN', `Max retry limit of ${maxMobileRetries} reached after ${exhaustedAttempts} attempt(s). Exiting retry loop`, 'warn') + } else { + const attempt = retryTracker.getAttemptCount() + log(this.isMobile, 'MAIN', `Attempt ${attempt}/${maxMobileRetries}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow') - // Close mobile browser - await this.browser.func.closeBrowser(browser, account.email) + // Close mobile browser before retrying to release resources + await this.browser.func.closeBrowser(browser, account.email) - // Create a new browser and try - return await this.Mobile(account) + // Create a new browser and try again with the same tracker + return await this.Mobile(account, retryTracker) + } } } else { log(this.isMobile, 'MAIN', 'Unable to fetch search points, your account is most likely too "new" for this! Try again later!', 'warn') diff --git a/src/util/MobileRetryTracker.ts b/src/util/MobileRetryTracker.ts new file mode 100644 index 0000000..a82d0fb --- /dev/null +++ b/src/util/MobileRetryTracker.ts @@ -0,0 +1,26 @@ +export class MobileRetryTracker { + private attempts = 0 + private readonly maxRetries: number + + constructor(maxRetries: number) { + const normalized = Number.isFinite(maxRetries) ? Math.floor(maxRetries) : 0 + this.maxRetries = Math.max(0, normalized) + } + + /** + * Register an incomplete mobile search attempt. + * @returns true when another retry should be attempted, false when the retry budget is exhausted. + */ + registerFailure(): boolean { + this.attempts += 1 + return this.attempts <= this.maxRetries + } + + hasExceeded(): boolean { + return this.attempts > this.maxRetries + } + + getAttemptCount(): number { + return this.attempts + } +} diff --git a/tests/mobileRetryTracker.test.js b/tests/mobileRetryTracker.test.js new file mode 100644 index 0000000..934044e --- /dev/null +++ b/tests/mobileRetryTracker.test.js @@ -0,0 +1,28 @@ +const test = require('node:test') +const assert = require('node:assert/strict') + +const { MobileRetryTracker } = require('../dist/util/MobileRetryTracker.js') + +test('MobileRetryTracker stops retries after configured limit', () => { + const tracker = new MobileRetryTracker(2) + + assert.equal(tracker.registerFailure(), true) + assert.equal(tracker.hasExceeded(), false) + assert.equal(tracker.getAttemptCount(), 1) + + assert.equal(tracker.registerFailure(), true) + assert.equal(tracker.hasExceeded(), false) + assert.equal(tracker.getAttemptCount(), 2) + + assert.equal(tracker.registerFailure(), false) + assert.equal(tracker.hasExceeded(), true) + assert.equal(tracker.getAttemptCount(), 3) +}) + +test('MobileRetryTracker normalizes invalid configuration', () => { + const tracker = new MobileRetryTracker(-3) + + assert.equal(tracker.registerFailure(), false) + assert.equal(tracker.hasExceeded(), true) + assert.equal(tracker.getAttemptCount(), 1) +}) diff --git a/tests/mobileRetryTracker.test.ts b/tests/mobileRetryTracker.test.ts new file mode 100644 index 0000000..5611c1b --- /dev/null +++ b/tests/mobileRetryTracker.test.ts @@ -0,0 +1,28 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { MobileRetryTracker } from '../src/util/MobileRetryTracker' + +test('MobileRetryTracker stops retries after configured limit', () => { + const tracker = new MobileRetryTracker(2) + + assert.equal(tracker.registerFailure(), true) + assert.equal(tracker.hasExceeded(), false) + assert.equal(tracker.getAttemptCount(), 1) + + assert.equal(tracker.registerFailure(), true) + assert.equal(tracker.hasExceeded(), false) + assert.equal(tracker.getAttemptCount(), 2) + + assert.equal(tracker.registerFailure(), false) + assert.equal(tracker.hasExceeded(), true) + assert.equal(tracker.getAttemptCount(), 3) +}) + +test('MobileRetryTracker normalizes invalid configuration', () => { + const tracker = new MobileRetryTracker(-3) + + assert.equal(tracker.registerFailure(), false) + assert.equal(tracker.hasExceeded(), true) + assert.equal(tracker.getAttemptCount(), 1) +})