From 967801515f721b8f044c398993a11d6a9d4a6045 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sun, 9 Nov 2025 22:23:46 +0100 Subject: [PATCH] feat: Simplify update process by removing deprecated methods and enhancing README; improve type safety in tests --- setup/README.md | 12 +++------- src/account-creation/AccountCreator.ts | 6 ----- src/index.ts | 28 ++--------------------- src/interface/Config.ts | 3 +-- src/util/Load.ts | 9 ++++---- src/util/Logger.ts | 8 +++++++ tests/loginStateDetector.test.ts | 31 ++++++++++++++------------ 7 files changed, 35 insertions(+), 62 deletions(-) diff --git a/setup/README.md b/setup/README.md index e65bf14..78fcf5a 100644 --- a/setup/README.md +++ b/setup/README.md @@ -33,21 +33,15 @@ npm start **Automatic update script** that keeps your bot up-to-date with the latest version. **Features:** -- Two update methods: Git-based or GitHub API (no Git needed) +- Uses GitHub API (downloads ZIP - no Git required) - Preserves your configuration and accounts -- No merge conflicts with GitHub API method +- No merge conflicts, always clean - Automatic dependency installation and rebuild **Usage:** ```bash -# Auto-detect method from config.jsonc +# Run update manually node setup/update/update.mjs - -# Force GitHub API method (recommended) -node setup/update/update.mjs --no-git - -# Force Git method -node setup/update/update.mjs --git ``` **Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc). diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index 2d4a0ef..fe40b11 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -1007,12 +1007,6 @@ export class AccountCreator { if (!suggestionsContainer) { log(false, 'CREATOR', 'No suggestions found from Microsoft', 'warn', 'yellow') - // Debug: check HTML content - const pageContent = await this.page.content() - const hasDataTestId = pageContent.includes('data-testid="suggestions"') - const hasToolbar = pageContent.includes('role="toolbar"') - log(false, 'CREATOR', `Debug - suggestions in HTML: ${hasDataTestId}, toolbar: ${hasToolbar}`, 'warn', 'yellow') - // CRITICAL FIX: Generate a new email automatically instead of freezing log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan') diff --git a/src/index.ts b/src/index.ts index a3296ea..1e493f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,14 +232,6 @@ export class MicrosoftRewardsBot { log('main', 'BANNER', `Microsoft Rewards Bot v${version}`) log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`) - - const upd = this.config.update || {} - const updTargets: string[] = [] - if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`) - if (upd.docker) updTargets.push('Docker') - if (updTargets.length > 0) { - log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`) - } } private getVersion(): string { @@ -770,28 +762,12 @@ export class MicrosoftRewardsBot { return 0 } - const args: string[] = [] - - // Determine update method from config (github-api is default and recommended) - const method = upd.method || 'github-api' - - if (method === 'github-api' || method === 'api' || method === 'zip') { - // Use GitHub API method (no Git needed, no conflicts) - args.push('--no-git') - } else { - // Unknown method, default to github-api - log('main', 'UPDATE', `Unknown update method "${method}", using github-api`, 'warn') - args.push('--no-git') - } - - // Add Docker flag if enabled - if (upd.docker) args.push('--docker') - + // New update.mjs uses GitHub API only and takes no CLI arguments log('main', 'UPDATE', `Running update script: ${scriptRel}`, 'log') // Run update script as a child process and capture exit code return new Promise((resolve) => { - const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' }) + const child = spawn(process.execPath, [scriptAbs], { stdio: 'inherit' }) child.on('close', (code) => { log('main', 'UPDATE', `Update script exited with code ${code ?? 0}`, code === 0 ? 'log' : 'warn') resolve(code ?? 0) diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 62caae9..7723971 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -83,11 +83,10 @@ export interface ConfigProxy { export interface ConfigUpdate { enabled?: boolean; // Master toggle for auto-updates (default: true) - method?: 'github-api' | 'api' | 'zip'; // Update method (default: "github-api") - docker?: boolean; // if true, run docker update routine (compose pull/up) after completion scriptPath?: string; // optional custom path to update script relative to repo root autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings) autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials) + // DEPRECATED (removed in v2.56.2+): method, docker - update.mjs now uses GitHub API only } export interface ConfigVacation { diff --git a/src/util/Load.ts b/src/util/Load.ts index e2de791..aa80084 100644 --- a/src/util/Load.ts +++ b/src/util/Load.ts @@ -70,11 +70,10 @@ function stripJsonComments(input: string): string { // Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface function normalizeConfig(raw: unknown): Config { - // TYPE SAFETY NOTE: Using `any` here is necessary for backwards compatibility - // JUSTIFIED USE OF `any`: The config format has evolved from flat → nested structure over time - // This needs to support BOTH formats for backward compatibility with existing user configs - // Runtime validation happens through explicit property checks and the Config interface return type ensures type safety at function boundary - // Alternative approaches (discriminated unions, conditional types) would require extensive runtime checks making code significantly more complex + // TYPE SAFETY NOTE: Using `any` here is intentional and justified + // Reason: Config format evolved from flat → nested structure. This function must support BOTH + // for backward compatibility. Runtime validation via explicit checks ensures safety. + // Return type (Config interface) provides type safety at function boundary. // eslint-disable-next-line @typescript-eslint/no-explicit-any const n = (raw || {}) as any diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 92c247c..986914e 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -49,6 +49,14 @@ if (typeof cleanupInterval.unref === 'function') { cleanupInterval.unref() } +/** + * Stop the webhook buffer cleanup interval + * Call this during graceful shutdown to prevent memory leaks + */ +export function stopWebhookCleanup(): void { + clearInterval(cleanupInterval) +} + /** * Get or create a webhook buffer for the given URL * Buffers batch log messages to reduce Discord API calls diff --git a/tests/loginStateDetector.test.ts b/tests/loginStateDetector.test.ts index 739d6cb..022094e 100644 --- a/tests/loginStateDetector.test.ts +++ b/tests/loginStateDetector.test.ts @@ -1,5 +1,5 @@ -import test from 'node:test' import assert from 'node:assert/strict' +import test from 'node:test' import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' @@ -7,6 +7,9 @@ import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' * Tests for LoginStateDetector - login flow state machine */ +// Type helper for mock Page objects in tests +type MockPage = Parameters[0] + test('LoginState enum contains expected states', () => { assert.ok(LoginState.EmailPage, 'Should have EmailPage state') assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state') @@ -16,19 +19,19 @@ test('LoginState enum contains expected states', () => { }) test('detectState returns LoginStateDetection structure', async () => { - // Mock page object + // Mock page object with proper Playwright Page interface const mockPage = { url: () => 'https://rewards.bing.com/', - locator: (selector: string) => ({ + 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 as any) + const detection = await LoginStateDetector.detectState(mockPage) assert.ok(detection, 'Should return detection object') assert.ok(typeof detection.state === 'string', 'Should have state property') @@ -57,7 +60,7 @@ test('detectState identifies LoggedIn state on rewards domain', async () => { evaluate: () => Promise.resolve(200) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -85,7 +88,7 @@ test('detectState identifies EmailPage state on login.live.com', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -112,7 +115,7 @@ test('detectState identifies PasswordPage state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -139,7 +142,7 @@ test('detectState identifies TwoFactorRequired state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -167,7 +170,7 @@ test('detectState identifies PasskeyPrompt state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -181,7 +184,7 @@ test('detectState identifies Blocked state', async () => { return { first: () => ({ isVisible: () => Promise.resolve(false), - textContent: () => Promise.resolve("We can't sign you in") + textContent: () => Promise.resolve('We can\'t sign you in') }) } } @@ -195,7 +198,7 @@ test('detectState identifies Blocked state', async () => { evaluate: () => Promise.resolve(100) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state') assert.equal(detection.confidence, 'high', 'Should have high confidence') @@ -213,7 +216,7 @@ test('detectState returns Unknown for ambiguous pages', async () => { evaluate: () => Promise.resolve(50) } - const detection = await LoginStateDetector.detectState(mockPage as any) + const detection = await LoginStateDetector.detectState(mockPage as MockPage) assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages') assert.equal(detection.confidence, 'low', 'Should have low confidence') @@ -231,7 +234,7 @@ test('detectState handles errors gracefully', async () => { } try { - await LoginStateDetector.detectState(mockPage as any) + await LoginStateDetector.detectState(mockPage as MockPage) assert.fail('Should throw error') } catch (e) { assert.ok(e instanceof Error, 'Should throw Error instance')