Merge pull request #12 from LightZirconite/main

Latest update, fixing many issues before V3
This commit is contained in:
Obsidian
2025-11-15 12:53:56 +01:00
committed by GitHub
13 changed files with 259 additions and 183 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "microsoft-rewards-bot", "name": "microsoft-rewards-bot",
"version": "2.59.1", "version": "2.60.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "microsoft-rewards-bot", "name": "microsoft-rewards-bot",
"version": "2.59.1", "version": "2.60.0",
"license": "CC-BY-NC-SA-4.0", "license": "CC-BY-NC-SA-4.0",
"dependencies": { "dependencies": {
"axios": "^1.8.4", "axios": "^1.8.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-bot", "name": "microsoft-rewards-bot",
"version": "2.59.1", "version": "2.60.0",
"description": "Automate Microsoft Rewards points collection", "description": "Automate Microsoft Rewards points collection",
"private": true, "private": true,
"main": "index.js", "main": "index.js",
@@ -26,7 +26,7 @@
"prestart": "node -e \"const fs = require('fs'); if (!fs.existsSync('dist/index.js')) { console.log('⚠️ Compiled files not found, building...'); require('child_process').execSync('npm run build', {stdio: 'inherit'}); }\"", "prestart": "node -e \"const fs = require('fs'); if (!fs.existsSync('dist/index.js')) { console.log('⚠️ Compiled files not found, building...'); require('child_process').execSync('npm run build', {stdio: 'inherit'}); }\"",
"start": "node --enable-source-maps ./dist/index.js", "start": "node --enable-source-maps ./dist/index.js",
"dev": "ts-node ./src/index.ts -dev", "dev": "ts-node ./src/index.ts -dev",
"test": "node --test --loader ts-node/esm tests/**/*.test.ts", "test": "node --test --require ts-node/register tests/**/*.test.ts",
"creator": "ts-node ./src/account-creation/cli.ts", "creator": "ts-node ./src/account-creation/cli.ts",
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard", "dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
"dashboard:dev": "ts-node ./src/index.ts -dashboard", "dashboard:dev": "ts-node ./src/index.ts -dashboard",

View File

@@ -105,6 +105,8 @@ export class HumanBehavior {
*/ */
async microGestures(context: string): Promise<void> { async microGestures(context: string): Promise<void> {
try { try {
const gestureNotes: string[] = []
// 60% chance of mouse movement (humans move mouse A LOT) // 60% chance of mouse movement (humans move mouse A LOT)
if (Math.random() < 0.6) { if (Math.random() < 0.6) {
const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px
@@ -115,8 +117,7 @@ export class HumanBehavior {
// Mouse move failed - page may be closed or unavailable // Mouse move failed - page may be closed or unavailable
}) })
// VERBOSE logging disabled - too noisy gestureNotes.push(`mouse→(${x},${y})`)
// log(false, 'CREATOR', `[${context}] 🖱️ Mouse moved to (${x}, ${y})`, 'log', 'gray')
} }
// 30% chance of scroll (humans scroll to read content) // 30% chance of scroll (humans scroll to read content)
@@ -129,8 +130,11 @@ export class HumanBehavior {
// Scroll failed - page may be closed or unavailable // Scroll failed - page may be closed or unavailable
}) })
// VERBOSE logging disabled - too noisy gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`)
// log(false, 'CREATOR', `[${context}] 📜 Scrolled ${direction > 0 ? 'down' : 'up'} ${distance}px`, 'log', 'gray') }
if (gestureNotes.length > 0) {
log(false, 'CREATOR', `[${context}] micro gestures: ${gestureNotes.join(', ')}`, 'log', 'gray')
} }
} catch { } catch {
// Gesture execution failed - not critical for operation // Gesture execution failed - not critical for operation

View File

@@ -170,7 +170,7 @@ export default class BrowserFunc {
}) })
this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG', this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG',
`DOM Diagnostic - ` + 'DOM Diagnostic - ' +
`URL: ${diagnosticInfo.url}, ` + `URL: ${diagnosticInfo.url}, ` +
`Title: "${diagnosticInfo.pageTitle}", ` + `Title: "${diagnosticInfo.pageTitle}", ` +
`Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` + `Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` +

View File

@@ -19,10 +19,10 @@ export interface AccountResult {
email: string email: string
pointsEarned: number pointsEarned: number
runDuration: number runDuration: number
initialPoints: number // Points avant l'exécution initialPoints: number // Points before execution
finalPoints: number // Points après l'exécution finalPoints: number // Points after execution
desktopPoints: number // Points gagnés sur Desktop desktopPoints: number // Points earned on desktop
mobilePoints: number // Points gagnés sur Mobile mobilePoints: number // Points earned on mobile
errors?: string[] errors?: string[]
banned?: boolean banned?: boolean
} }
@@ -38,12 +38,14 @@ export interface SummaryData {
export class SummaryReporter { export class SummaryReporter {
private config: Config private config: Config
private jobState: JobState private jobState?: JobState
constructor(config: Config) { constructor(config: Config) {
this.config = config this.config = config
if (config.jobState?.enabled !== false) {
this.jobState = new JobState(config) this.jobState = new JobState(config)
} }
}
/** /**
* Send comprehensive summary via webhook with complete statistics * Send comprehensive summary via webhook with complete statistics
@@ -85,7 +87,7 @@ export class SummaryReporter {
description += ` | **🚫 Banned:** ${bannedCount}` description += ` | **🚫 Banned:** ${bannedCount}`
} }
description += `\n\n**📊 Account Details**\n` description += '\n\n**📊 Account Details**\n'
const accountsWithErrors: AccountResult[] = [] const accountsWithErrors: AccountResult[] = []
@@ -100,13 +102,13 @@ export class SummaryReporter {
description += `• Duration: ${durationSec}s\n` description += `• Duration: ${durationSec}s\n`
// Collect accounts with errors for separate webhook // Collect accounts with errors for separate webhook
if ((account.errors?.length || account.banned) && account.email) { if (this.hasAccountFailure(account)) {
accountsWithErrors.push(account) accountsWithErrors.push(account)
} }
} }
// Footer summary // Footer summary
description += `\n**🌐 Total Balance**\n` description += '\n**🌐 Total Balance**\n'
description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})` description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})`
const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00 const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00
@@ -145,7 +147,7 @@ export class SummaryReporter {
// Error details // Error details
if (account.banned) { if (account.banned) {
errorDescription += `• Status: Account Banned/Suspended\n` errorDescription += '• Status: Account Banned/Suspended\n'
if (account.errors?.length && account.errors[0]) { if (account.errors?.length && account.errors[0]) {
errorDescription += `• Reason: ${account.errors[0]}\n` errorDescription += `• Reason: ${account.errors[0]}\n`
} }
@@ -153,14 +155,14 @@ export class SummaryReporter {
errorDescription += `• Error: ${account.errors[0]}\n` errorDescription += `• Error: ${account.errors[0]}\n`
} }
errorDescription += `\n` errorDescription += '\n'
} }
errorDescription += `**📋 Recommended Actions:**\n` errorDescription += '**📋 Recommended Actions:**\n'
errorDescription += `• Check account status manually\n` errorDescription += '• Check account status manually\n'
errorDescription += `• Review error messages above\n` errorDescription += '• Review error messages above\n'
errorDescription += `• Verify credentials if login failed\n` errorDescription += '• Verify credentials if login failed\n'
errorDescription += `• Consider proxy rotation if rate-limited` errorDescription += '• Consider proxy rotation if rate-limited'
await ConclusionWebhook( await ConclusionWebhook(
this.config, this.config,
@@ -195,6 +197,10 @@ export class SummaryReporter {
* Update job state with completion status * Update job state with completion status
*/ */
async updateJobState(summary: SummaryData): Promise<void> { async updateJobState(summary: SummaryData): Promise<void> {
if (!this.jobState) {
return
}
try { try {
const day = summary.endTime.toISOString().split('T')?.[0] const day = summary.endTime.toISOString().split('T')?.[0]
if (!day) return if (!day) return
@@ -205,7 +211,7 @@ export class SummaryReporter {
day, day,
{ {
totalCollected: account.pointsEarned, totalCollected: account.pointsEarned,
banned: false, banned: account.banned ?? false,
errors: account.errors?.length ?? 0 errors: account.errors?.length ?? 0
} }
) )
@@ -237,13 +243,15 @@ export class SummaryReporter {
log('main', 'SUMMARY', '─'.repeat(80)) log('main', 'SUMMARY', '─'.repeat(80))
for (const account of summary.accounts) { for (const account of summary.accounts) {
const status = account.errors?.length ? '❌ FAILED' : '✅ SUCCESS' const status = this.hasAccountFailure(account) ? (account.banned ? '🚫 BANNED' : '❌ FAILED') : '✅ SUCCESS'
const duration = Math.round(account.runDuration / 1000) const duration = Math.round(account.runDuration / 1000)
log('main', 'SUMMARY', `${status} | ${account.email}`) log('main', 'SUMMARY', `${status} | ${account.email}`)
log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`) log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`)
if (account.errors?.length) { if (account.banned) {
log('main', 'SUMMARY', ' Status: Account flagged as banned/suspended', 'error')
} else if (account.errors?.length) {
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error') log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
} }
} }
@@ -267,8 +275,8 @@ export class SummaryReporter {
endTime: Date endTime: Date
): SummaryData { ): SummaryData {
const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0) const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0)
const successCount = accounts.filter(acc => !acc.errors?.length).length const failureCount = accounts.filter(acc => this.hasAccountFailure(acc)).length
const failureCount = accounts.length - successCount const successCount = accounts.length - failureCount
return { return {
accounts, accounts,
@@ -279,4 +287,8 @@ export class SummaryReporter {
failureCount failureCount
} }
} }
private hasAccountFailure(account: AccountResult): boolean {
return Boolean(account.errors?.length) || account.banned === true
}
} }

View File

@@ -205,12 +205,12 @@ export class MicrosoftRewardsBot {
// Only cluster when there's more than 1 cluster demanded // Only cluster when there's more than 1 cluster demanded
if (this.config.clusters > 1) { if (this.config.clusters > 1) {
if (cluster.isPrimary) { if (cluster.isPrimary) {
this.runMaster() await this.runMaster()
} else if (cluster.worker) { } else if (cluster.worker) {
await this.runWorker() await this.runWorker()
} else { } else {
// Neither primary nor worker - something's wrong with clustering // Neither primary nor worker - something's wrong with clustering
log('main', 'MAIN', `ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.`, 'error') log('main', 'MAIN', 'ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.', 'error')
const passes = this.config.passesPerRun ?? 1 const passes = this.config.passesPerRun ?? 1
for (let pass = 1; pass <= passes; pass++) { for (let pass = 1; pass <= passes; pass++) {
if (passes > 1) { if (passes > 1) {
@@ -279,15 +279,17 @@ export class MicrosoftRewardsBot {
return this.accountSummaries return this.accountSummaries
} }
private runMaster() { private runMaster(): Promise<void> {
return new Promise((resolve) => {
log('main', 'MAIN-PRIMARY', 'Primary process started') log('main', 'MAIN-PRIMARY', 'Primary process started')
const totalAccounts = this.accounts.length const totalAccounts = this.accounts.length
// Validate accounts exist // Validate accounts exist
if (totalAccounts === 0) { if (totalAccounts === 0) {
log('main', 'MAIN-PRIMARY', 'No accounts found to process. Exiting.', 'warn') log('main', 'MAIN-PRIMARY', 'No accounts found to process. Nothing to do.', 'warn')
process.exit(0) resolve()
return
} }
// If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers. // If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers.
@@ -299,6 +301,19 @@ export class MicrosoftRewardsBot {
// Store worker-to-chunk mapping for crash recovery // Store worker-to-chunk mapping for crash recovery
const workerChunkMap = new Map<number, Account[]>() const workerChunkMap = new Map<number, Account[]>()
let resolved = false
const finishRun = async () => {
if (resolved) return
resolved = true
try {
await this.sendConclusion(this.accountSummaries)
} catch (e) {
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
log('main', 'MAIN-WORKER', 'All workers destroyed. Run complete.', 'warn')
resolve()
}
for (let i = 0; i < workerCount; i++) { for (let i = 0; i < workerCount; i++) {
const worker = cluster.fork() const worker = cluster.fork()
const chunk = accountChunks[i] || [] const chunk = accountChunks[i] || []
@@ -363,18 +378,10 @@ export class MicrosoftRewardsBot {
// Check if all workers have exited // Check if all workers have exited
if (this.activeWorkers === 0) { if (this.activeWorkers === 0) {
// All workers done -> send conclusion and exit (update check moved to startup) void finishRun()
(async () => {
try {
await this.sendConclusion(this.accountSummaries)
} catch (e) {
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
process.exit(0)
})()
} }
}) })
})
} }
private async runWorker() { private async runWorker() {
@@ -382,11 +389,21 @@ export class MicrosoftRewardsBot {
// Wait for chunk (either already received during init, or will arrive soon) // Wait for chunk (either already received during init, or will arrive soon)
const chunk = await new Promise<Account[]>((resolve) => { const chunk = await new Promise<Account[]>((resolve) => {
if ((global as any).__workerChunk) { if (global.__workerChunk) {
resolve((global as any).__workerChunk) const bufferedChunk = global.__workerChunk
} else { global.__workerChunk = undefined
(process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk: c }: { chunk: Account[] }) => resolve(c)) resolve(bufferedChunk)
return
} }
const handleMessage = (message: unknown): void => {
if (isWorkerChunkMessage(message)) {
process.off('message', handleMessage)
resolve(message.chunk)
}
}
process.on('message', handleMessage)
}) })
if (!chunk || chunk.length === 0) { if (!chunk || chunk.length === 0) {
@@ -896,6 +913,20 @@ function isWorkerMessage(msg: unknown): msg is WorkerMessage {
return m.type === 'summary' && Array.isArray(m.data) return m.type === 'summary' && Array.isArray(m.data)
} }
interface WorkerChunkMessage {
chunk: Account[]
}
function isWorkerChunkMessage(message: unknown): message is WorkerChunkMessage {
if (!message || typeof message !== 'object') return false
return Array.isArray((message as WorkerChunkMessage).chunk)
}
declare global {
// eslint-disable-next-line no-var
var __workerChunk: Account[] | undefined
}
// Use utility functions from Utils.ts // Use utility functions from Utils.ts
const shortErr = shortErrorMessage const shortErr = shortErrorMessage
const formatFullError = formatDetailedError const formatFullError = formatDetailedError
@@ -905,9 +936,14 @@ async function main(): Promise<void> {
// Workers initialize for ~2 seconds before reaching runWorker(), so messages // Workers initialize for ~2 seconds before reaching runWorker(), so messages
// sent by primary during initialization would be lost without this early listener // sent by primary during initialization would be lost without this early listener
if (!cluster.isPrimary && cluster.worker) { if (!cluster.isPrimary && cluster.worker) {
(process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk }: { chunk: Account[] }) => { const bufferChunk = (message: unknown): void => {
(global as any).__workerChunk = chunk if (isWorkerChunkMessage(message)) {
}) global.__workerChunk = message.chunk
process.off('message', bufferChunk)
}
}
process.on('message', bufferChunk)
} }
// Check for dashboard mode flag (standalone dashboard) // Check for dashboard mode flag (standalone dashboard)

View File

@@ -25,9 +25,16 @@ export function obfuscateWebhookUrl(url: string): string {
return Buffer.from(url).toString('base64') return Buffer.from(url).toString('base64')
} }
const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
export function deobfuscateWebhookUrl(encoded: string): string { export function deobfuscateWebhookUrl(encoded: string): string {
const trimmed = encoded.trim()
if (!trimmed || !BASE64_REGEX.test(trimmed)) {
return ''
}
try { try {
return Buffer.from(encoded, 'base64').toString('utf-8') return Buffer.from(trimmed, 'base64').toString('utf-8')
} catch { } catch {
return '' return ''
} }

View File

@@ -17,7 +17,7 @@ test('MobileFlow has run method', async () => {
// Mock bot instance // Mock bot instance
const mockBot = { const mockBot = {
log: () => {}, log: () => { },
isMobile: true, isMobile: true,
config: { config: {
workers: {}, workers: {},
@@ -52,10 +52,10 @@ test('MobileFlowResult interface has correct structure', async () => {
test('MobileFlow accepts retry tracker', async () => { test('MobileFlow accepts retry tracker', async () => {
const { MobileFlow } = await import('../../src/flows/MobileFlow') const { MobileFlow } = await import('../../src/flows/MobileFlow')
const { MobileRetryTracker } = await import('../../src/util/MobileRetryTracker') const { MobileRetryTracker } = await import('../../src/util/state/MobileRetryTracker')
const mockBot = { const mockBot = {
log: () => {}, log: () => { },
isMobile: true, isMobile: true,
config: { config: {
workers: {}, workers: {},

View File

@@ -39,8 +39,25 @@ test('SummaryReporter creates summary correctly', async () => {
const reporter = new SummaryReporter(mockConfig as never) const reporter = new SummaryReporter(mockConfig as never)
const accounts = [ const accounts = [
{ email: 'test@example.com', pointsEarned: 100, runDuration: 60000 }, {
{ email: 'test2@example.com', pointsEarned: 150, runDuration: 70000, errors: ['test error'] } email: 'test@example.com',
pointsEarned: 100,
runDuration: 60000,
initialPoints: 1000,
finalPoints: 1100,
desktopPoints: 60,
mobilePoints: 40
},
{
email: 'test2@example.com',
pointsEarned: 150,
runDuration: 70000,
initialPoints: 2000,
finalPoints: 2150,
desktopPoints: 90,
mobilePoints: 60,
errors: ['test error']
}
] ]
const startTime = new Date('2025-01-01T10:00:00Z') const startTime = new Date('2025-01-01T10:00:00Z')
@@ -67,7 +84,15 @@ test('SummaryData structure is correct', async () => {
const reporter = new SummaryReporter(mockConfig as never) const reporter = new SummaryReporter(mockConfig as never)
const summary = reporter.createSummary( const summary = reporter.createSummary(
[{ email: 'test@example.com', pointsEarned: 50, runDuration: 30000 }], [{
email: 'test@example.com',
pointsEarned: 50,
runDuration: 30000,
initialPoints: 500,
finalPoints: 550,
desktopPoints: 30,
mobilePoints: 20
}],
new Date(), new Date(),
new Date() new Date()
) )

View File

@@ -1,7 +1,7 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' import { LoginState, LoginStateDetector } from '../src/util/validation/LoginStateDetector'
/** /**
* Tests for LoginStateDetector - login flow state machine * Tests for LoginStateDetector - login flow state machine
@@ -10,6 +10,8 @@ import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
// Type helper for mock Page objects in tests // Type helper for mock Page objects in tests
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0] type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
const asMockPage = <T>(page: T): MockPage => page as unknown as MockPage
test('LoginState enum contains expected states', () => { test('LoginState enum contains expected states', () => {
assert.ok(LoginState.EmailPage, 'Should have EmailPage state') assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state') assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
@@ -60,7 +62,7 @@ test('detectState identifies LoggedIn state on rewards domain', async () => {
evaluate: () => Promise.resolve(200) evaluate: () => Promise.resolve(200)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state') assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -88,7 +90,7 @@ test('detectState identifies EmailPage state on login.live.com', async () => {
evaluate: () => Promise.resolve(100) evaluate: () => Promise.resolve(100)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state') assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -115,7 +117,7 @@ test('detectState identifies PasswordPage state', async () => {
evaluate: () => Promise.resolve(100) evaluate: () => Promise.resolve(100)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state') assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -142,7 +144,7 @@ test('detectState identifies TwoFactorRequired state', async () => {
evaluate: () => Promise.resolve(100) evaluate: () => Promise.resolve(100)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state') assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -170,7 +172,7 @@ test('detectState identifies PasskeyPrompt state', async () => {
evaluate: () => Promise.resolve(100) evaluate: () => Promise.resolve(100)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state') assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -198,7 +200,7 @@ test('detectState identifies Blocked state', async () => {
evaluate: () => Promise.resolve(100) evaluate: () => Promise.resolve(100)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state') assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
assert.equal(detection.confidence, 'high', 'Should have high confidence') assert.equal(detection.confidence, 'high', 'Should have high confidence')
@@ -216,7 +218,7 @@ test('detectState returns Unknown for ambiguous pages', async () => {
evaluate: () => Promise.resolve(50) evaluate: () => Promise.resolve(50)
} }
const detection = await LoginStateDetector.detectState(mockPage as MockPage) const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages') assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
assert.equal(detection.confidence, 'low', 'Should have low confidence') assert.equal(detection.confidence, 'low', 'Should have low confidence')
@@ -234,7 +236,7 @@ test('detectState handles errors gracefully', async () => {
} }
try { try {
await LoginStateDetector.detectState(mockPage as MockPage) await LoginStateDetector.detectState(asMockPage(mockPage))
assert.fail('Should throw error') assert.fail('Should throw error')
} catch (e) { } catch (e) {
assert.ok(e instanceof Error, 'Should throw Error instance') assert.ok(e instanceof Error, 'Should throw Error instance')

View File

@@ -1,7 +1,7 @@
import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test'
import { MobileRetryTracker } from '../src/util/MobileRetryTracker' import { MobileRetryTracker } from '../src/util/state/MobileRetryTracker'
test('MobileRetryTracker stops retries after configured limit', () => { test('MobileRetryTracker stops retries after configured limit', () => {
const tracker = new MobileRetryTracker(2) const tracker = new MobileRetryTracker(2)

View File

@@ -1,7 +1,7 @@
import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test'
import { QueryDiversityEngine } from '../src/util/QueryDiversityEngine' import { QueryDiversityEngine } from '../src/util/network/QueryDiversityEngine'
test('QueryDiversityEngine fetches and limits queries', async () => { test('QueryDiversityEngine fetches and limits queries', async () => {
const engine = new QueryDiversityEngine({ const engine = new QueryDiversityEngine({
@@ -13,7 +13,7 @@ test('QueryDiversityEngine fetches and limits queries', async () => {
assert.ok(queries.length > 0, 'Should return at least one query') assert.ok(queries.length > 0, 'Should return at least one query')
assert.ok(queries.length <= 10, 'Should respect count limit') assert.ok(queries.length <= 10, 'Should respect count limit')
assert.ok(queries.every(q => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings') assert.ok(queries.every((q: string) => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings')
}) })
test('QueryDiversityEngine deduplicates queries', async () => { test('QueryDiversityEngine deduplicates queries', async () => {

View File

@@ -3,18 +3,8 @@
* Tests intelligent page readiness and element detection * Tests intelligent page readiness and element detection
*/ */
import assert from 'node:assert' import assert from 'node:assert';
import { describe, it } from 'node:test' import { describe, it } from 'node:test';
// Mock Playwright types for testing
type MockPage = {
url: () => string
content: () => Promise<string>
waitForLoadState: (state: string, options?: { timeout: number }) => Promise<void>
waitForTimeout: (ms: number) => Promise<void>
locator: (selector: string) => MockLocator
evaluate: <T>(fn: () => T) => Promise<T>
}
type MockLocator = { type MockLocator = {
waitFor: (options: { state: string; timeout: number }) => Promise<void> waitFor: (options: { state: string; timeout: number }) => Promise<void>
@@ -94,7 +84,7 @@ describe('SmartWait', () => {
mockLogFn('✓ Element found quickly (567ms)') mockLogFn('✓ Element found quickly (567ms)')
assert.strictEqual(logs.length, 2, 'Should capture log messages') assert.strictEqual(logs.length, 2, 'Should capture log messages')
assert.ok(logs[0].includes('1234ms'), 'Should include timing data') assert.ok(logs[0]?.includes('1234ms'), 'Should include timing data')
}) })
it('should extract performance metrics from logs', () => { it('should extract performance metrics from logs', () => {
@@ -102,8 +92,8 @@ describe('SmartWait', () => {
const timeMatch = logMessage.match(/(\d+)ms/) const timeMatch = logMessage.match(/(\d+)ms/)
assert.ok(timeMatch, 'Should include parseable timing') assert.ok(timeMatch, 'Should include parseable timing')
if (timeMatch) { if (timeMatch && timeMatch[1]) {
const time = parseInt(timeMatch[1]) const time = parseInt(timeMatch[1], 10)
assert.ok(time > 0, 'Should extract valid timing') assert.ok(time > 0, 'Should extract valid timing')
} }
}) })