feat: Simplify update process by removing deprecated methods and enhancing README; improve type safety in tests

This commit is contained in:
2025-11-09 22:23:46 +01:00
parent e03761adfc
commit 967801515f
7 changed files with 35 additions and 62 deletions

View File

@@ -33,21 +33,15 @@ npm start
**Automatic update script** that keeps your bot up-to-date with the latest version. **Automatic update script** that keeps your bot up-to-date with the latest version.
**Features:** **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 - Preserves your configuration and accounts
- No merge conflicts with GitHub API method - No merge conflicts, always clean
- Automatic dependency installation and rebuild - Automatic dependency installation and rebuild
**Usage:** **Usage:**
```bash ```bash
# Auto-detect method from config.jsonc # Run update manually
node setup/update/update.mjs 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). **Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc).

View File

@@ -1007,12 +1007,6 @@ export class AccountCreator {
if (!suggestionsContainer) { if (!suggestionsContainer) {
log(false, 'CREATOR', 'No suggestions found from Microsoft', 'warn', 'yellow') 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 // CRITICAL FIX: Generate a new email automatically instead of freezing
log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan') log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan')

View File

@@ -232,14 +232,6 @@ export class MicrosoftRewardsBot {
log('main', 'BANNER', `Microsoft Rewards Bot v${version}`) log('main', 'BANNER', `Microsoft Rewards Bot v${version}`)
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`) 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 { private getVersion(): string {
@@ -770,28 +762,12 @@ export class MicrosoftRewardsBot {
return 0 return 0
} }
const args: string[] = [] // New update.mjs uses GitHub API only and takes no CLI arguments
// 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')
log('main', 'UPDATE', `Running update script: ${scriptRel}`, 'log') log('main', 'UPDATE', `Running update script: ${scriptRel}`, 'log')
// Run update script as a child process and capture exit code // Run update script as a child process and capture exit code
return new Promise<number>((resolve) => { return new Promise<number>((resolve) => {
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' }) const child = spawn(process.execPath, [scriptAbs], { stdio: 'inherit' })
child.on('close', (code) => { child.on('close', (code) => {
log('main', 'UPDATE', `Update script exited with code ${code ?? 0}`, code === 0 ? 'log' : 'warn') log('main', 'UPDATE', `Update script exited with code ${code ?? 0}`, code === 0 ? 'log' : 'warn')
resolve(code ?? 0) resolve(code ?? 0)

View File

@@ -83,11 +83,10 @@ export interface ConfigProxy {
export interface ConfigUpdate { export interface ConfigUpdate {
enabled?: boolean; // Master toggle for auto-updates (default: true) 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 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) 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) 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 { export interface ConfigVacation {

View File

@@ -70,11 +70,10 @@ function stripJsonComments(input: string): string {
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface // Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
function normalizeConfig(raw: unknown): Config { function normalizeConfig(raw: unknown): Config {
// TYPE SAFETY NOTE: Using `any` here is necessary for backwards compatibility // TYPE SAFETY NOTE: Using `any` here is intentional and justified
// JUSTIFIED USE OF `any`: The config format has evolved from flat → nested structure over time // Reason: Config format evolved from flat → nested structure. This function must support BOTH
// This needs to support BOTH formats for backward compatibility with existing user configs // for backward compatibility. Runtime validation via explicit checks ensures safety.
// Runtime validation happens through explicit property checks and the Config interface return type ensures type safety at function boundary // Return type (Config interface) provides type safety at function boundary.
// Alternative approaches (discriminated unions, conditional types) would require extensive runtime checks making code significantly more complex
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const n = (raw || {}) as any const n = (raw || {}) as any

View File

@@ -49,6 +49,14 @@ if (typeof cleanupInterval.unref === 'function') {
cleanupInterval.unref() 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 * Get or create a webhook buffer for the given URL
* Buffers batch log messages to reduce Discord API calls * Buffers batch log messages to reduce Discord API calls

View File

@@ -1,5 +1,5 @@
import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test'
import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector' 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 * Tests for LoginStateDetector - login flow state machine
*/ */
// Type helper for mock Page objects in tests
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
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')
@@ -16,19 +19,19 @@ test('LoginState enum contains expected states', () => {
}) })
test('detectState returns LoginStateDetection structure', async () => { test('detectState returns LoginStateDetection structure', async () => {
// Mock page object // Mock page object with proper Playwright Page interface
const mockPage = { const mockPage = {
url: () => 'https://rewards.bing.com/', url: () => 'https://rewards.bing.com/',
locator: (selector: string) => ({ locator: (_selector: string) => ({
first: () => ({ first: () => ({
isVisible: () => Promise.resolve(true), isVisible: () => Promise.resolve(true),
textContent: () => Promise.resolve('Test') textContent: () => Promise.resolve('Test')
}) })
}), }),
evaluate: () => Promise.resolve(150) evaluate: () => Promise.resolve(150)
} } as unknown as Parameters<typeof LoginStateDetector.detectState>[0]
const detection = await LoginStateDetector.detectState(mockPage as any) const detection = await LoginStateDetector.detectState(mockPage)
assert.ok(detection, 'Should return detection object') assert.ok(detection, 'Should return detection object')
assert.ok(typeof detection.state === 'string', 'Should have state property') 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) 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.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')
@@ -85,7 +88,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 any) const detection = await LoginStateDetector.detectState(mockPage as 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')
@@ -112,7 +115,7 @@ test('detectState identifies PasswordPage state', async () => {
evaluate: () => Promise.resolve(100) 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.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')
@@ -139,7 +142,7 @@ test('detectState identifies TwoFactorRequired state', async () => {
evaluate: () => Promise.resolve(100) 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.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')
@@ -167,7 +170,7 @@ test('detectState identifies PasskeyPrompt state', async () => {
evaluate: () => Promise.resolve(100) 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.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')
@@ -181,7 +184,7 @@ test('detectState identifies Blocked state', async () => {
return { return {
first: () => ({ first: () => ({
isVisible: () => Promise.resolve(false), 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) 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.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')
@@ -213,7 +216,7 @@ test('detectState returns Unknown for ambiguous pages', async () => {
evaluate: () => Promise.resolve(50) 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.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')
@@ -231,7 +234,7 @@ test('detectState handles errors gracefully', async () => {
} }
try { try {
await LoginStateDetector.detectState(mockPage as any) await LoginStateDetector.detectState(mockPage as 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')