diff --git a/.system/abstract_architecture.md b/.system/abstract_architecture.md index e69de29..53762c0 100644 --- a/.system/abstract_architecture.md +++ b/.system/abstract_architecture.md @@ -0,0 +1,174 @@ +# Hexagonal Architecture (Ports & Adapters) – Reference Manual + +## Overview + +Hexagonal Architecture, also known as **Ports and Adapters**, is a software design pattern that isolates an application’s **core business logic** from external systems such as user interfaces, databases, frameworks, and third-party services. + +The application is conceptually placed at the center (often drawn as a hexagon). All communication with the outside world happens through **ports** (abstract interfaces), which are implemented by **adapters**. + +The key idea: +> **The domain does not depend on technology. Technology depends on the domain.** + +--- + +## Fundamental Principles + +### 1. Business Logic First +The core domain represents the real business rules and use cases. It must: +- Be independent of UI, databases, frameworks, and delivery mechanisms +- Express *what* the system does, not *how* it is delivered + +### 2. Explicit Boundaries +All interactions between the core and external systems cross explicit boundaries (ports). This prevents accidental coupling. + +### 3. Dependency Inversion +Dependencies always point **inward**, toward the core. External components depend on abstractions defined by the core. + +--- + +## Core Concepts + +### Domain (Core) +The domain contains: +- Business entities +- Value objects +- Use cases / application services +- Domain rules and policies + +It contains **no technical concerns** such as HTTP, databases, file systems, or frameworks. + +--- + +### Port +A **port** is an abstract interface defined by the core. + +Ports describe: +- What the application *needs* from the outside world (output ports) +- What the application *offers* to the outside world (input ports) + +Ports are defined in the language of the domain, not infrastructure. + +Examples (conceptual): +- “Store an order” +- “Send a notification” +- “Execute a checkout use case” + +--- + +### Adapter +An **adapter** is a concrete implementation of a port using a specific technology. + +Adapters translate: +- External representations → domain concepts +- Domain requests → external system calls + +Adapters are replaceable and exist at the system’s edge. + +#### Adapter Types + +**Primary (Driving) Adapters** +- Initiate interaction with the core +- Examples: Web UI, CLI, REST controller, automated tests + +**Secondary (Driven) Adapters** +- Are used by the core +- Examples: Database repositories, message brokers, email services + +--- + +## Dependency Rule + +- The **core defines ports** +- **Adapters implement ports** +- The core knows nothing about adapters +- Adapters depend on the core, never the reverse + +This rule guarantees that business logic remains stable even when technologies change. + +--- + +## Interaction Flow + +1. A primary adapter receives input (e.g., user action) +2. It calls an **input port** +3. The core executes business logic +4. The core calls **output ports** as needed +5. Secondary adapters fulfill those ports using external systems + +All I/O stays outside the core. + +--- + +## Structuring a System + +A conceptual structure: + +- Core + - Domain entities + - Use cases + - Port interfaces +- Adapters + - Input adapters (UI, API, tests) + - Output adapters (DB, services, files) +- Composition root + - Wires ports to adapters at startup + +This structure is conceptual, not tied to folders or modules. + +--- + +## Testing Strategy + +Hexagonal architecture enables strong testing practices: + +- Test core logic using fake or in-memory adapters +- No need for databases or servers in unit tests +- Integration tests focus on individual adapters + +Testing becomes simpler because dependencies are explicit and replaceable. + +--- + +## Benefits + +- High testability +- Clear separation of concerns +- Technology independence +- Easier maintenance and evolution +- Multiple interfaces over the same core logic +- Strong alignment with Domain-Driven Design + +--- + +## Comparison to Layered Architecture + +Layered Architecture: +- Organizes code by technical layers +- Often allows UI → DB coupling +- Business logic can leak into infrastructure + +Hexagonal Architecture: +- Organizes around the domain +- Enforces strict boundaries +- Treats UI and DB as interchangeable details + +--- + +## When to Use + +Hexagonal architecture is well suited for: +- Medium to large systems +- Long-lived codebases +- Complex business domains +- Systems with multiple interfaces +- Applications that must remain adaptable + +It may be unnecessary for very small or trivial applications. + +--- + +## Summary + +Hexagonal Architecture places the domain at the center and treats all external systems as replaceable plugins. By communicating exclusively through ports and adapters, it ensures long-term flexibility, maintainability, and testability. + +This pattern is language-agnostic and focuses on **design principles**, not frameworks. diff --git a/docs/outcomes/FixAuthSchedulerEnvVars.md b/docs/outcomes/FixAuthSchedulerEnvVars.md new file mode 100644 index 0000000..875906f --- /dev/null +++ b/docs/outcomes/FixAuthSchedulerEnvVars.md @@ -0,0 +1,20 @@ +# Outcome - Fix Auth Scheduler Env Vars + +## Summary +Successfully fixed the environment variable loading issue in the authentication scheduler and updated the frequency configuration to support minutes. + +## Changes +- **Refactored Scheduler Logic:** + - Updated `src/lib/server/scheduler.ts` to use `$env/dynamic/private` for reliable environment variable access. + - Changed configuration from `intervalHours` to `intervalMinutes`. + - Updated `startScheduler` to calculate interval in milliseconds based on minutes. +- **Updated Documentation:** + - Updated `src/hooks.server.ts` JSDoc to reflect the new configuration. +- **Updated Configuration:** + - Updated `.env.local` to set `AUTH_SCHEDULER_INTERVAL_MINUTES=5`. +- **Verified Tests:** + - Updated `src/tests/scheduler.spec.ts` to mock `$env/dynamic/private` and verify the new logic. + - All tests passed. + +## Feature Branch +`feature/FixAuthSchedulerEnvVars` diff --git a/docs/outcomes/FixSchedulerConcurrencyAndBrowser.md b/docs/outcomes/FixSchedulerConcurrencyAndBrowser.md new file mode 100644 index 0000000..9e51e19 --- /dev/null +++ b/docs/outcomes/FixSchedulerConcurrencyAndBrowser.md @@ -0,0 +1,21 @@ +# Outcome - Fix Scheduler Concurrency and Browser Stability + +## Summary +Successfully implemented fixes for the scheduler concurrency issues and browser instability. + +## Changes +- **Scheduler Configuration Validation:** + - Updated `src/lib/server/scheduler.ts` to validate `intervalMinutes`. + - Added a check for `NaN` and a minimum interval of 15 minutes. + - Defaults to 720 minutes if the configuration is invalid. +- **Resource Cleanup:** + - Refactored `renewInstagramAuth` in `src/lib/server/scheduler.ts` to use a `finally` block for closing `page` and `context`. + - Ensures resources are released even if an error occurs during renewal. +- **Robust Browser Management:** + - Updated `src/lib/server/browser.ts` to check `browser.isConnected()`. + - Automatically re-initializes the browser if it is disconnected or crashed. + +## Verification +- The scheduler will now default to a safe interval if misconfigured, preventing console spam. +- Browser crashes will be automatically recovered from on the next scheduler run. +- Resource leaks from failed renewal attempts are prevented. diff --git a/docs/plans/FixSchedulerConcurrencyAndBrowser.md b/docs/plans/FixSchedulerConcurrencyAndBrowser.md new file mode 100644 index 0000000..1c9c507 --- /dev/null +++ b/docs/plans/FixSchedulerConcurrencyAndBrowser.md @@ -0,0 +1,89 @@ +# Execution Plan: Fix Scheduler Concurrency and Browser Stability + +## Context +The application is experiencing two related issues with the Instagram authentication scheduler: +1. **Console Spam**: "Auth renewal already in progress" is logged repeatedly. This indicates the scheduler is triggering new renewal attempts while a previous one is still active (or perceived as active). This is likely caused by an invalid or extremely short interval configuration (e.g., `NaN` resulting from parsing failure). +2. **Browser Instability**: "Target page, context or browser has been closed" errors. This occurs when the scheduler attempts to use a cached Playwright browser instance that has crashed or disconnected. + +## Goal +Ensure the authentication scheduler runs reliably at the configured interval without overlapping executions, and make the browser instance management robust against crashes. + +## Exception to workflow +Do not create a dedicated branch. It's a fix on a new feature. + +## Proposed Solution + +### Story 1: Fix Scheduler Configuration and Resource Cleanup +**Objective**: Prevent rapid-fire execution of the scheduler and ensure browser resources are cleaned up properly even when errors occur. + +**Changes**: +1. **Validate Configuration**: In `src/lib/server/scheduler.ts`, update `getConfig()` to strictly validate `intervalMinutes`. + * Handle `NaN` (parsing errors). + * Enforce a minimum interval (e.g., 15 minutes) to prevent spamming. + * Default to 720 minutes if invalid. +2. **Improve Resource Management**: Refactor `renewInstagramAuth` to ensure `page` and `context` are closed in a `finally` block (or nested `try/finally`), preventing resource leaks if an error occurs during the renewal process. + +**Verification**: +* Set `AUTH_SCHEDULER_INTERVAL_MINUTES` to an invalid value (e.g., "abc") and verify it defaults to 720. +* Verify that `setInterval` is called with a valid duration. + +### Story 2: Robust Browser Lifecycle Management +**Objective**: Ensure the application automatically recovers from browser crashes by detecting disconnected instances. + +**Changes**: +1. **Check Connection Status**: In `src/lib/server/browser.ts`, update `getBrowser()` to check `browser.isConnected()`. +2. **Auto-Recovery**: If the cached browser instance is not connected: + * Log a warning. + * Attempt to close the dead instance (swallowing errors). + * Re-initialize a new browser instance. + +**Verification**: +* Simulate a browser crash (e.g., by manually killing the chrome process if possible, or mocking `isConnected` to return false). +* Verify that the next call to `getBrowser()` creates a new instance instead of throwing. + +## Implementation Details + +### `src/lib/server/scheduler.ts` +```typescript +function getConfig(): SchedulerConfig { + const enabled = env.AUTH_SCHEDULER_ENABLED === 'true'; + let intervalMinutes = parseInt(env.AUTH_SCHEDULER_INTERVAL_MINUTES || '720', 10); + + if (isNaN(intervalMinutes) || intervalMinutes < 15) { + console.warn(`[Scheduler] Invalid or too short interval '${env.AUTH_SCHEDULER_INTERVAL_MINUTES}'. Defaulting to 720 minutes.`); + intervalMinutes = 720; + } + + return { enabled, intervalMinutes }; +} + +// In renewInstagramAuth: +let context = null; +let page = null; +try { + // ... setup ... + context = await browser.newContext(...); + page = await context.newPage(); + // ... logic ... +} catch (e) { + // ... error handling ... +} finally { + if (page) await page.close().catch(() => {}); + if (context) await context.close().catch(() => {}); + state.isRenewing = false; +} +``` + +### `src/lib/server/browser.ts` +```typescript +export async function getBrowser(): Promise { + if (!browser || !browser.isConnected()) { + if (browser) { + console.warn('Browser is disconnected. Re-initializing...'); + try { await browser.close(); } catch (e) { /* ignore */ } + } + return initializeBrowser(); + } + return browser; +} +``` diff --git a/secrets/auth.json b/secrets/auth.json index 7f04ab4..fc44169 100644 --- a/secrets/auth.json +++ b/secrets/auth.json @@ -5,7 +5,7 @@ "value": "SDRORLyWEsWWty2ZoVGdER", "domain": ".instagram.com", "path": "/", - "expires": 1799232681.423721, + "expires": 1800839244.918688, "httpOnly": false, "secure": true, "sameSite": "Lax" @@ -40,39 +40,39 @@ "secure": true, "sameSite": "None" }, - { - "name": "sessionid", - "value": "59661903731%3AbekaIlo4nn7x2n%3A29%3AAYgUJJp9m0KL9O319kXeeujlUYhEn2vNb-kd0dD9Rg", - "domain": ".instagram.com", - "path": "/", - "expires": 1796208680.65293, - "httpOnly": true, - "secure": true, - "sameSite": "Lax" - }, { "name": "ds_user_id", "value": "59661903731", "domain": ".instagram.com", "path": "/", - "expires": 1772448681.423801, + "expires": 1774055244.918777, "httpOnly": false, "secure": true, "sameSite": "None" }, + { + "name": "sessionid", + "value": "59661903731%3AbekaIlo4nn7x2n%3A29%3AAYiHJx9fnG7GZcaJ-BL1hIYE91xYvk2h_5n6NjpiBg", + "domain": ".instagram.com", + "path": "/", + "expires": 1797815010.233987, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, { "name": "wd", "value": "1280x720", "domain": ".instagram.com", "path": "/", - "expires": 1765277481, + "expires": 1766884045, "httpOnly": false, "secure": true, "sameSite": "Lax" }, { "name": "rur", - "value": "\"CLN\\05459661903731\\0541796208682:01fede18d45d4fee86d4f2a276c8a844cb9172b89b51e56eba94c8b8cefaf1f08c566656\"", + "value": "\"CLN\\05459661903731\\0541797815244:01fe3220c89f7ce57e28ead6feec8aed351b809536b4729e55496018e38ea6a7ca601a89\"", "domain": ".instagram.com", "path": "/", "expires": -1, @@ -85,29 +85,41 @@ { "origin": "https://www.instagram.com", "localStorage": [ - { - "name": "signal_flush_timestamp", - "value": "1764672681393" - }, - { - "name": "Session", - "value": "grovm1:1764672716275" - }, { "name": "chatd-deviceid", - "value": "b74919e2-1922-4616-b903-e51f41ac4efe" - }, - { - "name": "has_interop_upgraded", - "value": "{\"lastCheckedAt\":1764672681430,\"status\":false}" + "value": "1b416b56-d780-40db-b542-2a24ed66c77f" }, { "name": "hb_timestamp", - "value": "1764672679702" + "value": "1766279010726" }, { "name": "IGSession", - "value": "4ulad7:1764674481275" + "value": "6m2tlb:1766281044259" + }, + { + "name": "mutex_polaris_banzai", + "value": "t9hvzg:1766279244136" + }, + { + "name": "signal_flush_timestamp", + "value": "1766279010762" + }, + { + "name": "Session", + "value": "dicivj:1766279279259" + }, + { + "name": "has_interop_upgraded", + "value": "{\"lastCheckedAt\":1766279008975,\"status\":false}" + }, + { + "name": "mutex_banzai", + "value": "t9hvzg:1766279244136" + }, + { + "name": "banzai:last_storage_flush", + "value": "1766279009540.7998" } ] } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 99d8966..5b4c5f3 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -7,7 +7,7 @@ import type { ServerInit } from '@sveltejs/kit'; * * Environment variables: * - AUTH_SCHEDULER_ENABLED: Set to 'true' to enable periodic auth renewal - * - AUTH_SCHEDULER_INTERVAL_HOURS: Hours between each renewal (default: 12) + * - AUTH_SCHEDULER_INTERVAL_MINUTES: Minutes between each renewal (default: 720) */ export const init: ServerInit = async () => { console.log('[Server Init] Starting SvelteKit server...'); diff --git a/src/lib/server/browser.ts b/src/lib/server/browser.ts index a37c292..38e4def 100644 --- a/src/lib/server/browser.ts +++ b/src/lib/server/browser.ts @@ -19,7 +19,16 @@ export async function initializeBrowser(): Promise { } export async function getBrowser(): Promise { - if (!browser) { + if (!browser || !browser.isConnected()) { + if (browser) { + console.warn('Browser is disconnected. Re-initializing...'); + try { + await browser.close(); + } catch (e) { + /* ignore */ + } + browser = null; + } return initializeBrowser(); } return browser; diff --git a/src/lib/server/scheduler.ts b/src/lib/server/scheduler.ts index 8f18f55..faf4193 100644 --- a/src/lib/server/scheduler.ts +++ b/src/lib/server/scheduler.ts @@ -1,10 +1,11 @@ import fs from 'fs'; import path from 'path'; import { getBrowser } from './browser'; +import { env } from '$env/dynamic/private'; export interface SchedulerConfig { enabled: boolean; - intervalHours: number; + intervalMinutes: number; } interface SchedulerState { @@ -23,12 +24,19 @@ const state: SchedulerState = { * Get scheduler configuration from environment variables */ function getConfig(): SchedulerConfig { - const enabled = process.env.AUTH_SCHEDULER_ENABLED === 'true'; - const intervalHours = parseInt(process.env.AUTH_SCHEDULER_INTERVAL_HOURS || '12', 10); + const enabled = env.AUTH_SCHEDULER_ENABLED === 'true'; + let intervalMinutes = parseInt(env.AUTH_SCHEDULER_INTERVAL_MINUTES || '720', 10); + + if (isNaN(intervalMinutes) || intervalMinutes < 15) { + console.warn( + `[Scheduler] Invalid or too short interval '${env.AUTH_SCHEDULER_INTERVAL_MINUTES}'. Defaulting to 720 minutes.` + ); + intervalMinutes = 720; + } return { enabled, - intervalHours + intervalMinutes }; } @@ -70,14 +78,17 @@ async function renewInstagramAuth(): Promise { state.isRenewing = true; + let context = null; + let page = null; + try { console.log('[Scheduler] Starting Instagram authentication renewal...'); console.log(`[Scheduler] Loading existing auth from: ${authPath}`); const browser = await getBrowser(); // Load existing authentication state - const context = await browser.newContext({ storageState: authPath }); - const page = await context.newPage(); + context = await browser.newContext({ storageState: authPath }); + page = await context.newPage(); // Navigate to Instagram homepage - the existing auth will be used automatically await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded' }); @@ -88,9 +99,6 @@ async function renewInstagramAuth(): Promise { console.log('[Scheduler] Successfully authenticated with Instagram'); } catch (e) { console.warn('[Scheduler] Home icon not found - session may be expired or invalid'); - await page.close(); - await context.close(); - state.isRenewing = false; return false; } @@ -105,9 +113,6 @@ async function renewInstagramAuth(): Promise { // Update auth.json with refreshed session await context.storageState({ path: authPath }); - await page.close(); - await context.close(); - state.lastRenewalTime = Date.now(); console.log(`[Scheduler] Instagram authentication renewed successfully at ${new Date().toISOString()}`); console.log(`[Scheduler] Auth state updated at: ${authPath}`); @@ -117,6 +122,12 @@ async function renewInstagramAuth(): Promise { console.error('[Scheduler] Instagram authentication renewal failed:', error); return false; } finally { + if (page) { + await page.close().catch(() => {}); + } + if (context) { + await context.close().catch(() => {}); + } state.isRenewing = false; } } @@ -137,9 +148,9 @@ export async function startScheduler(): Promise { return; } - const intervalMs = config.intervalHours * 60 * 60 * 1000; + const intervalMs = config.intervalMinutes * 60 * 1000; - console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalHours}h interval`); + console.log(`[Scheduler] Starting authentication scheduler with ${config.intervalMinutes}min interval`); // Schedule periodic renewals state.intervalId = setInterval(async () => { diff --git a/src/tests/scheduler.spec.ts b/src/tests/scheduler.spec.ts index 1e66212..2246aa3 100644 --- a/src/tests/scheduler.spec.ts +++ b/src/tests/scheduler.spec.ts @@ -2,13 +2,18 @@ import { getSchedulerStatus, startScheduler, stopScheduler } from '$lib/server/s import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Mock environment variables -const setEnv = (key: string, value: string | undefined) => { - if (value === undefined) { - delete process.env[key]; - } else { - process.env[key] = value; - } -}; +const { mockEnv } = vi.hoisted(() => { + return { + mockEnv: { + AUTH_SCHEDULER_ENABLED: 'false', + AUTH_SCHEDULER_INTERVAL_MINUTES: '720' + } + }; +}); + +vi.mock('$env/dynamic/private', () => ({ + env: mockEnv +})); // Mock the browser module vi.mock('$lib/server/browser', () => ({ @@ -28,8 +33,8 @@ const mockFs = { describe('Scheduler Service', () => { beforeEach(() => { // Reset environment variables - setEnv('AUTH_SCHEDULER_ENABLED', undefined); - setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined); + mockEnv.AUTH_SCHEDULER_ENABLED = 'false'; + mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '720'; // Clear all mocks vi.clearAllMocks(); @@ -48,24 +53,24 @@ describe('Scheduler Service', () => { }); describe('Configuration', () => { - it('should use default interval when AUTH_SCHEDULER_INTERVAL_HOURS is not set', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); - setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', undefined); + it('should use default interval when AUTH_SCHEDULER_INTERVAL_MINUTES is not set', async () => { + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; + mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = ''; const status = getSchedulerStatus(); - expect(status.config.intervalHours).toBe(12); + expect(status.config.intervalMinutes).toBe(720); }); - it('should parse custom interval hours from environment', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); - setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '6'); + it('should parse custom interval minutes from environment', async () => { + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; + mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '30'; const status = getSchedulerStatus(); - expect(status.config.intervalHours).toBe(6); + expect(status.config.intervalMinutes).toBe(30); }); it('should disable scheduler when AUTH_SCHEDULER_ENABLED is not true', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'false'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'false'; const status = getSchedulerStatus(); expect(status.config.enabled).toBe(false); @@ -73,7 +78,7 @@ describe('Scheduler Service', () => { }); it('should parse AUTH_SCHEDULER_ENABLED as true when set to "true"', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; const status = getSchedulerStatus(); expect(status.config.enabled).toBe(true); @@ -82,7 +87,7 @@ describe('Scheduler Service', () => { describe('Scheduler Lifecycle', () => { it('should not start when disabled', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'false'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'false'; await startScheduler(); @@ -91,7 +96,7 @@ describe('Scheduler Service', () => { }); it('should start when enabled', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; mockFs.existsSync.mockReturnValue(true); await startScheduler(); @@ -101,7 +106,7 @@ describe('Scheduler Service', () => { }); it('should not start twice', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; mockFs.existsSync.mockReturnValue(true); await startScheduler(); @@ -113,7 +118,7 @@ describe('Scheduler Service', () => { }); it('should stop the scheduler', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; mockFs.existsSync.mockReturnValue(true); await startScheduler(); @@ -132,7 +137,7 @@ describe('Scheduler Service', () => { describe('Status Reporting', () => { it('should return scheduler status with default values', () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'false'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'false'; const status = getSchedulerStatus(); @@ -142,13 +147,13 @@ describe('Scheduler Service', () => { isRenewing: false, config: { enabled: false, - intervalHours: 12 + intervalMinutes: 720 } }); }); it('should report running state correctly', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; mockFs.existsSync.mockReturnValue(true); await startScheduler(); @@ -159,13 +164,13 @@ describe('Scheduler Service', () => { }); it('should track configuration', async () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); - setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', '24'); + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; + mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = '1440'; const status = getSchedulerStatus(); expect(status.config.enabled).toBe(true); - expect(status.config.intervalHours).toBe(24); + expect(status.config.intervalMinutes).toBe(1440); }); }); @@ -187,14 +192,14 @@ describe('Scheduler Service', () => { }); describe('Environment Variables', () => { - it('should handle empty AUTH_SCHEDULER_INTERVAL_HOURS with default', () => { - setEnv('AUTH_SCHEDULER_ENABLED', 'true'); - setEnv('AUTH_SCHEDULER_INTERVAL_HOURS', ''); + it('should handle empty AUTH_SCHEDULER_INTERVAL_MINUTES with default', () => { + mockEnv.AUTH_SCHEDULER_ENABLED = 'true'; + mockEnv.AUTH_SCHEDULER_INTERVAL_MINUTES = ''; const status = getSchedulerStatus(); // Empty string should fall back to default due to parseInt('', 10) returning NaN - // and the || 12 fallback - expect(status.config.intervalHours).toBeDefined(); + // and the || 720 fallback + expect(status.config.intervalMinutes).toBeDefined(); }); }); });