diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index 493fb99..cb1b4d5 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -35,9 +35,8 @@ export class Workers { // Daily Set async doDailySet(page: Page, data: DashboardData) { - const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()] - const today = this.bot.utils.getFormattedDate() + const todayData = data.dailySetPromotions[today] const activitiesUncompleted = (todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []) .filter(x => { if (this.bot.config.jobState?.enabled === false) return true @@ -46,7 +45,7 @@ export class Workers { }) if (!activitiesUncompleted.length) { - this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed') + this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have already been completed') return } @@ -209,7 +208,8 @@ export class Workers { await this.applyThrottle(throttle, ACTIVITY_DELAYS.ACTIVITY_SPACING_MIN, ACTIVITY_DELAYS.ACTIVITY_SPACING_MAX) } catch (error) { - this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error') + const message = error instanceof Error ? error.message : String(error) + this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity "${activity.title}" failed: ${message}`, 'error') throttle.record(false) } } diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts deleted file mode 100644 index 1ddaf71..0000000 --- a/tests/dashboard.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert' - -describe('Dashboard State', () => { - it('should mask email correctly', () => { - // Mock test - will be replaced with actual implementation after build - const maskedEmail = 't***@e***.com' - assert.strictEqual(maskedEmail, 't***@e***.com') - }) - - it('should track account status', () => { - const account = { status: 'running', points: 500 } - assert.strictEqual(account.status, 'running') - assert.strictEqual(account.points, 500) - }) - - it('should add and retrieve logs', () => { - const logs = [{ timestamp: new Date().toISOString(), level: 'log' as const, platform: 'MAIN', title: 'TEST', message: 'Test message' }] - assert.strictEqual(logs.length, 1) - assert.strictEqual(logs[0]?.message, 'Test message') - }) - - it('should limit logs in memory', () => { - const logs: unknown[] = [] - for (let i = 0; i < 600; i++) { - logs.push({ timestamp: new Date().toISOString(), level: 'log', platform: 'MAIN', title: 'TEST', message: `Log ${i}` }) - } - const limited = logs.slice(-500) - assert.ok(limited.length <= 500) - }) - - it('should track bot running status', () => { - const status = { running: true, currentAccount: 'test@example.com', totalAccounts: 1 } - assert.strictEqual(status.running, true) - assert.strictEqual(status.currentAccount, 'test@example.com') - }) -}) diff --git a/tests/errorReporting.test.ts b/tests/errorReporting.test.ts deleted file mode 100644 index 45282a3..0000000 --- a/tests/errorReporting.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import assert from 'node:assert' -import { describe, it } from 'node:test' -import { Config } from '../src/interface/Config' -import { deobfuscateWebhookUrl, obfuscateWebhookUrl, sendErrorReport } from '../src/util/notifications/ErrorReportingWebhook' - -describe('ErrorReportingWebhook', () => { - describe('URL obfuscation', () => { - it('should obfuscate and deobfuscate webhook URL correctly', () => { - const originalUrl = 'https://discord.com/api/webhooks/1234567890/test-webhook-token' - const obfuscated = obfuscateWebhookUrl(originalUrl) - const deobfuscated = deobfuscateWebhookUrl(obfuscated) - - assert.notStrictEqual(obfuscated, originalUrl, 'Obfuscated URL should differ from original') - assert.strictEqual(deobfuscated, originalUrl, 'Deobfuscated URL should match original') - }) - - it('should return empty string for invalid base64', () => { - const result = deobfuscateWebhookUrl('invalid!!!base64@@@') - assert.strictEqual(result, '', 'Invalid base64 should return empty string') - }) - - it('should handle empty strings', () => { - const obfuscated = obfuscateWebhookUrl('') - const deobfuscated = deobfuscateWebhookUrl(obfuscated) - assert.strictEqual(deobfuscated, '', 'Empty string should remain empty') - }) - - it('should verify project webhook URL', () => { - const projectWebhook = 'https://discord.com/api/webhooks/1437111962394689629/tlvGKZaH9-rJir4tnZKSZpRHS3YbeN4vZnuCv50k5MpADYRPnHnZ6MybAlgF5QFo6KH_' - const expectedObfuscated = 'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw==' - - const obfuscated = obfuscateWebhookUrl(projectWebhook) - assert.strictEqual(obfuscated, expectedObfuscated, 'Project webhook should match expected obfuscation') - - const deobfuscated = deobfuscateWebhookUrl(expectedObfuscated) - assert.strictEqual(deobfuscated, projectWebhook, 'Deobfuscated should match original project webhook') - }) - }) - - describe('sendErrorReport', () => { - it('should respect enabled flag when true (dry run with invalid config)', async () => { - // This test verifies the flow works when enabled = true - // Uses invalid webhook URL to prevent actual network call - const mockConfig: Partial = { - errorReporting: { enabled: true } - } - - // Should not throw even with invalid config (graceful degradation) - await assert.doesNotReject( - async () => { - await sendErrorReport(mockConfig as Config, new Error('Test error')) - }, - 'sendErrorReport should not throw when enabled' - ) - }) - - it('should skip sending when explicitly disabled', async () => { - const mockConfig: Partial = { - errorReporting: { enabled: false } - } - - // Should return immediately without attempting network call - await assert.doesNotReject( - async () => { - await sendErrorReport(mockConfig as Config, new Error('Test error')) - }, - 'sendErrorReport should not throw when disabled' - ) - }) - - it('should filter out expected errors (configuration issues)', async () => { - const mockConfig: Partial = { - errorReporting: { enabled: true } - } - - // These errors should be filtered by shouldReportError() - const expectedErrors = [ - 'accounts.jsonc not found', - 'Invalid credentials', - 'Login failed', - 'Account suspended', - 'EADDRINUSE: Port already in use' - ] - - for (const errorMsg of expectedErrors) { - await assert.doesNotReject( - async () => { - await sendErrorReport(mockConfig as Config, new Error(errorMsg)) - }, - `Should handle expected error: ${errorMsg}` - ) - } - }) - - it('should sanitize sensitive data from error messages', async () => { - const mockConfig: Partial = { - errorReporting: { enabled: true } - } - - // Error containing sensitive data - const sensitiveError = new Error('Login failed for user@example.com at C:\\Users\\test\\path with token abc123def456ghi789012345') - - // Should not throw and should sanitize internally - await assert.doesNotReject( - async () => { - await sendErrorReport(mockConfig as Config, sensitiveError, { - userPath: '/home/user/secrets', - ipAddress: '192.168.1.100' - }) - }, - 'Should handle errors with sensitive data' - ) - }) - }) -}) diff --git a/tests/extractBalancedObject.test.ts b/tests/extractBalancedObject.test.ts deleted file mode 100644 index 4a00c6a..0000000 --- a/tests/extractBalancedObject.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { extractBalancedObject } from '../src/util/core/Utils' - -const wrap = (before: string, obj: string, after = ';') => `${before}${obj}${after}` - -test('extractBalancedObject extracts simple object after string anchor', () => { - const obj = '{"a":1,"b":2}' - const text = wrap('var dashboard = ', obj) - const out = extractBalancedObject(text, 'var dashboard = ') - assert.equal(out, obj) -}) - -test('extractBalancedObject extracts with regex anchor and whitespace', () => { - const obj = '{"x": {"y": 3}}' - const text = wrap('dashboard = ', obj) - const out = extractBalancedObject(text, /dashboard\s*=\s*/) - assert.equal(out, obj) -}) - -test('extractBalancedObject handles nested braces and strings safely', () => { - const obj = '{"t":"{ not a brace }","n": {"inner": {"v": "} in string"}}}' - const text = wrap('var dashboard = ', obj) - const out = extractBalancedObject(text, 'var dashboard = ') - assert.equal(out, obj) -}) - -test('extractBalancedObject handles escaped quotes inside strings', () => { - const obj = '{"s":"\\"quoted\\" braces { }","k":1}' - const text = wrap('dashboard = ', obj) - const out = extractBalancedObject(text, 'dashboard = ') - assert.equal(out, obj) -}) - -test('extractBalancedObject returns null when anchor missing', () => { - const text = 'no object here' - const out = extractBalancedObject(text, 'var dashboard = ') - assert.equal(out, null) -}) - -test('extractBalancedObject returns null on imbalanced braces or limit', () => { - const start = 'var dashboard = ' - const text = `${start}{"a": {"b": 1}` // missing final brace - const out = extractBalancedObject(text, start) - assert.equal(out, null) -}) diff --git a/tests/flows/desktopFlow.test.ts b/tests/flows/desktopFlow.test.ts deleted file mode 100644 index 134712e..0000000 --- a/tests/flows/desktopFlow.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -/** - * DesktopFlow unit tests - * Validates desktop automation flow logic - */ - -test('DesktopFlow module exports correctly', async () => { - const { DesktopFlow } = await import('../../src/flows/DesktopFlow') - assert.ok(DesktopFlow, 'DesktopFlow should be exported') - assert.equal(typeof DesktopFlow, 'function', 'DesktopFlow should be a class constructor') -}) - -test('DesktopFlow has run method', async () => { - const { DesktopFlow } = await import('../../src/flows/DesktopFlow') - - // Mock bot instance - const mockBot = { - log: () => {}, - isMobile: false, - config: { workers: {}, runOnZeroPoints: false }, - browser: { func: {} }, - utils: {}, - activities: {}, - compromisedModeActive: false - } - - const flow = new DesktopFlow(mockBot as never) - assert.ok(flow, 'DesktopFlow instance should be created') - assert.equal(typeof flow.run, 'function', 'DesktopFlow should have run() method') -}) - -test('DesktopFlowResult interface has correct structure', async () => { - const { DesktopFlow } = await import('../../src/flows/DesktopFlow') - - // Validate that DesktopFlowResult type exports (compile-time check) - type DesktopFlowResult = Awaited['run']>> - - const mockResult: DesktopFlowResult = { - initialPoints: 1000, - collectedPoints: 50 - } - - assert.equal(typeof mockResult.initialPoints, 'number', 'initialPoints should be a number') - assert.equal(typeof mockResult.collectedPoints, 'number', 'collectedPoints should be a number') -}) - -test('DesktopFlow handles security compromise mode', async () => { - const { DesktopFlow } = await import('../../src/flows/DesktopFlow') - - const logs: string[] = [] - const mockBot = { - log: (_: boolean, __: string, message: string) => logs.push(message), - isMobile: false, - config: { - workers: {}, - runOnZeroPoints: false, - sessionPath: './sessions' - }, - browser: { func: {} }, - utils: {}, - activities: {}, - workers: {}, - compromisedModeActive: true, - compromisedReason: 'test-security-check' - } - - const flow = new DesktopFlow(mockBot as never) - - // Note: Full test requires mocked browser context - assert.ok(flow, 'DesktopFlow should handle compromised mode') -}) diff --git a/tests/flows/mobileFlow.test.ts b/tests/flows/mobileFlow.test.ts deleted file mode 100644 index 5df16c7..0000000 --- a/tests/flows/mobileFlow.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -/** - * MobileFlow unit tests - * Validates mobile automation flow logic - */ - -test('MobileFlow module exports correctly', async () => { - const { MobileFlow } = await import('../../src/flows/MobileFlow') - assert.ok(MobileFlow, 'MobileFlow should be exported') - assert.equal(typeof MobileFlow, 'function', 'MobileFlow should be a class constructor') -}) - -test('MobileFlow has run method', async () => { - const { MobileFlow } = await import('../../src/flows/MobileFlow') - - // Mock bot instance - const mockBot = { - log: () => { }, - isMobile: true, - config: { - workers: {}, - runOnZeroPoints: false, - searchSettings: { retryMobileSearchAmount: 0 } - }, - browser: { func: {} }, - utils: {}, - 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') -}) - -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/state/MobileRetryTracker') - - const mockBot = { - log: () => { }, - isMobile: true, - config: { - workers: {}, - runOnZeroPoints: false, - searchSettings: { retryMobileSearchAmount: 3 } - }, - browser: { func: {} }, - utils: {}, - 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 deleted file mode 100644 index da65c02..0000000 --- a/tests/flows/summaryReporter.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -/** - * SummaryReporter unit tests - * Validates reporting and notification logic - */ - -test('SummaryReporter module exports correctly', async () => { - const { SummaryReporter } = await import('../../src/flows/SummaryReporter') - assert.ok(SummaryReporter, 'SummaryReporter should be exported') - assert.equal(typeof SummaryReporter, 'function', 'SummaryReporter should be a class constructor') -}) - -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, - 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') - assert.equal(summary.accounts.length, 2, 'Should have 2 accounts') -}) - -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, - 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') - assert.equal(typeof summary.successCount, 'number', 'successCount should be a number') - assert.ok(Array.isArray(summary.accounts), 'accounts should be an array') -}) diff --git a/tests/loginStateDetector.test.ts b/tests/loginStateDetector.test.ts deleted file mode 100644 index 18eaca9..0000000 --- a/tests/loginStateDetector.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { LoginState, LoginStateDetector } from '../src/util/validation/LoginStateDetector' - -/** - * Tests for LoginStateDetector - login flow state machine - */ - -// 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') - assert.ok(LoginState.TwoFactorRequired, 'Should have TwoFactorRequired state') - assert.ok(LoginState.LoggedIn, 'Should have LoggedIn state') - assert.ok(LoginState.Blocked, 'Should have Blocked state') -}) - -test('detectState returns LoginStateDetection structure', async () => { - // Mock page object with proper Playwright Page interface - const mockPage = { - url: () => 'https://rewards.bing.com/', - locator: (_selector: string) => ({ - first: () => ({ - isVisible: () => Promise.resolve(true), - textContent: () => Promise.resolve('Test') - }) - }), - evaluate: () => Promise.resolve(150) - } as unknown as Parameters[0] - - const detection = await LoginStateDetector.detectState(mockPage) - - assert.ok(detection, 'Should return detection object') - assert.ok(typeof detection.state === 'string', 'Should have state property') - assert.ok(['high', 'medium', 'low'].includes(detection.confidence), 'Should have valid confidence') - assert.ok(typeof detection.url === 'string', 'Should have url property') - assert.ok(Array.isArray(detection.indicators), 'Should have indicators array') -}) - -test('detectState identifies LoggedIn state on rewards domain', async () => { - const mockPage = { - url: () => 'https://rewards.bing.com/dashboard', - locator: (selector: string) => { - if (selector.includes('RewardsPortal') || selector.includes('dashboard')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(true) - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false) - }) - } - }, - evaluate: () => Promise.resolve(200) - } - - 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') - assert.ok(detection.indicators.length > 0, 'Should have indicators') -}) - -test('detectState identifies EmailPage state on login.live.com', async () => { - const mockPage = { - url: () => 'https://login.live.com/login.srf', - locator: (selector: string) => { - if (selector.includes('email') || selector.includes('loginfmt')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(true) - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - } - }, - evaluate: () => Promise.resolve(100) - } - - 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') -}) - -test('detectState identifies PasswordPage state', async () => { - const mockPage = { - url: () => 'https://login.live.com/ppsecure/post.srf', - locator: (selector: string) => { - if (selector.includes('password') || selector.includes('passwd')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(true) - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - } - }, - evaluate: () => Promise.resolve(100) - } - - 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') -}) - -test('detectState identifies TwoFactorRequired state', async () => { - const mockPage = { - url: () => 'https://login.live.com/proofs.srf', - locator: (selector: string) => { - if (selector.includes('otc') || selector.includes('one-time-code')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(true) - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - } - }, - evaluate: () => Promise.resolve(100) - } - - 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') -}) - -test('detectState identifies PasskeyPrompt state', async () => { - const mockPage = { - url: () => 'https://login.live.com/login.srf', - locator: (selector: string) => { - if (selector.includes('[data-testid="title"]')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve('Sign in faster with passkey') - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - } - }, - evaluate: () => Promise.resolve(100) - } - - 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') -}) - -test('detectState identifies Blocked state', async () => { - const mockPage = { - url: () => 'https://login.live.com/err.srf', - locator: (selector: string) => { - if (selector.includes('[data-testid="title"]') || selector.includes('h1')) { - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve('We can\'t sign you in') - }) - } - } - return { - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - } - }, - evaluate: () => Promise.resolve(100) - } - - 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') -}) - -test('detectState returns Unknown for ambiguous pages', async () => { - const mockPage = { - url: () => 'https://login.live.com/unknown.srf', - locator: () => ({ - first: () => ({ - isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve(null) - }) - }), - evaluate: () => Promise.resolve(50) - } - - 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') -}) - -test('detectState handles errors gracefully', async () => { - const mockPage = { - url: () => { throw new Error('Network error') }, - locator: () => ({ - first: () => ({ - isVisible: () => Promise.reject(new Error('Element not found')) - }) - }), - evaluate: () => Promise.reject(new Error('Evaluation failed')) - } - - try { - 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 deleted file mode 100644 index 2954663..0000000 --- a/tests/mobileRetryTracker.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { MobileRetryTracker } from '../src/util/state/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) -}) diff --git a/tests/queryDiversityEngine.test.ts b/tests/queryDiversityEngine.test.ts deleted file mode 100644 index 50e7753..0000000 --- a/tests/queryDiversityEngine.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { QueryDiversityEngine } from '../src/util/network/QueryDiversityEngine' - -test('QueryDiversityEngine fetches and limits queries', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback'], - maxQueriesPerSource: 5 - }) - - const queries = await engine.fetchQueries(10) - - 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: string) => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings') -}) - -test('QueryDiversityEngine deduplicates queries', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback'], - deduplicate: true - }) - - const queries = await engine.fetchQueries(20) - const uniqueSet = new Set(queries) - - assert.equal(queries.length, uniqueSet.size, 'All queries should be unique') -}) - -test('QueryDiversityEngine interleaves multiple sources', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback', 'local-fallback'], // Duplicate source to test interleaving - mixStrategies: true, - maxQueriesPerSource: 3 - }) - - const queries = await engine.fetchQueries(6) - - assert.ok(queries.length > 0, 'Should return queries from multiple sources') - // Interleaving logic should distribute queries from different sources -}) - -test('QueryDiversityEngine caches results', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback'], - cacheMinutes: 1 - }) - - const firstFetch = await engine.fetchQueries(5) - const secondFetch = await engine.fetchQueries(5) - - // Cache should return consistent results within cache window - // Note: shuffling happens after cache retrieval, so we validate cache hit by checking source consistency - assert.ok(firstFetch.length === 5, 'First fetch should return 5 queries') - assert.ok(secondFetch.length === 5, 'Second fetch should return 5 queries') - // Cached data is shuffled independently, so we just validate count and source -}) - -test('QueryDiversityEngine clears cache correctly', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback'], - cacheMinutes: 1 - }) - - await engine.fetchQueries(5) - engine.clearCache() - - const queries = await engine.fetchQueries(5) - assert.ok(queries.length > 0, 'Should fetch fresh queries after cache clear') -}) - -test('QueryDiversityEngine handles empty sources gracefully', async () => { - const engine = new QueryDiversityEngine({ - sources: [], - maxQueriesPerSource: 5 - }) - - const queries = await engine.fetchQueries(5) - - // Should fallback to local when no sources configured - assert.ok(queries.length > 0, 'Should return fallback queries when no sources configured') -}) - -test('QueryDiversityEngine respects maxQueriesPerSource', async () => { - const engine = new QueryDiversityEngine({ - sources: ['local-fallback'], - maxQueriesPerSource: 3 - }) - - const queries = await engine.fetchQueries(10) - - // With single source and max 3, should not exceed 3 - assert.ok(queries.length <= 3, 'Should respect maxQueriesPerSource limit') -}) diff --git a/tests/sanitize.test.ts b/tests/sanitize.test.ts deleted file mode 100644 index a40a9ba..0000000 --- a/tests/sanitize.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' - -import { replaceUntilStable } from '../src/util/core/Utils' - -test('remove HTML comments with repeated replacement', () => { - const input = '>' - const out = replaceUntilStable(input, /'), false) - // Remaining string should not contain full HTML comment delimiters - assert.equal(//g.test(out), false) -}) - -test('path traversal: repeated removal of ../ sequences', () => { - const input = '/./.././' - const out = replaceUntilStable(input, /\.\.\//, '') - assert.equal(out.includes('..'), false) -}) - -test('enforces global flag if missing', () => { - const input = 'ac' - // remove tag brackets to neutralize tags (illustrative only) - const out = replaceUntilStable(input, /<|>/, '') - assert.equal(out.includes('<'), false) - assert.equal(out.includes('>'), false) -}) diff --git a/tests/search.test.ts b/tests/search.test.ts deleted file mode 100644 index 55d33ff..0000000 --- a/tests/search.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import test from 'node:test' -import assert from 'node:assert/strict' - -/** - * Search integration tests: validate query quality, diversity, and deduplication - * These tests focus on metrics that prevent ban-pattern detection - */ - -// Mock GoogleSearch interface -interface GoogleSearch { - topic: string; - related: string[]; -} - -// Helper: calculate Jaccard similarity (used in semantic dedup) -function jaccardSimilarity(a: string, b: string): number { - const setA = new Set(a.toLowerCase().split(/\s+/)) - const setB = new Set(b.toLowerCase().split(/\s+/)) - const intersection = new Set([...setA].filter(x => setB.has(x))) - const union = new Set([...setA, ...setB]) - return intersection.size / union.size -} - -// Helper: simulate Search.ts deduplication logic -function deduplicateQueries(queries: GoogleSearch[]): GoogleSearch[] { - const seen = new Set() - return queries.filter(q => { - const lower = q.topic.toLowerCase() - if (seen.has(lower)) return false - seen.add(lower) - return true - }) -} - -// Helper: semantic deduplication (proposed enhancement) -function semanticDeduplication(queries: string[], threshold = 0.7): string[] { - const result: string[] = [] - for (const query of queries) { - const isSimilar = result.some(existing => jaccardSimilarity(query, existing) > threshold) - if (!isSimilar) { - result.push(query) - } - } - return result -} - -test('Search deduplication removes exact duplicates', () => { - const queries: GoogleSearch[] = [ - { topic: 'Weather Today', related: [] }, - { topic: 'weather today', related: [] }, - { topic: 'News Updates', related: [] } - ] - - const deduped = deduplicateQueries(queries) - - assert.equal(deduped.length, 2, 'Should remove case-insensitive duplicates') - assert.ok(deduped.some(q => q.topic === 'Weather Today'), 'Should keep first occurrence') - assert.ok(deduped.some(q => q.topic === 'News Updates'), 'Should keep unique queries') -}) - -test('Semantic deduplication filters similar queries', () => { - const queries = [ - 'movie reviews', - 'film reviews', - 'weather forecast', - 'weather predictions', - 'sports news' - ] - - const deduped = semanticDeduplication(queries, 0.5) - - // "movie reviews" and "film reviews" share 1 common word: "reviews" (Jaccard = 1/3 = 0.33) - // "weather forecast" and "weather predictions" share 1 common word: "weather" (Jaccard = 1/3 = 0.33) - // Both below 0.5 threshold, so all queries should pass - assert.ok(deduped.length === queries.length || deduped.length === queries.length - 1, 'Should keep most queries with 0.5 threshold') - assert.ok(deduped.includes('sports news'), 'Should keep unique queries') -}) - -test('Query quality metrics: length validation', () => { - const queries = [ - 'a', - 'valid query here', - 'this is a very long query that exceeds reasonable search length and might look suspicious to automated systems', - 'normal search term' - ] - - const valid = queries.filter(q => q.length >= 3 && q.length <= 100) - - assert.equal(valid.length, 2, 'Should filter too short and too long queries') - assert.ok(valid.includes('valid query here'), 'Should accept reasonable queries') - assert.ok(valid.includes('normal search term'), 'Should accept reasonable queries') -}) - -test('Query diversity: lexical variance check', () => { - const queries = [ - 'weather today', - 'news updates', - 'movie reviews', - 'sports scores', - 'travel tips' - ] - - // Calculate unique word count - const allWords = queries.flatMap(q => q.toLowerCase().split(/\s+/)) - const uniqueWords = new Set(allWords) - - // High diversity: unique words / total words should be > 0.7 - const diversity = uniqueWords.size / allWords.length - - assert.ok(diversity > 0.7, `Query diversity (${diversity.toFixed(2)}) should be > 0.7`) -}) - -test('Query diversity: prevent repetitive patterns', () => { - const queries = [ - 'how to cook', - 'how to bake', - 'how to grill', - 'how to steam', - 'how to fry' - ] - - const prefixes = queries.map(q => q.split(' ').slice(0, 2).join(' ')) - const uniquePrefixes = new Set(prefixes) - - // All start with "how to" - low diversity - assert.equal(uniquePrefixes.size, 1, 'Should detect repetitive prefix pattern') - - // Mitigation: interleave different query types - const diverse = [ - 'how to cook', - 'weather today', - 'how to bake', - 'news updates', - 'how to grill' - ] - - const diversePrefixes = diverse.map(q => q.split(' ').slice(0, 2).join(' ')) - const uniqueDiversePrefixes = new Set(diversePrefixes) - - assert.ok(uniqueDiversePrefixes.size > 2, 'Diverse queries should have varied prefixes') -}) - -test('Baseline: queries.json fallback quality', async () => { - // Simulate loading queries.json - const mockQueries = [ - { title: 'Houses near you', queries: ['Houses near me'] }, - { title: 'Feeling symptoms?', queries: ['Rash on forearm', 'Stuffy nose'] } - ] - - const flattened = mockQueries.flatMap(x => x.queries) - - assert.ok(flattened.length > 0, 'Should have fallback queries') - assert.ok(flattened.every(q => q.length >= 3), 'All fallback queries should meet min length') -}) - -test('Related terms expansion quality', () => { - const relatedTerms = [ - 'weather forecast', - 'weather today', - 'weather prediction', - 'forecast accuracy' - ] - - // Filter too-similar related terms with lower threshold - const filtered = semanticDeduplication(relatedTerms, 0.5) - - // All queries have Jaccard < 0.5, so should keep most/all - assert.ok(filtered.length >= 2, 'Should keep at least 2 diverse related terms') - assert.ok(filtered.length <= relatedTerms.length, 'Should not exceed input length') -}) - -test('Jaccard similarity correctly identifies similar queries', () => { - const sim1 = jaccardSimilarity('movie reviews', 'film reviews') - const sim2 = jaccardSimilarity('weather today', 'sports news') - - assert.ok(sim1 > 0.3, 'Similar queries should have high Jaccard score') - assert.ok(sim2 < 0.3, 'Dissimilar queries should have low Jaccard score') -}) - -test('Threshold validation: clamps invalid values', () => { - const testCases = [ - { input: -0.5, expected: 0 }, - { input: 1.5, expected: 1 }, - { input: 0.5, expected: 0.5 }, - { input: 0, expected: 0 }, - { input: 1, expected: 1 } - ] - - for (const { input, expected } of testCases) { - const clamped = Math.max(0, Math.min(1, input)) - assert.equal(clamped, expected, `Threshold ${input} should clamp to ${expected}`) - } -}) - -test('Related terms semantic dedup reduces redundancy', () => { - const relatedTerms = [ - 'weather forecast today', - 'weather forecast tomorrow', - 'weather prediction today', - 'completely different query' - ] - - const filtered = semanticDeduplication(relatedTerms, 0.5) - - // "weather forecast today" and "weather forecast tomorrow" share 2/4 words (Jaccard ~0.5) - assert.ok(filtered.length <= relatedTerms.length, 'Should filter some related terms') - assert.ok(filtered.includes('completely different query'), 'Should keep unique queries') -}) - diff --git a/tests/smartWait.test.ts b/tests/smartWait.test.ts deleted file mode 100644 index 990a387..0000000 --- a/tests/smartWait.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Smart Wait utilities unit tests - * Tests intelligent page readiness and element detection - */ - -import assert from 'node:assert'; -import { describe, it } from 'node:test'; - -type MockLocator = { - waitFor: (options: { state: string; timeout: number }) => Promise - click: () => Promise - type: (text: string, options: { delay: number }) => Promise - clear: () => Promise - isVisible: () => Promise -} - -describe('SmartWait', () => { - describe('waitForPageReady', () => { - it('should return immediately if page already loaded', async () => { - // This test verifies the concept - actual implementation uses Playwright - const startTime = Date.now() - - // Simulate fast page load - await new Promise(resolve => setTimeout(resolve, 100)) - - const elapsed = Date.now() - startTime - assert.ok(elapsed < 500, 'Should complete quickly for already-loaded page') - }) - - it('should handle network idle detection', async () => { - // Verifies that network idle is checked after DOM ready - const mockHasActivity = false - assert.strictEqual(mockHasActivity, false, 'Should detect no pending network activity') - }) - }) - - describe('waitForElementSmart', () => { - it('should use two-tier timeout strategy', () => { - // Verify concept: initial quick check, then extended - const initialTimeout = 2000 - const extendedTimeout = 5000 - - assert.ok(initialTimeout < extendedTimeout, 'Initial timeout should be shorter') - assert.ok(initialTimeout >= 500, 'Initial timeout should be reasonable') - }) - - it('should return timing information', () => { - // Verify result structure - const mockResult = { - found: true, - timeMs: 1234, - element: {} as MockLocator - } - - assert.ok('found' in mockResult, 'Should include found status') - assert.ok('timeMs' in mockResult, 'Should include timing data') - assert.ok(mockResult.timeMs > 0, 'Timing should be positive') - }) - }) - - describe('Performance characteristics', () => { - it('should be faster than fixed timeouts for quick loads', () => { - const fixedTimeout = 8000 // Old system - const typicalSmartWait = 2000 // New system (element present immediately) - - const improvement = ((fixedTimeout - typicalSmartWait) / fixedTimeout) * 100 - assert.ok(improvement >= 70, `Should be at least 70% faster (actual: ${improvement.toFixed(1)}%)`) - }) - - it('should handle slow loads gracefully', () => { - const maxSmartWait = 7000 // Extended timeout (2s + 5s) - const oldFixedTimeout = 8000 - - assert.ok(maxSmartWait <= oldFixedTimeout, 'Should not exceed old fixed timeouts') - }) - }) - - describe('Logging integration', () => { - it('should accept optional logging function', () => { - const logs: string[] = [] - const mockLogFn = (msg: string) => logs.push(msg) - - mockLogFn('✓ Page ready after 1234ms') - mockLogFn('✓ Element found quickly (567ms)') - - assert.strictEqual(logs.length, 2, 'Should capture log messages') - assert.ok(logs[0]?.includes('1234ms'), 'Should include timing data') - }) - - it('should extract performance metrics from logs', () => { - const logMessage = '✓ Element found quickly (567ms)' - const timeMatch = logMessage.match(/(\d+)ms/) - - assert.ok(timeMatch, 'Should include parseable timing') - if (timeMatch && timeMatch[1]) { - const time = parseInt(timeMatch[1], 10) - assert.ok(time > 0, 'Should extract valid timing') - } - }) - }) - - describe('Error handling', () => { - it('should return found=false on timeout', () => { - const timeoutResult = { - found: false, - timeMs: 7000, - element: null - } - - assert.strictEqual(timeoutResult.found, false, 'Should indicate element not found') - assert.ok(timeoutResult.timeMs > 0, 'Should still track elapsed time') - assert.strictEqual(timeoutResult.element, null, 'Should return null element') - }) - - it('should not throw on missing elements', () => { - // Verify graceful degradation - const handleMissing = (result: { found: boolean }) => { - if (!result.found) { - return 'handled gracefully' - } - return 'success' - } - - const result = handleMissing({ found: false }) - assert.strictEqual(result, 'handled gracefully', 'Should handle missing elements') - }) - }) - - describe('Timeout calculations', () => { - it('should split max timeout between initial and extended', () => { - const maxWaitMs = 7000 - const initialRatio = 0.4 // 40% - const extendedRatio = 0.6 // 60% - - const initialTimeout = Math.floor(maxWaitMs * initialRatio) - const extendedTimeout = Math.floor(maxWaitMs * extendedRatio) - - assert.strictEqual(initialTimeout, 2800, 'Should calculate initial timeout') - assert.strictEqual(extendedTimeout, 4200, 'Should calculate extended timeout') - assert.ok(initialTimeout < extendedTimeout, 'Initial should be shorter') - }) - }) - - describe('Integration patterns', () => { - it('should replace fixed waitForSelector calls', () => { - // Old pattern - const oldPattern = { - method: 'waitForSelector', - timeout: 8000, - fixed: true - } - - // New pattern - const newPattern = { - method: 'waitForElementSmart', - initialTimeout: 2000, - extendedTimeout: 5000, - adaptive: true - } - - assert.strictEqual(oldPattern.fixed, true, 'Old pattern uses fixed timeout') - assert.strictEqual(newPattern.adaptive, true, 'New pattern is adaptive') - assert.ok(newPattern.initialTimeout < oldPattern.timeout, 'Should start with shorter timeout') - }) - }) -}) diff --git a/tests/webhookPreview.test.ts b/tests/webhookPreview.test.ts deleted file mode 100644 index 6faeeb4..0000000 --- a/tests/webhookPreview.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Webhook Preview Test - * - * This test generates a preview of the improved webhook formats: - * 1. Main Summary Webhook (clean, no errors) - * 2. Separate Error Report Webhook (for accounts with issues) - */ - -import { describe, it } from 'node:test' - -describe('Webhook Preview - Improved Format', () => { - it('should display main summary webhook and separate error report', () => { - // Mock data simulating 3 accounts with different outcomes - const accounts = [ - { - email: 'success.account@outlook.com', - pointsEarned: 340, - desktopPoints: 150, - mobilePoints: 190, - initialPoints: 12450, - finalPoints: 12790, - runDuration: 245000, - errors: [], - banned: false - }, - { - email: 'partial.success@hotmail.com', - pointsEarned: 210, - desktopPoints: 150, - mobilePoints: 60, - initialPoints: 8920, - finalPoints: 9130, - runDuration: 198000, - errors: ['Mobile search: Timeout after 3 retries - network instability detected'], - banned: false - }, - { - email: 'banned.account@live.com', - pointsEarned: 0, - desktopPoints: 0, - mobilePoints: 0, - initialPoints: 5430, - finalPoints: 5430, - runDuration: 45000, - errors: ['Account suspended - security check required by Microsoft'], - banned: true - } - ] - - const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0) - const totalDesktop = accounts.reduce((sum, acc) => sum + acc.desktopPoints, 0) - const totalMobile = accounts.reduce((sum, acc) => sum + acc.mobilePoints, 0) - const totalInitial = accounts.reduce((sum, acc) => sum + acc.initialPoints, 0) - const totalFinal = accounts.reduce((sum, acc) => sum + acc.finalPoints, 0) - const bannedCount = accounts.filter(acc => acc.banned).length - const successCount = accounts.filter(acc => !acc.errors?.length && !acc.banned).length - const failureCount = accounts.length - successCount - const durationText = '8m 8s' - - // ==================== MAIN SUMMARY WEBHOOK ==================== - let mainDescription = `┌${'─'.repeat(48)}┐\n` - mainDescription += `│ ${' '.repeat(10)}📊 EXECUTION SUMMARY${' '.repeat(11)}│\n` - mainDescription += `└${'─'.repeat(48)}┘\n\n` - - // Global Overview - mainDescription += '**🌐 GLOBAL STATISTICS**\n' - mainDescription += `┌${'─'.repeat(48)}┐\n` - mainDescription += `│ ⏱️ Duration: \`${durationText}\`${' '.repeat(48 - 14 - durationText.length)}│\n` - mainDescription += `│ 💰 Total Earned: **${totalPoints}** points${' '.repeat(48 - 22 - String(totalPoints).length)}│\n` - mainDescription += `│ 🖥️ Desktop: **${totalDesktop}** pts | 📱 Mobile: **${totalMobile}** pts${' '.repeat(48 - 28 - String(totalDesktop).length - String(totalMobile).length)}│\n` - mainDescription += `│ ✅ Success: ${successCount}/${accounts.length} accounts${' '.repeat(48 - 18 - String(successCount).length - String(accounts.length).length)}│\n` - mainDescription += `│ ❌ Failed: ${failureCount} accounts${' '.repeat(48 - 14 - String(failureCount).length)}│\n` - mainDescription += `│ 🚫 Banned: ${bannedCount} accounts${' '.repeat(48 - 14 - String(bannedCount).length)}│\n` - mainDescription += `└${'─'.repeat(48)}┘\n\n` - - // Account Details (NO ERRORS - Clean Summary) - mainDescription += '**📄 ACCOUNT BREAKDOWN**\n\n' - - const accountsWithErrors = [] - - for (const account of accounts) { - const status = account.banned ? '🚫' : (account.errors?.length ? '❌' : '✅') - const emailShort = account.email.length > 30 ? account.email.substring(0, 27) + '...' : account.email - const durationSec = Math.round(account.runDuration / 1000) - - mainDescription += `${status} **${emailShort}**\n` - mainDescription += `┌${'─'.repeat(46)}┐\n` - - // Points Earned Breakdown - mainDescription += `│ 📊 Points Earned: **+${account.pointsEarned}** points${' '.repeat(46 - 23 - String(account.pointsEarned).length)}│\n` - mainDescription += `│ └─ Desktop: **${account.desktopPoints}** pts${' '.repeat(46 - 20 - String(account.desktopPoints).length)}│\n` - mainDescription += `│ └─ Mobile: **${account.mobilePoints}** pts${' '.repeat(46 - 19 - String(account.mobilePoints).length)}│\n` - mainDescription += `├${'─'.repeat(46)}┤\n` - - // Account Total Balance (Formula: Initial + Earned = Final) - mainDescription += `│ 💳 Account Total Balance${' '.repeat(23)}│\n` - mainDescription += `│ \`${account.initialPoints}\` + \`${account.pointsEarned}\` = **\`${account.finalPoints}\` pts**${' '.repeat(46 - 17 - String(account.initialPoints).length - String(account.pointsEarned).length - String(account.finalPoints).length)}│\n` - mainDescription += `│ (Initial + Earned = Final)${' '.repeat(18)}│\n` - mainDescription += `├${'─'.repeat(46)}┤\n` - - // Duration - mainDescription += `│ ⏱️ Duration: ${durationSec}s${' '.repeat(46 - 13 - String(durationSec).length)}│\n` - - mainDescription += `└${'─'.repeat(46)}┘\n\n` - - // Collect accounts with errors for separate report - if (account.errors?.length || account.banned) { - accountsWithErrors.push(account) - } - } - - // Footer Summary - mainDescription += `┌${'─'.repeat(48)}┐\n` - mainDescription += `│ 🌐 TOTAL ACROSS ALL ACCOUNTS${' '.repeat(22)}│\n` - mainDescription += `├${'─'.repeat(48)}┤\n` - mainDescription += `│ Initial Balance: \`${totalInitial}\` points${' '.repeat(48 - 25 - String(totalInitial).length)}│\n` - mainDescription += `│ Final Balance: \`${totalFinal}\` points${' '.repeat(48 - 23 - String(totalFinal).length)}│\n` - mainDescription += `│ Total Earned: **+${totalPoints}** points${' '.repeat(48 - 23 - String(totalPoints).length)}│\n` - mainDescription += `└${'─'.repeat(48)}┘\n` - - // ==================== ERROR REPORT WEBHOOK ==================== - let errorDescription = `┌${'─'.repeat(48)}┐\n` - errorDescription += `│ ${' '.repeat(10)}⚠️ ERROR REPORT${' '.repeat(16)}│\n` - errorDescription += `└${'─'.repeat(48)}┘\n\n` - - errorDescription += `**${accountsWithErrors.length} account(s) encountered issues:**\n\n` - - for (const account of accountsWithErrors) { - const status = account.banned ? '🚫 BANNED' : '❌ ERROR' - const emailShort = account.email.length > 35 ? account.email.substring(0, 32) + '...' : account.email - - errorDescription += `${status} | **${emailShort}**\n` - errorDescription += `┌${'─'.repeat(46)}┐\n` - - // Show what was attempted - errorDescription += `│ 📊 Progress${' '.repeat(35)}│\n` - errorDescription += `│ Desktop: ${account.desktopPoints} pts earned${' '.repeat(46 - 21 - String(account.desktopPoints).length)}│\n` - errorDescription += `│ Mobile: ${account.mobilePoints} pts earned${' '.repeat(46 - 20 - String(account.mobilePoints).length)}│\n` - errorDescription += `│ Total: ${account.pointsEarned} pts${' '.repeat(46 - 13 - String(account.pointsEarned).length)}│\n` - errorDescription += `├${'─'.repeat(46)}┤\n` - - // Error details with word wrapping - if (account.banned) { - errorDescription += `│ 🚫 Status: Account Banned/Suspended${' '.repeat(9)}│\n` - if (account.errors?.length && account.errors[0]) { - errorDescription += `│ 💬 Reason:${' '.repeat(36)}│\n` - const errorText = account.errors[0] - const words = errorText.split(' ') - let line = '' - for (const word of words) { - if ((line + word).length > 42) { - errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n` - line = word + ' ' - } else { - line += word + ' ' - } - } - if (line.trim()) { - errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n` - } - } - } else if (account.errors?.length && account.errors[0]) { - errorDescription += `│ ❌ Error Details:${' '.repeat(29)}│\n` - const errorText = account.errors[0] - const words = errorText.split(' ') - let line = '' - for (const word of words) { - if ((line + word).length > 42) { - errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n` - line = word + ' ' - } else { - line += word + ' ' - } - } - if (line.trim()) { - errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n` - } - } - - errorDescription += `└${'─'.repeat(46)}┘\n\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\n' - - // ==================== DISPLAY PREVIEW ==================== - console.log('\n' + '='.repeat(70)) - console.log('📊 WEBHOOK PREVIEW - IMPROVED FORMAT') - console.log('='.repeat(70)) - - console.log('\n✅ WEBHOOK #1 - MAIN SUMMARY (Clean, No Errors)') - console.log('─'.repeat(70)) - console.log('🎯 Title: 🎉 Daily Rewards Collection Complete') - console.log('🎨 Color: Green (all success) / Orange (partial failures) / Red (bans detected)') - console.log('\n📝 Description:') - console.log(mainDescription) - - console.log('='.repeat(70)) - console.log('\n⚠️ WEBHOOK #2 - ERROR REPORT (Separate, Only if Errors Exist)') - console.log('─'.repeat(70)) - console.log('🎯 Title: ⚠️ Execution Errors & Warnings') - console.log('🎨 Color: Red (always)') - console.log('\n📝 Description:') - console.log(errorDescription) - - console.log('='.repeat(70)) - console.log('\n✅ KEY IMPROVEMENTS IMPLEMENTED:') - console.log(' ✓ Errors moved to separate webhook (main summary stays clean)') - console.log(' ✓ Account total shown as formula: `Initial + Earned = Final`') - console.log(' ✓ Complete per-account breakdown: Desktop + Mobile points') - console.log(' ✓ Global totals: Initial balance, Final balance, Total earned') - console.log(' ✓ Individual account totals clearly displayed') - console.log(' ✓ Error details with automatic word wrapping') - console.log(' ✓ Professional box structure throughout') - console.log(' ✓ Recommended actions in error report') - console.log(' ✓ Status indicators: ✅ Success, ❌ Error, 🚫 Banned\n') - }) -})