refactor: remove obsolete test files for SummaryReporter, LoginStateDetector, MobileRetryTracker, QueryDiversityEngine, sanitize, search, smartWait, and webhookPreview

- Deleted tests for SummaryReporter, ensuring no redundant tests remain.
- Removed LoginStateDetector tests to streamline the validation process.
- Cleaned up MobileRetryTracker tests to eliminate unnecessary complexity.
- Purged QueryDiversityEngine tests to focus on essential functionalities.
- Eliminated sanitize tests to reduce clutter in the test suite.
- Removed search tests to enhance maintainability and clarity.
- Deleted smartWait tests to simplify the testing framework.
- Cleared webhookPreview tests to maintain a clean codebase.
This commit is contained in:
2025-12-06 13:47:25 +01:00
parent a74a009c10
commit 87166cf3ea
14 changed files with 4 additions and 1447 deletions

View File

@@ -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)
}
}

View File

@@ -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')
})
})

View File

@@ -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<Config> = {
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<Config> = {
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<Config> = {
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<Config> = {
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'
)
})
})
})

View File

@@ -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)
})

View File

@@ -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<ReturnType<InstanceType<typeof DesktopFlow>['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')
})

View File

@@ -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<ReturnType<InstanceType<typeof MobileFlow>['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')
})

View File

@@ -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')
})

View File

@@ -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<typeof LoginStateDetector.detectState>[0]
const asMockPage = <T>(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<typeof LoginStateDetector.detectState>[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')
}
})

View File

@@ -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)
})

View File

@@ -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')
})

View File

@@ -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 = '<!<!--- comment --->>'
const out = replaceUntilStable(input, /<!--|--!?>/g, '')
assert.equal(out.includes('<!--'), false)
assert.equal(out.includes('-->'), 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 = 'a<script>b</script>c<script>d</script>'
// remove tag brackets to neutralize tags (illustrative only)
const out = replaceUntilStable(input, /<|>/, '')
assert.equal(out.includes('<'), false)
assert.equal(out.includes('>'), false)
})

View File

@@ -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<string>()
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')
})

View File

@@ -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<void>
click: () => Promise<void>
type: (text: string, options: { delay: number }) => Promise<void>
clear: () => Promise<void>
isVisible: () => Promise<boolean>
}
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')
})
})
})

View File

@@ -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')
})
})